Source code for openflash.basic_region_geometry

# basic_region_geometry.py

from typing import List, Optional
import numpy as np

from .geometry import Geometry, ConcentricBodyGroup
from .body import SteppedBody
from .domain import Domain

[docs] class BasicRegionGeometry(Geometry): """ A geometry where body radii are increasing from the center. This configuration results in a simple, non-overlapping series of circular fluid domains, where the mapping from bodies to domains is trivial. Args: body_arrangement (ConcentricBodyGroup): A group of concentric bodies. h (float): The total water depth. NMK (List[int]): List of the number of harmonics for each resulting domain. """ def __init__(self, body_arrangement: ConcentricBodyGroup, h: float, NMK: List[int]): super().__init__(body_arrangement, h) self.NMK = NMK # --- Assertions --- # Verify that radii are strictly and increasing. all_radii = self.body_arrangement.a if not np.all(np.diff(all_radii) > 0): raise ValueError("Radii 'a' must be strictly increasing for BasicRegionGeometry. Use AnyRegionGeometry for other cases.") # Verify NMK has the correct length (one for each body segment + one for the exterior). if len(NMK) != len(all_radii) + 1: raise ValueError("Length of NMK must be one greater than the total number of body radii.") # FIX 1: Generate the domains and store the final dictionary ONCE during initialization. # The old line was: self._domain_list: List[Domain] = self.make_fluid_domains() domains_as_list = self.make_fluid_domains() self._domain_dict: dict = {domain.index: domain for domain in domains_as_list} @property def domain_list(self) -> dict: """ Returns a dictionary of domains keyed by index. Required for MEEMEngine compatibility. """ # FIX 2: Simply return the dictionary created during __init__. Do not recalculate. return self._domain_dict
[docs] @classmethod def from_vectors(cls, a: np.ndarray, d: np.ndarray, h: float, NMK: List[int], slant_angle: Optional[np.ndarray] = None, body_map: Optional[List[int]] = None, heaving_map: Optional[List[bool]] = None): """ Method to create a BasicRegionGeometry from vector inputs. This is useful for users who prefer to define the geometry directly without explicitly creating Body objects. This version includes robust validation to prevent invalid body_map/heaving_map combinations and enforces the global monotonicity invariant required by BasicRegionGeometry. """ if slant_angle is None: slant_angle = np.zeros_like(a) if body_map is None: body_map = [0] * len(a) # Determine number of bodies from the mapping num_bodies = max(body_map) + 1 # Validate heaving_map length if heaving_map is None: heaving_map = [False] * num_bodies elif len(heaving_map) != num_bodies: raise ValueError( f"Length of heaving_map ({len(heaving_map)}) does not match inferred number of bodies ({num_bodies})" ) # Build radii groups based on body_map and check contiguity + global monotonicity reconstructed = [] last_value = -np.inf # Start with negative infinity for strict monotonicity check for body_idx in range(num_bodies): # Extract indices for this body in original order indices = [j for j, idx in enumerate(body_map) if idx == body_idx] if not indices: raise ValueError(f"Body index {body_idx} is declared in body_map but has no assigned radii.") body_radii = a[indices] # Local monotonicity inside body is not required, but when flattened back, # the entire vector must be strictly increasing to satisfy BasicRegionGeometry rules. for r in body_radii: if r <= last_value: raise ValueError( "Radii 'a' must be strictly increasing after applying body_map. " "Detected non-monotonic or backtracking group arrangement." ) last_value = r reconstructed.append(body_radii) # Now safe to construct bodies bodies = [] for body_idx in range(num_bodies): indices = [j for j, idx in enumerate(body_map) if idx == body_idx] bodies.append(SteppedBody( a=a[indices], d=d[indices], slant_angle=slant_angle[indices], heaving=heaving_map[body_idx] )) arrangement = ConcentricBodyGroup(bodies) return cls(arrangement, h, NMK)
[docs] def make_fluid_domains(self) -> List[Domain]: """ Creates a list of fluid domains for the simple concentric case. """ domains: List[Domain] = [] last_outer_radius = 0.0 arr = self.body_arrangement all_radii = arr.a all_d = arr.d all_heaving = arr.heaving all_slants = arr.slant_angle # Create interior domains under the bodies for i, (outer_r, d, is_heaving, is_slanted) in enumerate(zip(all_radii, all_d, all_heaving, all_slants)): domain = Domain( index=i, NMK=self.NMK[i], a_inner=last_outer_radius, a_outer=outer_r, d_lower=d, geometry_h=self.h, # pass the geometry's total depth heaving=is_heaving, slant=bool(is_slanted), category="interior" ) domains.append(domain) last_outer_radius = outer_r # Create final exterior domain domains.append(Domain( index=len(all_radii), NMK=self.NMK[-1], a_inner=last_outer_radius, a_outer=np.inf, d_lower=0.0, # Seabed geometry_h=self.h, category="exterior" )) return domains