-
Notifications
You must be signed in to change notification settings - Fork 9
[FXC-5449] allow varying refinements on axisymmetric body faces #1882
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
7ddd2f7
bdc0285
d62042e
6dbf834
69c56f8
c7fc526
55c50bd
f77647f
c04f7d6
bdc9aaf
8e45cfc
359b8e1
b19cd53
200f6b4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,7 @@ | |
|
|
||
| # pylint: disable=too-many-lines | ||
|
|
||
| from typing import Literal, Optional, Union | ||
| from typing import Dict, Literal, Optional, Union | ||
|
|
||
| import pydantic as pd | ||
| from typing_extensions import deprecated | ||
|
|
@@ -18,6 +18,7 @@ | |
| Box, | ||
| CustomVolume, | ||
| Cylinder, | ||
| Face, | ||
| GenericVolume, | ||
| GhostSurface, | ||
| MirroredSurface, | ||
|
|
@@ -54,13 +55,17 @@ def __get__(self, obj, owner): | |
| class UniformRefinement(Flow360BaseModel): | ||
| """ | ||
| Uniform spacing refinement inside specified region of mesh. | ||
| For AxisymmetricBody entities, specify per-face spacing overrides via ``face_spacing``. | ||
|
|
||
| Example | ||
| ------- | ||
|
|
||
| >>> fl.UniformRefinement( | ||
| ... entities=[cylinder, box, axisymmetric_body, sphere], | ||
| ... spacing=1*fl.u.cm | ||
| ... spacing=1*fl.u.cm, | ||
| ... face_spacing={ | ||
| ... axisymmetric_body.face(2): 0.2*fl.u.cm, | ||
| ... } | ||
| ... ) | ||
|
|
||
| ==== | ||
|
|
@@ -79,6 +84,32 @@ class UniformRefinement(Flow360BaseModel): | |
| None, | ||
| description="Whether to include the refinement in the surface mesh. Defaults to True when using snappy.", | ||
| ) | ||
| face_spacing: Optional[Dict[str, Dict[int, LengthType.Positive]]] = pd.Field( | ||
| None, | ||
| description="Per-face spacing overrides for AxisymmetricBody entities. " | ||
| "Use `body.face(i)` as keys, where face i is defined by the segment between" | ||
| "profile_curve[i] and profile_curve[i+1]. Faces without overrides use the default `spacing`.", | ||
| ) | ||
|
|
||
| @pd.field_validator("face_spacing", mode="before") | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's not use before validators unless really necessary. Why don't we do this instead? face_spacing: Optional[Dict[AxisSymmetricSegment, LengthType.Positive]] = pd.Field(And then we have: class AxisSymmetricSegment(Flow360BaseModel):
entity_id: str
segment_index: pd.PositiveIntso the Then we do not need this complicate and fragile before validator? |
||
| @classmethod | ||
| def _convert_face_spacing(cls, value): | ||
| """Convert {Face: spacing} to {entity_name: {idx: spacing}} internal format.""" | ||
| if value is None or not isinstance(value, dict): | ||
| return value | ||
| result = {} | ||
| for key, val in value.items(): | ||
| if isinstance(key, Face): | ||
| result.setdefault(key.entity_name, {})[key.index] = val | ||
| elif isinstance(key, str) and isinstance(val, dict): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Face overrides lose entity identityMedium Severity
Additional Locations (2) |
||
| # Already in {name: {idx: spacing}} format (e.g., from JSON deserialization) | ||
| result[key] = {int(k): v for k, v in val.items()} | ||
| else: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mixed key formats drop face overridesLow Severity
Additional Locations (1) |
||
| raise ValueError( | ||
| f"Invalid face_spacing key {key!r}. " | ||
| f"Use body.face(i) or provide a valid serialized face_spacing dict." | ||
| ) | ||
| return result | ||
|
|
||
| @contextual_field_validator("entities", mode="after") | ||
| @classmethod | ||
|
|
@@ -131,6 +162,35 @@ def check_project_to_surface_with_snappy(self, param_info: ParamsValidationInfo) | |
|
|
||
| return self | ||
|
|
||
| @pd.model_validator(mode="after") | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. make this a contextual validator. You can then retrieve the axisymmetric body instance by querying the entity registry. Take a look at |
||
| def check_face_spacing(self): | ||
| """Validate face_spacing keys match AxisymmetricBody entities.""" | ||
| if self.face_spacing is None: | ||
| return self | ||
|
|
||
| entity_map = {} | ||
| if self.entities is not None: | ||
| for entity in self.entities.stored_entities: | ||
| if isinstance(entity, AxisymmetricBody): | ||
| entity_map[entity.name] = entity | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Selector entities rejected in face spacing validationMedium Severity
|
||
| for entity_name, face_overrides in self.face_spacing.items(): | ||
| if entity_name not in entity_map: | ||
| raise ValueError( | ||
| f"face_spacing references '{entity_name}' which is not an " | ||
| f"AxisymmetricBody in this refinement's entities list." | ||
| ) | ||
| entity = entity_map[entity_name] | ||
| num_faces = len(entity.profile_curve) - 1 | ||
| for face_idx in face_overrides: | ||
| if face_idx >= num_faces or face_idx < 0: | ||
| raise ValueError( | ||
| f"Face index {face_idx} for entity '{entity.name}' is out of range. " | ||
| f"Valid range: [0, {num_faces - 1}]." | ||
| ) | ||
|
|
||
| return self | ||
|
|
||
|
|
||
| class StructuredBoxRefinement(Flow360BaseModel): | ||
| """ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -303,6 +303,19 @@ class Edge(EntityBase): | |
| ) | ||
|
|
||
|
|
||
| @final | ||
| class Face(pd.BaseModel): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Face will be reserved for geometry concepts. It is a reserved word. |
||
| """Reference to a specific face of a geometric entity. Currently only supported | ||
| for AxisymmetricBody, for which faces correspond to profile curve segments.""" | ||
|
|
||
| model_config = pd.ConfigDict(frozen=True) | ||
|
|
||
| type_name: Literal["Face"] = pd.Field("Face", frozen=True) | ||
| entity_id: str = pd.Field(description="The private_attribute_id of the owning entity.") | ||
| entity_name: str = pd.Field(description="The name of the owning entity.") | ||
| index: int = pd.Field(ge=0, description="Index along the profile curve (0-based).") | ||
|
|
||
|
|
||
| @final | ||
| class GenericVolume(_VolumeEntityBase): | ||
| """ | ||
|
|
@@ -672,6 +685,13 @@ def _check_profile_curve_has_no_duplicates(cls, curve): | |
|
|
||
| return curve | ||
|
|
||
| def face(self, index: int) -> Face: | ||
| """Return a Face reference for the given index.""" | ||
| num_faces = len(self.profile_curve) - 1 | ||
| if index < 0 or index >= num_faces: | ||
| raise IndexError(f"Face index {index} out of range [0, {num_faces - 1}]") | ||
| return Face(entity_id=self.private_attribute_id, entity_name=self.name, index=index) | ||
|
|
||
| def _apply_transformation(self, matrix: np.ndarray) -> "AxisymmetricBody": | ||
| """Apply 3x4 transformation matrix with uniform scale validation.""" | ||
| new_center, uniform_scale = _validate_uniform_scale_and_transform_center( | ||
|
|
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is too nested and confusing. We need to redesign the interface.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
//User interface
face_spacing={
axisymmetric_body.segments[1] : 0.2*fl.u.cm
}
// Storage side
"face_spacing":{
("entioty_id":"$axisymmetric_body.private_attribute_id", "segment_index": 1) : {"value": 0.2, "units":"cm"}
}
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then you need a Pydantic model for the "segment" class.
It will be similar relationship between Windtunnel farfield and the windtunnel ghost surfaces.