Skip to content
Merged
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
97 changes: 67 additions & 30 deletions flow360/component/simulation/meshing_param/volume_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@
from flow360.exceptions import Flow360ValueError


class classproperty: # pylint: disable=invalid-name,too-few-public-methods
"""Descriptor to create class-level properties that can be accessed from the class itself."""

def __init__(self, func):
self.func = func

def __get__(self, obj, owner):
return self.func(owner)


class UniformRefinement(Flow360BaseModel):
"""
Uniform spacing refinement inside specified region of mesh.
Expand Down Expand Up @@ -614,6 +624,28 @@ def _validate_wheel_belt_ranges(self):
f"Front wheel belt maximum x ({self.front_wheel_belt_x_range[1]}) "
f"must be less than rear wheel belt minimum x ({self.rear_wheel_belt_x_range[0]})."
)

# Central belt is centered at y=0 and extends from -width/2 to +width/2
# It must fit within the inner edges of the wheel belts
front_wheel_inner_edge = self.front_wheel_belt_y_range[0]
rear_wheel_inner_edge = self.rear_wheel_belt_y_range[0]

# Validate central belt width against front wheel belt inner edge
if self.central_belt_width > 2 * front_wheel_inner_edge:
raise ValueError(
f"Central belt width ({self.central_belt_width}) "
f"must be less than or equal to twice the front wheel belt inner edge "
f"(2 × {front_wheel_inner_edge} = {2 * front_wheel_inner_edge})."
)

# Validate central belt width against rear wheel belt inner edge
if self.central_belt_width > 2 * rear_wheel_inner_edge:
raise ValueError(
f"Central belt width ({self.central_belt_width}) "
f"must be less than or equal to twice the rear wheel belt inner edge "
f"(2 × {rear_wheel_inner_edge} = {2 * rear_wheel_inner_edge})."
)

return self


Expand All @@ -638,6 +670,8 @@ class WindTunnelFarfield(_FarfieldBase):
)
"""

model_config = pd.ConfigDict(ignored_types=(classproperty,))

type: Literal["WindTunnelFarfield"] = pd.Field("WindTunnelFarfield", frozen=True)
name: str = pd.Field("Wind Tunnel Farfield", description="Name of the wind tunnel farfield.")

Expand Down Expand Up @@ -679,58 +713,61 @@ def symmetry_plane(self) -> GhostSurface:
)
return GhostSurface(name="symmetric")

@staticmethod
def left():
# pylint: disable=no-self-argument
@classproperty
def left(cls):
"""Return the ghost surface representing the tunnel's left wall."""
return WindTunnelGhostSurface(name="windTunnelLeft")

@staticmethod
def right():
@classproperty
def right(cls):
"""Return the ghost surface representing the tunnel's right wall."""
return WindTunnelGhostSurface(name="windTunnelRight")

@staticmethod
def inlet():
@classproperty
def inlet(cls):
"""Return the ghost surface corresponding to the wind tunnel inlet."""
return WindTunnelGhostSurface(name="windTunnelInlet")

@staticmethod
def outlet():
@classproperty
def outlet(cls):
"""Return the ghost surface corresponding to the wind tunnel outlet."""
return WindTunnelGhostSurface(name="windTunnelOutlet")

@staticmethod
def ceiling():
@classproperty
def ceiling(cls):
"""Return the ghost surface for the tunnel ceiling."""
return WindTunnelGhostSurface(name="windTunnelCeiling")

@staticmethod
def floor():
@classproperty
def floor(cls):
"""Return the ghost surface for the tunnel floor."""
return WindTunnelGhostSurface(name="windTunnelFloor")

@staticmethod
def friction_patch():
@classproperty
def friction_patch(cls):
"""Return the ghost surface for the floor friction patch used by static floors."""
return WindTunnelGhostSurface(name="windTunnelFrictionPatch", used_by=["StaticFloor"])

@staticmethod
def central_belt():
@classproperty
def central_belt(cls):
"""Return the ghost surface used by central and wheel belt floor types."""
return WindTunnelGhostSurface(
name="windTunnelCentralBelt", used_by=["CentralBelt", "WheelBelts"]
)

@staticmethod
def front_wheel_belts():
@classproperty
def front_wheel_belts(cls):
"""Return the ghost surface for the front wheel belt region."""
return WindTunnelGhostSurface(name="windTunnelFrontWheelBelt", used_by=["WheelBelts"])

@staticmethod
def rear_wheel_belts():
@classproperty
def rear_wheel_belts(cls):
"""Return the ghost surface for the rear wheel belt region."""
return WindTunnelGhostSurface(name="windTunnelRearWheelBelt", used_by=["WheelBelts"])

# pylint: enable=no-self-argument

@staticmethod
def _get_valid_ghost_surfaces(
floor_string: Optional[str] = "all", domain_string: Optional[str] = None
Expand All @@ -740,20 +777,20 @@ def _get_valid_ghost_surfaces(
or ``all``, and the domain type as a string.
"""
common_ghost_surfaces = [
WindTunnelFarfield.inlet(),
WindTunnelFarfield.outlet(),
WindTunnelFarfield.ceiling(),
WindTunnelFarfield.floor(),
WindTunnelFarfield.inlet,
WindTunnelFarfield.outlet,
WindTunnelFarfield.ceiling,
WindTunnelFarfield.floor,
]
if domain_string != "half_body_negative_y":
common_ghost_surfaces += [WindTunnelFarfield.right()]
common_ghost_surfaces += [WindTunnelFarfield.right]
if domain_string != "half_body_positive_y":
common_ghost_surfaces += [WindTunnelFarfield.left()]
common_ghost_surfaces += [WindTunnelFarfield.left]
for ghost_surface_type in [
WindTunnelFarfield.friction_patch(),
WindTunnelFarfield.central_belt(),
WindTunnelFarfield.front_wheel_belts(),
WindTunnelFarfield.rear_wheel_belts(),
WindTunnelFarfield.friction_patch,
WindTunnelFarfield.central_belt,
WindTunnelFarfield.front_wheel_belts,
WindTunnelFarfield.rear_wheel_belts,
]:
if floor_string == "all" or floor_string in ghost_surface_type.used_by:
common_ghost_surfaces += [ghost_surface_type]
Expand Down
4 changes: 3 additions & 1 deletion flow360/component/simulation/primitives.py
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,8 @@ class GhostSurface(_SurfaceEntityBase):
All `GhostSurface` entities will be replaced with exact entity instances before simulation.json submission.
"""

name: str = pd.Field(frozen=True)

private_attribute_entity_type_name: Literal["GhostSurface"] = pd.Field(
"GhostSurface", frozen=True
)
Expand All @@ -675,7 +677,7 @@ class WindTunnelGhostSurface(GhostSurface):
Literal["StaticFloor", "FullyMovingFloor", "CentralBelt", "WheelBelts", "all"]
] = pd.Field(default_factory=lambda: ["all"], frozen=True)

def exists(self, validation_info) -> bool:
def exists(self, _) -> bool:
"""Currently, .exists() is only called on automated farfield"""
raise ValueError(".exists should not be called on wind tunnel farfield")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1160,12 +1160,12 @@ def test_wind_tunnel_invalid_dimensions():
):
# wheel belt y outer too large
_ = WindTunnelFarfield(
width=538, # here
width=538,
floor_type=WheelBelts(
central_belt_x_range=(-200, 256),
central_belt_width=120,
front_wheel_belt_x_range=(-30, 50),
front_wheel_belt_y_range=(70, 270), # here
front_wheel_belt_y_range=(70, 270),
rear_wheel_belt_x_range=(260, 380),
rear_wheel_belt_y_range=(70, 120),
),
Expand All @@ -1176,10 +1176,75 @@ def test_wind_tunnel_invalid_dimensions():
width=1024,
floor_type=WheelBelts(
central_belt_x_range=(-100, 105),
central_belt_width=900.1,
central_belt_width=90.1,
front_wheel_belt_x_range=(-30, 50),
front_wheel_belt_y_range=(70, 123),
rear_wheel_belt_x_range=(260, 380),
rear_wheel_belt_y_range=(70, 120),
),
)


def test_central_belt_width_validation():
with CGS_unit_system:
# Test central belt width larger than 2x front wheel belt inner edge
with pytest.raises(
pd.ValidationError,
match=r"must be less than or equal to twice the front wheel belt inner edge",
):
_ = WheelBelts(
central_belt_x_range=(-200, 256),
central_belt_width=150, # Width is 150
front_wheel_belt_x_range=(-30, 50),
front_wheel_belt_y_range=(70, 120), # Inner edge is 70, 2×70 = 140 < 150
rear_wheel_belt_x_range=(260, 380),
rear_wheel_belt_y_range=(80, 170), # Inner edge is 80, 2×80 = 160 > 150
)

# Test central belt width larger than 2x rear wheel belt inner edge
with pytest.raises(
pd.ValidationError,
match=r"must be less than or equal to twice the rear wheel belt inner edge",
):
_ = WheelBelts(
central_belt_x_range=(-200, 256),
central_belt_width=150, # Width is 150
front_wheel_belt_x_range=(-30, 50),
front_wheel_belt_y_range=(80, 170), # Inner edge is 80, 2×80 = 160 > 150
rear_wheel_belt_x_range=(260, 380),
rear_wheel_belt_y_range=(70, 200), # Inner edge is 70, 2×70 = 140 < 150
)

# Test central belt width larger than both inner edges
with pytest.raises(
pd.ValidationError,
match=r"must be less than or equal to twice the front wheel belt inner edge",
):
_ = WheelBelts(
central_belt_x_range=(-200, 256),
central_belt_width=200, # Width is 200
front_wheel_belt_x_range=(-30, 50),
front_wheel_belt_y_range=(90, 120), # Inner edge is 90, 2×90 = 180 < 200
rear_wheel_belt_x_range=(260, 380),
rear_wheel_belt_y_range=(95, 140), # Inner edge is 95, 2×95 = 190 < 200
)

# Legal: central belt width equal to 2x inner edges
_ = WheelBelts(
central_belt_x_range=(-200, 256),
central_belt_width=140, # Width is 140
front_wheel_belt_x_range=(-30, 50),
front_wheel_belt_y_range=(70, 170), # Inner edge is 70, 2×70 = 140
rear_wheel_belt_x_range=(260, 380),
rear_wheel_belt_y_range=(70, 170), # Inner edge is 70, 2×70 = 140
)

# Legal: central belt width less than 2x inner edges
_ = WheelBelts(
central_belt_x_range=(-200, 256),
central_belt_width=100, # Width is 100
front_wheel_belt_x_range=(-30, 50),
front_wheel_belt_y_range=(70, 170), # Inner edge is 70, 2×70 = 140 > 100
rear_wheel_belt_x_range=(260, 380),
rear_wheel_belt_y_range=(80, 190), # Inner edge is 80, 2×80 = 160 > 100
)
Original file line number Diff line number Diff line change
Expand Up @@ -49,22 +49,22 @@ def create_windtunnel_params():
),
fl.SlipWall(
entities=[
wind_tunnel.left(),
wind_tunnel.right(),
wind_tunnel.ceiling(),
wind_tunnel.left,
wind_tunnel.right,
wind_tunnel.ceiling,
]
),
fl.Wall(entities=[wind_tunnel.floor()], use_wall_function=False),
fl.Wall(entities=[wind_tunnel.floor], use_wall_function=False),
fl.Wall(
entities=[
wind_tunnel.central_belt(),
wind_tunnel.front_wheel_belts(),
wind_tunnel.rear_wheel_belts(),
wind_tunnel.central_belt,
wind_tunnel.front_wheel_belts,
wind_tunnel.rear_wheel_belts,
],
velocity=[30 * fl.u.m / fl.u.s, 0 * fl.u.m / fl.u.s, 0 * fl.u.m / fl.u.s],
use_wall_function=True,
),
fl.Freestream(entities=[wind_tunnel.inlet(), wind_tunnel.outlet()]),
fl.Freestream(entities=[wind_tunnel.inlet, wind_tunnel.outlet]),
],
time_stepping=fl.Steady(
CFL=fl.AdaptiveCFL(max=1000),
Expand All @@ -76,10 +76,10 @@ def create_windtunnel_params():
),
fl.SurfaceOutput(
entities=[
wind_tunnel.floor(),
wind_tunnel.central_belt(),
wind_tunnel.front_wheel_belts(),
wind_tunnel.rear_wheel_belts(),
wind_tunnel.floor,
wind_tunnel.central_belt,
wind_tunnel.front_wheel_belts,
wind_tunnel.rear_wheel_belts,
],
output_format="paraview",
output_fields=["primitiveVars", "Cp", "Cf", "yPlus"],
Expand Down
Loading