Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions flow360/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
Fluid,
ForcePerArea,
FromUserDefinedDynamics,
Gravity,
HeatEquationInitialCondition,
NavierStokesInitialCondition,
NavierStokesModifiedRestartSolution,
Expand Down
2 changes: 2 additions & 0 deletions flow360/component/simulation/exposed_units.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"temperature": [],
"delta_temperature": [],
"velocity": [],
"acceleration": [],
"area": [],
"force": [],
"pressure": [],
Expand Down Expand Up @@ -71,6 +72,7 @@
"(length)**2": {"SI": "m**2", "CGS": "cm**2", "Imperial": "ft**2"},
"(length)**(-2)": {"SI": "1/m**2", "CGS": "1/cm**2", "Imperial": "1/ft**2"},
"(length)/(time)": {"SI": "m/s", "CGS": "cm/s", "Imperial": "ft/s"},
"(length)/(time)**2": {"SI": "m/s**2", "CGS": "cm/s**2", "Imperial": "ft/s**2"}, # Acceleration
"(angle)/(time)": ["rad/s", "degree/s", "rpm"], # list --> Unit system agnostic dimensions
"(angle)": ["degree", "rad"],
"(temperature)": {"SI": "K", "CGS": "K", "Imperial": "degF"},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,7 @@ class AutomatedFarfield(_FarfieldBase):
def farfield(self):
"""Returns the farfield boundary surface."""
# Make sure the naming is the same here and what the geometry/surface mesh pipeline generates.
return GhostSurface(name="farfield", private_attribute_id="farfield")
return GhostSurface(name="farField", private_attribute_id="farField")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Farfield name change breaks registry pattern matching

High Severity

The GhostSurface name was changed from "farfield" to "farField", but simulation_params.py still uses the pattern "farfield" in _set_boundary_full_name_with_zone_name. The pattern matching in the entity registry uses case-sensitive regex (^farfield$), which won't match "farField". This means the farfield boundary's private_attribute_full_name will never be set, breaking boundary condition resolution for all users of AutomatedFarfield.

Fix in Cursor Fix in Web


@property
def symmetry_plane(self) -> GhostSurface:
Expand Down
49 changes: 49 additions & 0 deletions flow360/component/simulation/models/volume_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
SeedpointVolume,
)
from flow360.component.simulation.unit_system import (
AccelerationType,
AngleType,
AngularVelocityType,
HeatSourceType,
Expand Down Expand Up @@ -1485,11 +1486,59 @@ def _validate_volumetric_heat_source_for_liquid(
return value


class Gravity(Flow360BaseModel):
"""
:class:`Gravity` class for specifying gravitational body force.

The gravity model applies a body force ρg to the momentum equations and ρ(g·u) to the
energy equation, enabling simulation of buoyancy-driven flows.

Example
-------

Define gravity acting in the negative z-direction with Earth's gravitational acceleration:

>>> fl.Gravity(
... direction=(0, 0, -1),
... magnitude=9.81 * fl.u.m / fl.u.s**2,
... )

Define gravity applied only to specific volume zones:

>>> fl.Gravity(
... entities=[volume_mesh["fluid_zone"]],
... direction=(0, 0, -1),
... magnitude=9.81 * fl.u.m / fl.u.s**2,
... )

====
"""

name: Optional[str] = pd.Field("Gravity", description="Name of the `Gravity` model.")
type: Literal["Gravity"] = pd.Field("Gravity", frozen=True)
entities: Optional[EntityList[GenericVolume]] = pd.Field(
default=None,
alias="volumes",
description="The entity list for the `Gravity` model. "
+ "If not specified, gravity is applied to all fluid zones.",
)
direction: Axis = pd.Field(
description="The direction of the gravitational acceleration vector. "
+ "This vector will be normalized automatically."
)
magnitude: AccelerationType = pd.Field(
description="The magnitude of the gravitational acceleration. "
+ "For Earth's surface gravity, use 9.81 m/s²."
)
private_attribute_id: str = pd.Field(default_factory=generate_uuid, frozen=True)


VolumeModelTypes = Union[
Fluid,
Solid,
ActuatorDisk,
BETDisk,
Rotation,
PorousMedium,
Gravity,
]
6 changes: 6 additions & 0 deletions flow360/component/simulation/simulation_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@
_check_duplicate_entities_in_models,
_check_duplicate_isosurface_names,
_check_duplicate_surface_usage,
_check_gravity_model_conflicts,
_check_hybrid_model_to_use_zonal_enforcement,
_check_low_mach_preconditioner_output,
_check_numerical_dissipation_factor_output,
Expand Down Expand Up @@ -587,6 +588,11 @@ def check_duplicate_entities_in_models(self, param_info: ParamsValidationInfo):
"""Only allow each Surface/Volume entity to appear once in the Surface/Volume model"""
return _check_duplicate_entities_in_models(self, param_info)

@pd.model_validator(mode="after")
def check_gravity_model_conflicts(self):
"""Disallow multiple Gravity models when one applies to all zones"""
return _check_gravity_model_conflicts(self)

@contextual_model_validator(mode="after")
def check_unique_selector_names(self):
"""Ensure all EntitySelector names are unique"""
Expand Down
55 changes: 55 additions & 0 deletions flow360/component/simulation/translator/solver_translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
AngularVelocity,
BETDisk,
Fluid,
Gravity,
NavierStokesInitialCondition,
NavierStokesModifiedRestartSolution,
PorousMedium,
Expand Down Expand Up @@ -1335,6 +1336,43 @@ def porous_media_translator(model: PorousMedium):
return porous_medium


def gravity_entity_info_serializer(volume):
"""Gravity entity serializer"""
if volume is None:
return {"zoneType": "global"}
return {
"zoneType": "mesh",
"zoneName": volume.full_name,
}


def gravity_translator(model: Gravity, params):
"""Gravity translator - converts dimensional gravity to non-dimensional form.

Non-dimensionalization: g* = g * L_ref / a_∞²
where L_ref is the reference length (mesh unit) and a_∞ is the speed of sound.
"""
# Get the magnitude value in m/s²
magnitude = model.magnitude.to("m/s**2").value.item()

# Get direction (already normalized by validator)
direction = model.direction

# Compute the dimensional gravity vector
gravity_vector_dimensional = [d * magnitude for d in direction]

# Compute non-dimensionalization factor: L_ref / a_∞²
base_length = params.base_length.to("m").value
base_velocity = params.base_velocity.to("m/s").value # speed of sound

nondim_factor = base_length / (base_velocity**2)

# Non-dimensionalize the gravity vector
gravity_vector_nondim = [g * nondim_factor for g in gravity_vector_dimensional]

return {"gravityVector": gravity_vector_nondim}


def bet_disk_entity_info_serializer(volume):
"""BET disk entity serializer"""
v = convert_tuples_to_lists(remove_units_in_dict(dump_dict(volume)))
Expand Down Expand Up @@ -2351,6 +2389,23 @@ def get_solver_json(
entity_injection_func=porous_media_entity_info_serializer,
)

##:: Step 9b: Get gravity
if has_instance_in_list(input_params.models, Gravity):
gravity_list = []
for gravity_model in get_all_entries_of_type(input_params.models, Gravity):
gravity_dict = gravity_translator(gravity_model, input_params)
if gravity_model.entities is None:
# Apply to all zones (global)
gravity_dict.update(gravity_entity_info_serializer(None))
gravity_list.append(gravity_dict)
else:
# Apply to specified zones
for entity in gravity_model.entities.stored_entities:
entity_gravity_dict = gravity_dict.copy()
entity_gravity_dict.update(gravity_entity_info_serializer(entity))
gravity_list.append(entity_gravity_dict)
translated["gravity"] = gravity_list

##:: Step 10: Get heat transfer zones
solid_zone_boundaries = set()
if has_instance_in_list(input_params.models, Solid):
Expand Down
27 changes: 27 additions & 0 deletions flow360/component/simulation/unit_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
udim.viscosity = udim.pressure * udim.time
udim.kinematic_viscosity = udim.length * udim.length / udim.time
udim.angular_velocity = udim.angle / udim.time
udim.acceleration = udim.length / udim.time**2
udim.heat_flux = udim.mass / udim.time**3
udim.moment = udim.force * udim.length
udim.heat_source = udim.mass / udim.time**3 / udim.length
Expand Down Expand Up @@ -918,6 +919,16 @@ class _VelocityType(_DimensionedType):
VelocityType = Annotated[_VelocityType, PlainSerializer(_dimensioned_type_serializer)]


class _AccelerationType(_DimensionedType):
""":class: AccelerationType"""

dim = udim.acceleration
dim_name = "acceleration"


AccelerationType = Annotated[_AccelerationType, PlainSerializer(_dimensioned_type_serializer)]


class _AreaType(_DimensionedType):
""":class: AreaType"""

Expand Down Expand Up @@ -1379,6 +1390,13 @@ class Flow360VelocityUnit(_Flow360BaseUnit):
unit_name = "flow360_velocity_unit"


class Flow360AccelerationUnit(_Flow360BaseUnit):
""":class: Flow360AccelerationUnit"""

dimension_type = AccelerationType
unit_name = "flow360_acceleration_unit"


class Flow360AreaUnit(_Flow360BaseUnit):
""":class: Flow360AreaUnit"""

Expand Down Expand Up @@ -1548,6 +1566,7 @@ class BaseSystemType(Enum):
"time",
"temperature",
"velocity",
"acceleration",
"area",
"force",
"pressure",
Expand Down Expand Up @@ -1581,6 +1600,7 @@ class UnitSystem(pd.BaseModel):
time: TimeType = pd.Field()
temperature: AbsoluteTemperatureType = pd.Field()
velocity: VelocityType = pd.Field()
acceleration: AccelerationType = pd.Field()
area: AreaType = pd.Field()
force: ForceType = pd.Field()
pressure: PressureType = pd.Field()
Expand Down Expand Up @@ -1739,6 +1759,7 @@ def __exit__(self, exc_type, exc_val, exc_tb):
flow360_time_unit = Flow360TimeUnit()
flow360_temperature_unit = Flow360TemperatureUnit()
flow360_velocity_unit = Flow360VelocityUnit()
flow360_acceleration_unit = Flow360AccelerationUnit()
flow360_area_unit = Flow360AreaUnit()
flow360_force_unit = Flow360ForceUnit()
flow360_pressure_unit = Flow360PressureUnit()
Expand Down Expand Up @@ -1766,6 +1787,7 @@ def __exit__(self, exc_type, exc_val, exc_tb):
flow360_time_unit,
flow360_temperature_unit,
flow360_velocity_unit,
flow360_acceleration_unit,
flow360_area_unit,
flow360_force_unit,
flow360_pressure_unit,
Expand Down Expand Up @@ -1807,6 +1829,9 @@ class Flow360ConversionUnitSystem(pd.BaseModel):
base_velocity: float = pd.Field(
np.inf, json_schema_extra={"target_dimension": Flow360VelocityUnit}
)
base_acceleration: float = pd.Field(
np.inf, json_schema_extra={"target_dimension": Flow360AccelerationUnit}
)
base_area: float = pd.Field(np.inf, json_schema_extra={"target_dimension": Flow360AreaUnit})
base_force: float = pd.Field(np.inf, json_schema_extra={"target_dimension": Flow360ForceUnit})
base_density: float = pd.Field(
Expand Down Expand Up @@ -1886,6 +1911,7 @@ def __init__(self):
)

conversion_system["velocity"] = "flow360_velocity_unit"
conversion_system["acceleration"] = "flow360_acceleration_unit"
conversion_system["area"] = "flow360_area_unit"
conversion_system["force"] = "flow360_force_unit"
conversion_system["density"] = "flow360_density_unit"
Expand Down Expand Up @@ -1936,6 +1962,7 @@ class _PredefinedUnitSystem(UnitSystem):
time: TimeType = pd.Field(exclude=True)
temperature: AbsoluteTemperatureType = pd.Field(exclude=True)
velocity: VelocityType = pd.Field(exclude=True)
acceleration: AccelerationType = pd.Field(exclude=True)
area: AreaType = pd.Field(exclude=True)
force: ForceType = pd.Field(exclude=True)
pressure: PressureType = pd.Field(exclude=True)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from flow360.component.simulation.models.volume_models import (
ActuatorDisk,
Fluid,
Gravity,
Rotation,
Solid,
)
Expand Down Expand Up @@ -115,7 +116,7 @@ def _check_duplicate_entities_in_models(params, param_info: ParamsValidationInfo
usage = EntityUsageMap()

for model in models:
if hasattr(model, "entities"):
if hasattr(model, "entities") and model.entities is not None:
expanded_entities = param_info.expand_entity_list(model.entities)
# seen_entity_hashes: set[str] = set()
for entity in expanded_entities:
Expand Down Expand Up @@ -145,6 +146,39 @@ def _check_duplicate_entities_in_models(params, param_info: ParamsValidationInfo
return params


def _check_gravity_model_conflicts(params):
"""Check that at most one Gravity model applies to all zones (entities=None).

When a Gravity model has entities=None it applies to every fluid zone,
so having more than one such model or mixing a global one with
zone-specific ones would create conflicting gravity definitions.
"""
if not params.models:
return params

gravity_models = [m for m in params.models if isinstance(m, Gravity)]
if len(gravity_models) <= 1:
return params

global_gravity = [m for m in gravity_models if m.entities is None]

if len(global_gravity) > 1:
raise ValueError(
"Multiple Gravity models with unspecified entities (applying to all zones) "
"are not allowed. Please specify explicit entities for each Gravity model "
"or use a single Gravity model for all zones."
)

if len(global_gravity) == 1:
raise ValueError(
"A Gravity model that applies to all zones (entities not specified) "
"cannot coexist with other Gravity models. Please specify explicit "
"entities for each Gravity model or use a single Gravity model for all zones."
)

return params


def _check_low_mach_preconditioner_output(v):
models = v.models

Expand Down
Loading
Loading