Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
2cdc54e
[SCFD-5659] Select body groups for force distribution plots (#1460)
benflexcompute Oct 2, 2025
3bdde24
SurfaceEdgeRefinement validation check (#1462)
savithru-flex Oct 3, 2025
5eea4e8
Add support for Axisymmetric Sliding Interfaces (#1459)
shreyas-flex Oct 3, 2025
aa87ac8
[FXC-3414] Fix a bug where the private attribute id is included in ha…
benflexcompute Oct 3, 2025
3154da7
Fixed publish workflow failing to import version
benflexcompute Oct 6, 2025
303bb12
Add structured box region to volume meshing params (#1463)
shreyas-flex Oct 7, 2025
53391a2
bugfix (#1468)
shreyas-flex Oct 7, 2025
1a54208
Fix fl.AxisymmetricBody not exposed to users (#1470)
shreyas-flex Oct 7, 2025
c87cfd6
Merge pull request #1469 from flexcompute/main
benflexcompute Oct 8, 2025
17746cd
Limit the maximum pydantic version in pyproject.toml (#1472)
angranl-flex Oct 8, 2025
28f623b
Merge pull request #1473 from flexcompute/main
angranl-flex Oct 8, 2025
3adc64c
Add a slack notification to the sync-main-to-develop workflow (#1477)
angranl-flex Oct 8, 2025
085c865
Merge pull request #1479 from flexcompute/main
angranl-flex Oct 8, 2025
1397777
fix(): stripping file to basename causing file not found and also fak…
benflexcompute Oct 8, 2025
da831f8
Merge pull request #1481 from flexcompute/main
benflexcompute Oct 8, 2025
e687d5e
Merge branch 'develop' into piotr/add-gap-strength-w-snappy
piotrkluba Oct 9, 2025
cd7d0a2
develop rebase fixes
piotrkluba Oct 9, 2025
a6f3532
moved gap treatment strength to params class from the defaults
piotrkluba Oct 9, 2025
92ee5f9
formatter
piotrkluba Oct 9, 2025
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
4 changes: 2 additions & 2 deletions .github/workflows/pypi-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,11 @@ jobs:
run: poetry version ${{ env.RELEASE_VERSION }}
- name: check version
run: |
version=$(poetry run python -c "import flow360; print(flow360.__version__)")
version=$(poetry run python -c "import flow360; print(flow360.version.__version__)")
publish_version="${{ env.RELEASE_VERSION }}"
publish_version="${publish_version:1}"
if [ "$version" != "$publish_version" ]; then
echo "version ${version}!=${publish_version} in flow360.__version__ does not match to release version ${{ inputs.version }}"
echo "version ${version}!=${publish_version} in flow360.version.__version__ does not match to release version ${{ inputs.version }}"
exit 1
fi
- name: Setup pipy token
Expand Down
51 changes: 39 additions & 12 deletions .github/workflows/sync-main-to-develop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -145,19 +145,46 @@ jobs:

- name: Open PR main -> develop (on conflict OR push failure)
if: steps.up.outputs.up_to_date == 'false' && (steps.merge.outputs.conflicted == 'true' || steps.push.outputs.exit_code != '0')
uses: repo-sync/pull-request@v2
id: pr
shell: bash
run: |
PR_BODY=$(cat <<'EOF'
Automated sync needs review:
- Merge conflicted: ${{ steps.merge.outputs.conflicted == 'true' }}
- Direct push exit code: ${{ steps.push.outputs.exit_code || 'n/a' }}
Please resolve and merge to bring `develop` up to date with `main`.
EOF
)
PR_URL=$(gh pr create \
--base develop \
--head main \
--title "Scheduled sync: main → develop" \
--body "$PR_BODY")
echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT"
echo "Created PR: $PR_URL"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Send Slack notification
if: steps.up.outputs.up_to_date == 'false' && (steps.merge.outputs.conflicted == 'true' || steps.push.outputs.exit_code != '0')
uses: slackapi/slack-github-action@v2
with:
source_branch: "main"
destination_branch: "develop"
pr_title: "Scheduled sync: main → develop"
pr_body: |
Automated sync needs review:
- Merge conflicted: ${{ steps.merge.outputs.conflicted == 'true' }}
- Direct push exit code: ${{ steps.push.outputs.exit_code || 'n/a' }}
Please resolve and merge to bring `develop` up to date with `main`.
github_token: ${{ secrets.GITHUB_TOKEN }}
# If using bot PAT, switch to:
# github_token: ${{ secrets.BOT_PAT }}
webhook-type: incoming-webhook
payload: |
{
"text": "<!channel> 🔄 Sync PR Created: main → develop",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "<!channel> 🔄 *Scheduled sync PR created*\n\n<${{ steps.pr.outputs.pr_url }}|View Pull Request>\n\n• Merge conflicted: `${{ steps.merge.outputs.conflicted == 'true' }}`\n• Direct push failed: `${{ steps.push.outputs.exit_code != '0' }}`"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_MERGE_NOTIFICATION_URL }}

- name: Final summary
run: |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
outer_radius=1.0,
height=2.5,
)
sliding_interface = fl.RotationCylinder(
sliding_interface = fl.RotationVolume(
spacing_axial=0.04,
spacing_radial=0.04,
spacing_circumferential=0.04,
Expand Down
26 changes: 20 additions & 6 deletions examples/advanced_simulations/rotorcraft/isolated_propeller.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,21 @@

with fl.SI_unit_system:
rotating_cylinder = fl.Cylinder(
name="Rotating zone", center=[0, 0, 0], axis=[1, 0, 0], outer_radius=2, height=0.8
name="Rotating zone",
center=[0, 0, 0],
axis=[1, 0, 0],
outer_radius=2,
height=0.8,
)
refinement_cylinder = fl.Cylinder(
name="Refinement zone", center=[1.9, 0, 0], axis=[1, 0, 0], outer_radius=2, height=4
name="Refinement zone",
center=[1.9, 0, 0],
axis=[1, 0, 0],
outer_radius=2,
height=4,
)
slice = fl.Slice(name="Slice", normal=[1, 0, 0], origin=[0.6, 0, 0])
volume_zone_rotating_cylinder = fl.RotationCylinder(
volume_zone_rotating_cylinder = fl.RotationVolume(
name="Rotation cylinder",
spacing_axial=0.05,
spacing_radial=0.05,
Expand All @@ -30,11 +38,14 @@
params = fl.SimulationParams(
meshing=fl.MeshingParams(
defaults=fl.MeshingDefaults(
surface_max_edge_length=1, boundary_layer_first_layer_thickness=0.1 * fl.u.mm
surface_max_edge_length=1,
boundary_layer_first_layer_thickness=0.1 * fl.u.mm,
),
refinements=[
fl.UniformRefinement(
name="Uniform refinement", spacing=0.025, entities=[refinement_cylinder]
name="Uniform refinement",
spacing=0.025,
entities=[refinement_cylinder],
)
],
volume_zones=[farfield, volume_zone_rotating_cylinder],
Expand Down Expand Up @@ -63,7 +74,10 @@
step_size=0.0025,
max_pseudo_steps=35,
CFL=fl.AdaptiveCFL(
min=0.1, max=10000, max_relative_change=1, convergence_limiting_factor=0.5
min=0.1,
max=10000,
max_relative_change=1,
convergence_limiting_factor=0.5,
),
),
outputs=[
Expand Down
26 changes: 20 additions & 6 deletions examples/post_processing/field_data/time_averaged_isosurfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,21 @@

with fl.SI_unit_system:
rotating_cylinder = fl.Cylinder(
name="Rotating zone", center=[0, 0, 0], axis=[1, 0, 0], outer_radius=2, height=0.8
name="Rotating zone",
center=[0, 0, 0],
axis=[1, 0, 0],
outer_radius=2,
height=0.8,
)
refinement_cylinder = fl.Cylinder(
name="Refinement zone", center=[1.9, 0, 0], axis=[1, 0, 0], outer_radius=2, height=4
name="Refinement zone",
center=[1.9, 0, 0],
axis=[1, 0, 0],
outer_radius=2,
height=4,
)
slice = fl.Slice(name="Slice", normal=[1, 0, 0], origin=[0.6, 0, 0])
volume_zone_rotating_cylinder = fl.RotationCylinder(
volume_zone_rotating_cylinder = fl.RotationVolume(
name="Rotation cylinder",
spacing_axial=0.05,
spacing_radial=0.05,
Expand All @@ -31,11 +39,14 @@
params = fl.SimulationParams(
meshing=fl.MeshingParams(
defaults=fl.MeshingDefaults(
surface_max_edge_length=1, boundary_layer_first_layer_thickness=0.1 * fl.u.mm
surface_max_edge_length=1,
boundary_layer_first_layer_thickness=0.1 * fl.u.mm,
),
refinements=[
fl.UniformRefinement(
name="Uniform refinement", spacing=0.025, entities=[refinement_cylinder]
name="Uniform refinement",
spacing=0.025,
entities=[refinement_cylinder],
)
],
volume_zones=[farfield, volume_zone_rotating_cylinder],
Expand Down Expand Up @@ -64,7 +75,10 @@
step_size=0.0025,
max_pseudo_steps=35,
CFL=fl.AdaptiveCFL(
min=0.1, max=10000, max_relative_change=1, convergence_limiting_factor=0.5
min=0.1,
max=10000,
max_relative_change=1,
convergence_limiting_factor=0.5,
),
),
outputs=[
Expand Down
6 changes: 6 additions & 0 deletions flow360/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
AutomatedFarfield,
AxisymmetricRefinement,
RotationCylinder,
RotationVolume,
StructuredBoxRefinement,
UniformRefinement,
UserDefinedFarfield,
)
Expand Down Expand Up @@ -156,6 +158,7 @@
VolumeOutput,
)
from flow360.component.simulation.primitives import (
AxisymmetricBody,
Box,
CustomVolume,
Cylinder,
Expand Down Expand Up @@ -215,12 +218,15 @@
"SurfaceRefinement",
"AutomatedFarfield",
"AxisymmetricRefinement",
"StructuredBoxRefinement",
"RotationCylinder",
"RotationVolume",
"UniformRefinement",
"SurfaceEdgeRefinement",
"HeightBasedRefinement",
"ReferenceGeometry",
"Cylinder",
"AxisymmetricBody",
"AerospaceCondition",
"ThermalState",
"LiquidOperatingCondition",
Expand Down
2 changes: 1 addition & 1 deletion flow360/component/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ def submit(self, description="", progress_callback=None, run_async=False) -> Geo
if mesh_parser.is_ugrid() and os.path.isfile(
mesh_parser.get_associated_mapbc_filename()
):
file_name_mapbc = os.path.basename(mesh_parser.get_associated_mapbc_filename())
file_name_mapbc = mesh_parser.get_associated_mapbc_filename()
mapbc_files.append(file_name_mapbc)

# Files with 'main' type are treated as MASTER_FILES and are processed after uploading
Expand Down
29 changes: 25 additions & 4 deletions flow360/component/results/base_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import copy
import os
import re
import shutil
Expand Down Expand Up @@ -93,6 +94,11 @@ def _filter_headers_by_prefix(
A list of headers that satisfy the inclusion/exclusion criteria.
"""

if not include and not exclude:
raise RuntimeError(
"Invalid use of filtering, include and exclude patterns are both empty/None."
)

if suffixes is None:
pattern = re.compile(r"(.*)$")
else:
Expand Down Expand Up @@ -660,18 +666,29 @@ def _filtered_sum(self):
def _create_forces_group(self, entity_groups: dict[str, List[str]]) -> PerEntityResultCSVModel:
"""
Create new CSV model for the given entity groups.
Warning: This function will modify the current instance!!!
"""

def full_name_pattern(word: str) -> re.Pattern:
# Find the pattern that matches the name exactly or the full name (zone/boundary)
return rf"^(?:{re.escape(word)}|[^/]+/{re.escape(word)})$"

self.reload_data() # Remove all the imposed filters
print(">> _x_columns =", self._x_columns)
raw_values = {}
for x_column in self._x_columns:
raw_values[x_column] = np.array(self.raw_values[x_column])

for name, entities in entity_groups.items():
entity_patterns = [full_name_pattern(name) for name in entities]
self.filter(include=entity_patterns)

# generates self.values[f"total{variable}"] for below
try:
self.filter(include=entity_patterns)
except RuntimeError:
# No entities matched the include or exclude patterns
continue

for variable in self._variables:
partial_sum = np.array(self.values[f"total{variable}"])
if f"{name}_{variable}" not in raw_values:
Expand All @@ -682,7 +699,7 @@ def full_name_pattern(word: str) -> re.Pattern:
raw_values = {key: val.tolist() for key, val in raw_values.items()}
entity_groups = {key: sorted(val) for key, val in entity_groups.items()}

return self.from_dict(data=raw_values, group=entity_groups)
return self.__class__.from_dict(data=raw_values, group=entity_groups)

def by_boundary_condition(self, params: SimulationParams) -> PerEntityResultCSVModel:
"""
Expand All @@ -699,7 +716,9 @@ def by_boundary_condition(self, params: SimulationParams) -> PerEntityResultCSVM
entity_groups[boundary_name].extend(
[entity.name for entity in model.entities.stored_entities]
)
return self._create_forces_group(entity_groups=entity_groups)
self_copy = copy.deepcopy(self) # Shield from modifying the current instance
# pylint: disable=protected-access
return self_copy._create_forces_group(entity_groups=entity_groups)

def by_body_group(self, params: SimulationParams) -> PerEntityResultCSVModel:
"""
Expand All @@ -722,7 +741,9 @@ def by_body_group(self, params: SimulationParams) -> PerEntityResultCSVModel:
"please upgrade the project to the latest version and re-run the case."
)
entity_groups = entity_info.get_body_group_to_face_group_name_map()
return self._create_forces_group(entity_groups=entity_groups)
self_copy = copy.deepcopy(self) # Shield from modifying the current instance
# pylint: disable=protected-access
return self_copy._create_forces_group(entity_groups=entity_groups)

def reload_data(self, filter_physical_steps_only: bool = False, include_time: bool = False):
"""
Expand Down
3 changes: 1 addition & 2 deletions flow360/component/results/case_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@
_HEAT_FLUX = "HeatFlux"
_X = "X"
_Y = "Y"
_STRIDE = "stride"
_CUMULATIVE_CD_CURVE = "Cumulative_CD_Curve"
_CD_PER_STRIP = "CD_per_strip"
_CFx_PER_SPAN = "CFx_per_span"
Expand Down Expand Up @@ -276,7 +275,7 @@ class YSlicingForceDistributionResultCSVModel(PerEntityResultCSVModel):

_variables: List[str] = [_CFx_PER_SPAN, _CFz_PER_SPAN, _CMy_PER_SPAN]
_filter_when_zero = [_CFx_PER_SPAN, _CFz_PER_SPAN, _CMy_PER_SPAN]
_x_columns: List[str] = [_Y, _STRIDE]
_x_columns: List[str] = [_Y]


class SurfaceHeatTransferResultCSVModel(PerEntityResultCSVModel, TimeSeriesResultCSVModel):
Expand Down
28 changes: 26 additions & 2 deletions flow360/component/simulation/framework/base_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -544,8 +544,29 @@ def _handle_dict_with_hash(cls, model_dict):

@classmethod
def _calculate_hash(cls, model_dict):
def remove_private_attribute_id(obj):
"""
Recursively remove all 'private_attribute_id' keys from the data structure.
This ensures hash consistency when private_attribute_id contains UUID4 values
that change between runs.
"""
if isinstance(obj, dict):
# Create new dict excluding 'private_attribute_id' keys
return {
key: remove_private_attribute_id(value)
for key, value in obj.items()
if key != "private_attribute_id"
}
if isinstance(obj, list):
# Recursively process list elements
return [remove_private_attribute_id(item) for item in obj]
# Return other types as-is (maintains reference for immutable objects)
return obj

# Remove private_attribute_id before calculating hash
cleaned_dict = remove_private_attribute_id(model_dict)
hasher = hashlib.sha256()
json_string = json.dumps(model_dict, sort_keys=True)
json_string = json.dumps(cleaned_dict, sort_keys=True)
hasher.update(json_string.encode("utf-8"))
return hasher.hexdigest()

Expand Down Expand Up @@ -642,7 +663,10 @@ def preprocess(
required_by = []

solver_values = self._nondimensionalization(
params=params, exclude=exclude, required_by=required_by, registry_lookup=registry_lookup
params=params,
exclude=exclude,
required_by=required_by,
registry_lookup=registry_lookup,
)
for property_name, value in self.__dict__.items():
if property_name in exclude:
Expand Down
13 changes: 13 additions & 0 deletions flow360/component/simulation/meshing_param/edge_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
from flow360.component.simulation.framework.entity_base import EntityList
from flow360.component.simulation.primitives import Edge
from flow360.component.simulation.unit_system import AngleType, LengthType
from flow360.component.simulation.validation.validation_context import (
get_validation_info,
)


class AngleBasedRefinement(Flow360BaseModel):
Expand Down Expand Up @@ -104,3 +107,13 @@ class SurfaceEdgeRefinement(Flow360BaseModel):
description="Method for determining the spacing. See :class:`AngleBasedRefinement`,"
" :class:`HeightBasedRefinement`, :class:`AspectRatioBasedRefinement`, :class:`ProjectAnisoSpacing`",
)

@pd.model_validator(mode="after")
def ensure_not_geometry_ai(self):
"""Ensure that geometry AI is disabled when using this feature."""
validation_info = get_validation_info()
if validation_info is None:
return self
if validation_info.use_geometry_AI:
raise ValueError("SurfaceEdgeRefinement is not currently supported with geometry AI.")
return self
Loading
Loading