Skip to content

Commit 92cc90a

Browse files
Merge branch 'main' into develop
2 parents ce0d3ed + 85fea5a commit 92cc90a

11 files changed

Lines changed: 349 additions & 72 deletions

File tree

flow360/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,12 @@
135135
UserDefinedField,
136136
VolumeOutput,
137137
)
138-
from flow360.component.simulation.primitives import Box, Cylinder, ReferenceGeometry
138+
from flow360.component.simulation.primitives import (
139+
Box,
140+
CustomVolume,
141+
Cylinder,
142+
ReferenceGeometry,
143+
)
139144
from flow360.component.simulation.simulation_params import SimulationParams
140145
from flow360.component.simulation.time_stepping.time_stepping import (
141146
AdaptiveCFL,
@@ -169,6 +174,7 @@
169174
"GeometryRefinement",
170175
"Env",
171176
"Case",
177+
"CustomVolume",
172178
"AngleBasedRefinement",
173179
"AspectRatioBasedRefinement",
174180
"ProjectAnisoSpacing",

flow360/component/project_utils.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
)
2323
from flow360.component.simulation.primitives import (
2424
Box,
25+
CustomVolume,
2526
Cylinder,
2627
Edge,
2728
GeometryBodyGroup,
@@ -223,7 +224,7 @@ def _set_up_params_non_persistent_entity_info(entity_info, params: SimulationPar
223224

224225
entity_registry = params.used_entity_registry
225226
# Creating draft entities
226-
for draft_type in [Box, Cylinder, Point, PointArray, PointArray2D, Slice]:
227+
for draft_type in [Box, Cylinder, Point, PointArray, PointArray2D, Slice, CustomVolume]:
227228
draft_entities = entity_registry.find_by_type(draft_type)
228229
for draft_entity in draft_entities:
229230
if draft_entity not in entity_info.draft_entities:

flow360/component/simulation/meshing_param/params.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
UniformRefinement,
2323
UserDefinedFarfield,
2424
)
25+
from flow360.component.simulation.primitives import CustomVolume
2526
from flow360.component.simulation.unit_system import AngleType, LengthType
2627
from flow360.component.simulation.validation.validation_context import (
2728
SURFACE_MESH,
@@ -46,7 +47,8 @@
4647
]
4748

4849
VolumeZonesTypes = Annotated[
49-
Union[RotationCylinder, AutomatedFarfield, UserDefinedFarfield], pd.Field(discriminator="type")
50+
Union[RotationCylinder, AutomatedFarfield, UserDefinedFarfield, CustomVolume],
51+
pd.Field(discriminator="type"),
5052
]
5153

5254

@@ -317,6 +319,25 @@ def _check_volume_zones_has_farfied(cls, v):
317319

318320
return v
319321

322+
@pd.field_validator("volume_zones", mode="after")
323+
@classmethod
324+
def _check_volume_zones_have_unique_names(cls, v):
325+
"""Ensure there won't be duplicated volume zone names."""
326+
327+
if v is None:
328+
return v
329+
to_be_generated_volume_zone_names = set()
330+
for volume_zone in v:
331+
if not isinstance(volume_zone, CustomVolume):
332+
continue
333+
if volume_zone.name in to_be_generated_volume_zone_names:
334+
raise ValueError(
335+
f"Multiple CustomVolume with the same name `{volume_zone.name}` are not allowed."
336+
)
337+
to_be_generated_volume_zone_names.add(volume_zone.name)
338+
339+
return v
340+
320341
@pd.model_validator(mode="after")
321342
def _check_no_reused_volume_entities(self) -> Self:
322343
"""

flow360/component/simulation/primitives.py

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,21 @@
1818
rotation_matrix_from_axis_and_angle,
1919
)
2020
from flow360.component.simulation.framework.base_model import Flow360BaseModel
21-
from flow360.component.simulation.framework.entity_base import EntityBase, generate_uuid
21+
from flow360.component.simulation.framework.entity_base import (
22+
EntityBase,
23+
EntityList,
24+
generate_uuid,
25+
)
2226
from flow360.component.simulation.framework.multi_constructor_model_base import (
2327
MultiConstructorBaseModel,
2428
)
2529
from flow360.component.simulation.framework.unique_list import UniqueStringList
2630
from flow360.component.simulation.unit_system import AngleType, AreaType, LengthType
2731
from flow360.component.simulation.user_code.core.types import ValueOrExpression
2832
from flow360.component.simulation.utils import BoundingBoxType, model_attribute_unlock
33+
from flow360.component.simulation.validation.validation_context import (
34+
get_validation_info,
35+
)
2936
from flow360.component.types import Axis
3037
from flow360.exceptions import Flow360BoundaryMissingError
3138

@@ -467,7 +474,7 @@ def _will_be_deleted_by_mesher(
467474
# pylint: disable=too-many-arguments, too-many-return-statements
468475
self,
469476
at_least_one_body_transformed: bool,
470-
farfield_method: Optional[Literal["auto", "quasi-3d"]],
477+
farfield_method: Optional[Literal["auto", "quasi-3d", "user-defined"]],
471478
global_bounding_box: Optional[BoundingBoxType],
472479
planar_face_tolerance: Optional[float],
473480
half_model_symmetry_plane_center_y: Optional[float],
@@ -486,6 +493,10 @@ def _will_be_deleted_by_mesher(
486493
# VolumeMesh or Geometry/SurfaceMesh with legacy schema.
487494
return False
488495

496+
if farfield_method == "user-defined":
497+
# Not applicable to user defined farfield
498+
return False
499+
489500
length_tolerance = global_bounding_box.largest_dimension * planar_face_tolerance
490501

491502
if farfield_method == "auto":
@@ -635,4 +646,38 @@ def __str__(self):
635646
return ",".join(sorted([self.pair[0].name, self.pair[1].name]))
636647

637648

638-
VolumeEntityTypes = Union[GenericVolume, Cylinder, Box, str]
649+
@final
650+
class CustomVolume(_VolumeEntityBase):
651+
"""
652+
CustomVolume is a volume zone defined by its surrounding surfaces.
653+
It will be generated by the volume mesher.
654+
"""
655+
656+
private_attribute_entity_type_name: Literal["CustomVolume"] = pd.Field(
657+
"CustomVolume", frozen=True
658+
)
659+
type: Literal["CustomVolume"] = pd.Field("CustomVolume", frozen=True)
660+
boundaries: EntityList[Surface] = pd.Field(
661+
description="The surfaces that define the boundaries of the custom volume."
662+
)
663+
private_attribute_id: str = pd.Field(default_factory=generate_uuid, frozen=True)
664+
665+
@pd.field_validator("boundaries", mode="after")
666+
@classmethod
667+
def ensure_unique_boundary_names(cls, v):
668+
"""Check if the boundaries have different names within a CustomVolume."""
669+
if len(v.stored_entities) != len({boundary.name for boundary in v.stored_entities}):
670+
raise ValueError("The boundaries of a CustomVolume must have different names.")
671+
return v
672+
673+
@pd.model_validator(mode="after")
674+
def ensure_usable(self):
675+
"""Check if the beta mesher is enabled and that the user is using user defined farfield."""
676+
validation_info = get_validation_info()
677+
if validation_info is None:
678+
return self
679+
if validation_info.is_beta_mesher and validation_info.farfield_method == "user-defined":
680+
return self
681+
raise ValueError(
682+
"CustomVolume is only supported when beta mesher and user defined farfield are enabled."
683+
)

flow360/component/simulation/translator/volume_meshing_translator.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
UniformRefinement,
1313
UserDefinedFarfield,
1414
)
15-
from flow360.component.simulation.primitives import Box, Cylinder, Surface
15+
from flow360.component.simulation.primitives import Box, CustomVolume, Cylinder, Surface
1616
from flow360.component.simulation.simulation_params import SimulationParams
1717
from flow360.component.simulation.translator.utils import (
1818
get_global_setting_from_first_instance,
@@ -140,8 +140,27 @@ def rotation_cylinder_entity_injector(entity: Cylinder):
140140
}
141141

142142

143+
def _get_custom_volumes(volume_zones: list):
144+
"""Get translated custom volumes from volume zones."""
145+
custom_volumes = []
146+
for zone in volume_zones:
147+
if isinstance(zone, CustomVolume):
148+
custom_volumes.append(
149+
{
150+
"name": zone.name,
151+
"patches": sorted(
152+
[surface.name for surface in zone.boundaries.stored_entities]
153+
),
154+
}
155+
)
156+
if custom_volumes:
157+
# Sort custom volumes by name
158+
custom_volumes.sort(key=lambda x: x["name"])
159+
return custom_volumes
160+
161+
143162
@preprocess_input
144-
# pylint: disable=unused-argument,too-many-branches
163+
# pylint: disable=unused-argument,too-many-branches,too-many-statements
145164
def get_volume_meshing_json(input_params: SimulationParams, mesh_units):
146165
"""
147166
Get JSON for surface meshing.
@@ -285,4 +304,9 @@ def get_volume_meshing_json(input_params: SimulationParams, mesh_units):
285304
if sliding_interfaces:
286305
translated["slidingInterfaces"] = sliding_interfaces
287306

307+
##:: Step 6: Get custom volumes
308+
custom_volumes = _get_custom_volumes(meshing_params.volume_zones)
309+
if custom_volumes:
310+
translated["zones"] = custom_volumes
311+
288312
return translated

flow360/component/simulation/user_defined_dynamics/user_defined_dynamics.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ def ensure_output_surface_existence(cls, value):
126126
# pylint: disable=protected-access
127127
if isinstance(value, Surface) and value._will_be_deleted_by_mesher(
128128
at_least_one_body_transformed=validation_info.at_least_one_body_transformed,
129-
farfield_method=validation_info.auto_farfield_method,
129+
farfield_method=validation_info.farfield_method,
130130
global_bounding_box=validation_info.global_bounding_box,
131131
planar_face_tolerance=validation_info.planar_face_tolerance,
132132
half_model_symmetry_plane_center_y=validation_info.half_model_symmetry_plane_center_y,

flow360/component/simulation/validation/validation_context.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ class ParamsValidationInfo: # pylint:disable=too-few-public-methods,too-many-in
122122
"""
123123

124124
__slots__ = [
125-
"auto_farfield_method",
125+
"farfield_method",
126126
"is_beta_mesher",
127127
"use_geometry_AI",
128128
"using_liquid_as_material",
@@ -138,7 +138,7 @@ class ParamsValidationInfo: # pylint:disable=too-few-public-methods,too-many-in
138138
]
139139

140140
@classmethod
141-
def _get_auto_farfield_method_(cls, param_as_dict: dict):
141+
def _get_farfield_method_(cls, param_as_dict: dict):
142142
volume_zones = None
143143
try:
144144
if param_as_dict["meshing"]:
@@ -150,6 +150,8 @@ def _get_auto_farfield_method_(cls, param_as_dict: dict):
150150
for zone in volume_zones:
151151
if zone["type"] == "AutomatedFarfield":
152152
return zone["method"]
153+
if zone["type"] == "UserDefinedFarfield":
154+
return "user-defined"
153155
return None
154156

155157
@classmethod
@@ -297,7 +299,7 @@ def _get_at_least_one_body_transformed(cls, param_as_dict: dict): # pylint:disa
297299
return False
298300

299301
def __init__(self, param_as_dict: dict, referenced_expressions: list):
300-
self.auto_farfield_method = self._get_auto_farfield_method_(param_as_dict=param_as_dict)
302+
self.farfield_method = self._get_farfield_method_(param_as_dict=param_as_dict)
301303
self.is_beta_mesher = self._get_is_beta_mesher_(param_as_dict=param_as_dict)
302304
self.use_geometry_AI = self._get_use_geometry_AI_( # pylint:disable=invalid-name
303305
param_as_dict=param_as_dict

flow360/component/simulation/validation/validation_simulation_params.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
TimeAverageSurfaceOutput,
2424
VolumeOutput,
2525
)
26+
from flow360.component.simulation.primitives import CustomVolume
2627
from flow360.component.simulation.time_stepping.time_stepping import Steady, Unsteady
2728
from flow360.component.simulation.utils import is_exact_instance
2829
from flow360.component.simulation.validation.validation_context import (
@@ -311,14 +312,17 @@ def _validate_cht_has_heat_transfer(params):
311312

312313
def _check_complete_boundary_condition_and_unknown_surface(
313314
params,
314-
): # pylint:disable=too-many-branches
315+
): # pylint:disable=too-many-branches, too-many-locals
315316
## Step 1: Get all boundaries patches from asset cache
316317
current_lvls = get_validation_levels() if get_validation_levels() else []
317318
if all(level not in current_lvls for level in (ALL, CASE)):
318319
return params
319320

320321
validation_info = get_validation_info()
321322

323+
if not validation_info:
324+
return params
325+
322326
asset_boundary_entities = params.private_attribute_asset_cache.boundaries
323327

324328
# Filter out the ones that will be deleted by mesher
@@ -359,6 +363,13 @@ def _check_complete_boundary_condition_and_unknown_surface(
359363
if item.name != "symmetric"
360364
]
361365

366+
potential_zone_zone_interfaces = set()
367+
if validation_info.farfield_method == "user-defined":
368+
for zones in params.meshing.volume_zones:
369+
if isinstance(zones, CustomVolume):
370+
for boundary in zones.boundaries.stored_entities:
371+
potential_zone_zone_interfaces.add(boundary.name)
372+
362373
if asset_boundary_entities is None or asset_boundary_entities == []:
363374
raise ValueError("[Internal] Failed to retrieve asset boundaries")
364375

@@ -388,7 +399,7 @@ def _check_complete_boundary_condition_and_unknown_surface(
388399
used_boundaries.add(entity.name)
389400

390401
## Step 3: Use set operations to find missing and unknown boundaries
391-
missing_boundaries = asset_boundaries - used_boundaries
402+
missing_boundaries = asset_boundaries - used_boundaries - potential_zone_zone_interfaces
392403
unknown_boundaries = used_boundaries - asset_boundaries
393404

394405
if missing_boundaries:

flow360/component/simulation/validation/validation_utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ def check_deleted_surface_in_entity_list(value):
7979
surface, Surface
8080
) and surface._will_be_deleted_by_mesher( # pylint:disable=protected-access
8181
at_least_one_body_transformed=validation_info.at_least_one_body_transformed,
82-
farfield_method=validation_info.auto_farfield_method,
82+
farfield_method=validation_info.farfield_method,
8383
global_bounding_box=validation_info.global_bounding_box,
8484
planar_face_tolerance=validation_info.planar_face_tolerance,
8585
half_model_symmetry_plane_center_y=validation_info.half_model_symmetry_plane_center_y,
@@ -108,7 +108,7 @@ def check_deleted_surface_pair(value):
108108
for surface in value.pair:
109109
if surface._will_be_deleted_by_mesher( # pylint:disable=protected-access
110110
at_least_one_body_transformed=validation_info.at_least_one_body_transformed,
111-
farfield_method=validation_info.auto_farfield_method,
111+
farfield_method=validation_info.farfield_method,
112112
global_bounding_box=validation_info.global_bounding_box,
113113
planar_face_tolerance=validation_info.planar_face_tolerance,
114114
half_model_symmetry_plane_center_y=validation_info.half_model_symmetry_plane_center_y,

0 commit comments

Comments
 (0)