From c6892be05a4c09b097f80b7e2869b71ae10724a8 Mon Sep 17 00:00:00 2001 From: Feilin Jia Date: Mon, 3 Nov 2025 10:18:29 -0500 Subject: [PATCH 01/38] tested the import_surface_field_and_integral added example for imported surface trim the path prefix of file_name of imported surface lint update solver version use release-25.7.5 to run workflow using imported surfaces remove solver version from submission Dec 3 start case pipeline can successfully run remove storage name --- .../import_surface_field_and_integral.py | 89 +++++++++++++++++++ flow360/component/project.py | 2 +- flow360/component/project_utils.py | 60 ++++++++++++- flow360/component/simulation/primitives.py | 1 + flow360/examples/__init__.py | 2 + flow360/examples/base_test_case.py | 3 + flow360/examples/oblique_channel.py | 16 ++++ 7 files changed, 170 insertions(+), 3 deletions(-) create mode 100644 examples/post_processing/imported_surfaces/import_surface_field_and_integral.py create mode 100644 flow360/examples/oblique_channel.py diff --git a/examples/post_processing/imported_surfaces/import_surface_field_and_integral.py b/examples/post_processing/imported_surfaces/import_surface_field_and_integral.py new file mode 100644 index 000000000..77bb26870 --- /dev/null +++ b/examples/post_processing/imported_surfaces/import_surface_field_and_integral.py @@ -0,0 +1,89 @@ +import flow360 as fl +from flow360.examples import ObliqueChannel + +ObliqueChannel.get_files() + +project = fl.Project.from_volume_mesh( + ObliqueChannel.mesh_filename, name="Cartesian channel mesh", +) + +volume_mesh = project.volume_mesh + +with fl.SI_unit_system: + op = fl.GenericReferenceCondition.from_mach( + mach=0.3, + ) + massFlowRate = fl.UserVariable( + name="MassFluxProjected", + value=-1 + * fl.solution.density + * fl.math.dot(fl.solution.velocity, fl.solution.node_unit_normal), + ) + massFlowRateIntegral = fl.SurfaceIntegralOutput( + name="MassFluxIntegral", + output_fields=[massFlowRate], + surfaces=volume_mesh["VOLUME/LEFT"], + ) + params = fl.SimulationParams( + operating_condition=op, + models=[ + fl.Fluid( + navier_stokes_solver=fl.NavierStokesSolver(absolute_tolerance=1e-10), + turbulence_model_solver=fl.NoneSolver(), + ), + fl.Inflow( + entities=[volume_mesh["VOLUME/LEFT"]], + total_temperature=op.thermal_state.temperature * 1.018, + velocity_direction=(1.0, 0.0, 0.0), + spec=fl.MassFlowRate( + value=op.velocity_magnitude * op.thermal_state.density * (0.2 * fl.u.m**2) + ), + ), + fl.Outflow( + entities=[volume_mesh["VOLUME/RIGHT"]], + spec=fl.Pressure(op.thermal_state.pressure), + ), + fl.SlipWall( + entities=[ + volume_mesh["VOLUME/FRONT"], + volume_mesh["VOLUME/BACK"], + volume_mesh["VOLUME/TOP"], + volume_mesh["VOLUME/BOTTOM"], + ] + ), + ], + time_stepping=fl.Steady(), + outputs=[ + fl.VolumeOutput( + output_format="paraview", + output_fields=["primitiveVars"], + ), + fl.SurfaceOutput( + output_fields=[ + fl.solution.velocity, + fl.solution.Cp, + ], + surfaces=[ + fl.ImportedSurface( + name="normal", file_name=ObliqueChannel.extra["rectangle_normal"] + ), + fl.ImportedSurface( + name="oblique", file_name=ObliqueChannel.extra["rectangle_oblique"] + ), + ], + ), + fl.SurfaceIntegralOutput( + name="MassFlowRateImportedSurface", + output_fields=[massFlowRate], + surfaces=[ + fl.ImportedSurface( + name="normal", file_name=ObliqueChannel.extra["rectangle_normal"] + ), + fl.ImportedSurface( + name="oblique", file_name=ObliqueChannel.extra["rectangle_oblique"] + ), + ], + ), + ], + ) +project.run_case(params, "test_imported_surfaces_field_and_integral") diff --git a/flow360/component/project.py b/flow360/component/project.py index af8c632b5..a7cd02232 100644 --- a/flow360/component/project.py +++ b/flow360/component/project.py @@ -1590,8 +1590,8 @@ def _run( params.pre_submit_summary() - draft.update_simulation_params(params) upload_imported_surfaces_to_draft(params, draft, fork_from) + draft.update_simulation_params(params) if draft_only: # pylint: disable=import-outside-toplevel diff --git a/flow360/component/project_utils.py b/flow360/component/project_utils.py index 585b6e766..914544d35 100644 --- a/flow360/component/project_utils.py +++ b/flow360/component/project_utils.py @@ -36,6 +36,7 @@ from flow360.component.simulation.unit_system import LengthType from flow360.component.simulation.user_code.core.types import save_user_variables from flow360.component.simulation.utils import model_attribute_unlock +from flow360.component.surface_mesh_v2 import SurfaceMeshV2 from flow360.component.utils import parse_datetime from flow360.exceptions import Flow360ConfigurationError, Flow360ValueError from flow360.log import log @@ -584,5 +585,60 @@ def upload_imported_surfaces_to_draft(params, draft, parent_case): for file_path_to_import in current_draft_surface_file_paths_to_import: file_basename = os.path.basename(file_path_to_import) if file_basename not in parent_existing_imported_file_basenames: - deduplicated_surface_file_paths_to_import.append(file_path_to_import) - draft.upload_imported_surfaces(deduplicated_surface_file_paths_to_import) + deduplicated_surface_file_paths_to_import.append( + {"path": file_path_to_import, "basename": file_basename} + ) + + if not deduplicated_surface_file_paths_to_import: + _normalize_imported_surface_entities(params) + return + + response = draft.post( + json={ + "filenames": [entry["basename"] for entry in deduplicated_surface_file_paths_to_import] + }, + method="imported-surfaces", + ) + + basename_to_surface_mesh_ids: dict[str, list[str]] = {} + for entry, resp in zip(deduplicated_surface_file_paths_to_import, response): + surface_mesh_id = resp.get("surfaceMeshId") + _upload_surface_mesh_resource( + surface_mesh_id=surface_mesh_id, + local_file_path=entry["path"], + ) + basename_to_surface_mesh_ids.setdefault(entry["basename"], []).append(surface_mesh_id) + + _normalize_imported_surface_entities(params, basename_to_surface_mesh_ids) + + +def _upload_surface_mesh_resource(surface_mesh_id: str, local_file_path: str): + if not surface_mesh_id: + raise Flow360ConfigurationError("Surface mesh metadata missing for imported surface upload.") + surface_mesh = SurfaceMeshV2(surface_mesh_id) + remote_file_name = surface_mesh.info.file_name + surface_mesh._webapi._upload_file(remote_file_name=remote_file_name, file_name=local_file_path) + surface_mesh._webapi._complete_upload() + + +def _normalize_imported_surface_entities( + params: SimulationParams, basename_to_surface_mesh_ids: dict[str, list[str]] | None = None +): + if params is None or params.outputs is None: + return + + basename_to_iter = {} + if basename_to_surface_mesh_ids: + basename_to_iter = { + basename: iter(mesh_ids) for basename, mesh_ids in basename_to_surface_mesh_ids.items() + } + + for output in params.outputs: + if isinstance(output, (SurfaceOutput, SurfaceIntegralOutput)): + for surface in output.entities.stored_entities: + if isinstance(surface, ImportedSurface): + file_basename = os.path.basename(surface.file_name) + surface.file_name = file_basename + iterator = basename_to_iter.get(file_basename) + if iterator is not None: + surface.surface_mesh_id = next(iterator, surface.surface_mesh_id) diff --git a/flow360/component/simulation/primitives.py b/flow360/component/simulation/primitives.py index 14e72f209..c34f9d7ad 100644 --- a/flow360/component/simulation/primitives.py +++ b/flow360/component/simulation/primitives.py @@ -633,6 +633,7 @@ class ImportedSurface(EntityBase): "ImportedSurface", frozen=True ) file_name: str + surface_mesh_id: Optional[str] = None class GhostSurface(_SurfaceEntityBase): diff --git a/flow360/examples/__init__.py b/flow360/examples/__init__.py index 732f1cdc8..d6d36878d 100644 --- a/flow360/examples/__init__.py +++ b/flow360/examples/__init__.py @@ -13,6 +13,7 @@ from .isolated_propeller import IsolatedPropeller from .monitors import MonitorsAndSlices from .NLF_airfoil import NLFAirfoil2D +from .oblique_channel import ObliqueChannel from .om6wing import OM6wing from .quadcopter import Quadcopter from .rotating_spheres import RotatingSpheres @@ -60,4 +61,5 @@ "TutorialUDDStructural", "Quadcopter", "XV15_CSM", + "ObliqueChannel", ] diff --git a/flow360/examples/base_test_case.py b/flow360/examples/base_test_case.py index afdcd23b0..02644a741 100644 --- a/flow360/examples/base_test_case.py +++ b/flow360/examples/base_test_case.py @@ -223,3 +223,6 @@ def get_files(cls): cls._get_file(cls.url.geometry, cls._geometry_filename) if hasattr(cls.url, "surface_json"): cls._get_file(cls.url.surface_json, cls._surface_json) + if hasattr(cls.url, "extra"): + for key, value in cls.url.extra.items(): + cls._get_file(value, cls._extra[key]) diff --git a/flow360/examples/oblique_channel.py b/flow360/examples/oblique_channel.py new file mode 100644 index 000000000..85adf7f30 --- /dev/null +++ b/flow360/examples/oblique_channel.py @@ -0,0 +1,16 @@ +""" +Cartesian mesh with oblique boundaries example +""" + +from .base_test_case import BaseTestCase + + +class ObliqueChannel(BaseTestCase): + name = "obliqueChannel" + + class url: + mesh = "https://simcloud-public-1.s3.amazonaws.com/examples/obliqueChannel/cartesian_2d_mesh.oblique.cgns" + extra = { + "rectangle_normal": "https://simcloud-public-1.s3.amazonaws.com/examples/obliqueChannel/rectangle_normal.cgns", + "rectangle_oblique": "https://simcloud-public-1.s3.amazonaws.com/examples/obliqueChannel/rectangle_oblique.cgns", + } From 810e9c5007dbae966007f88a39d8099875116a31 Mon Sep 17 00:00:00 2001 From: Feilin Jia Date: Tue, 16 Dec 2025 00:11:54 +0000 Subject: [PATCH 02/38] update from frontend simulation.json --- flow360/component/simulation/entity_info.py | 1 + flow360/component/simulation/framework/param_utils.py | 5 +++++ flow360/component/simulation/primitives.py | 3 +-- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/flow360/component/simulation/entity_info.py b/flow360/component/simulation/entity_info.py index e4113e6ae..3c8b21faf 100644 --- a/flow360/component/simulation/entity_info.py +++ b/flow360/component/simulation/entity_info.py @@ -155,6 +155,7 @@ class GeometryEntityInfo(EntityInfoModel): description="The default value based on uploaded geometry for geometry_accuracy.", ) + def group_in_registry( self, entity_type_name: Literal["face", "edge", "body", "snappy_body"], diff --git a/flow360/component/simulation/framework/param_utils.py b/flow360/component/simulation/framework/param_utils.py index 3d91f6713..cbe2fa001 100644 --- a/flow360/component/simulation/framework/param_utils.py +++ b/flow360/component/simulation/framework/param_utils.py @@ -15,6 +15,7 @@ from flow360.component.simulation.framework.entity_selector import EntitySelector from flow360.component.simulation.framework.unique_list import UniqueStringList from flow360.component.simulation.primitives import ( + ImportedSurface, _SurfaceEntityBase, _VolumeEntityBase, ) @@ -55,6 +56,10 @@ class AssetCache(Flow360BaseModel): None, description="Collected entity selectors for token reference.", ) + imported_surfaces: Optional[List[Union[ImportedSurface]]] = pd.Field( + None, + description="List of imported surface meshes for post-processing.", + ) @property def boundaries(self): diff --git a/flow360/component/simulation/primitives.py b/flow360/component/simulation/primitives.py index c34f9d7ad..cf6ce85f7 100644 --- a/flow360/component/simulation/primitives.py +++ b/flow360/component/simulation/primitives.py @@ -632,10 +632,9 @@ class ImportedSurface(EntityBase): private_attribute_entity_type_name: Literal["ImportedSurface"] = pd.Field( "ImportedSurface", frozen=True ) - file_name: str + file_name: Optional[str] = None surface_mesh_id: Optional[str] = None - class GhostSurface(_SurfaceEntityBase): """ Represents a boundary surface that may or may not be generated therefore may or may not exist. From f0ea8ddadba3045f683287f00605677010ea5300 Mon Sep 17 00:00:00 2001 From: Feilin Jia Date: Wed, 17 Dec 2025 22:19:00 +0000 Subject: [PATCH 03/38] remove bucket --- flow360/component/simulation/primitives.py | 1 - 1 file changed, 1 deletion(-) diff --git a/flow360/component/simulation/primitives.py b/flow360/component/simulation/primitives.py index cf6ce85f7..6bb173b34 100644 --- a/flow360/component/simulation/primitives.py +++ b/flow360/component/simulation/primitives.py @@ -628,7 +628,6 @@ def _will_be_deleted_by_mesher( class ImportedSurface(EntityBase): """ImportedSurface for post-processing""" - private_attribute_registry_bucket_name: Literal["SurfaceEntityType"] = "SurfaceEntityType" private_attribute_entity_type_name: Literal["ImportedSurface"] = pd.Field( "ImportedSurface", frozen=True ) From ec936dfce4bd42faf740531da014a7c3c5c310c6 Mon Sep 17 00:00:00 2001 From: Angran Date: Wed, 17 Dec 2025 20:41:34 +0000 Subject: [PATCH 04/38] Implementation for uploading geometry dependency --- flow360/cloud/flow360_requests.py | 15 + flow360/component/geometry.py | 344 +++++++++++++++--- flow360/component/project.py | 76 ++++ flow360/component/resource_base.py | 19 + .../component/simulation/web/asset_base.py | 14 +- 5 files changed, 407 insertions(+), 61 deletions(-) diff --git a/flow360/cloud/flow360_requests.py b/flow360/cloud/flow360_requests.py index 76ca7cac7..95197726e 100644 --- a/flow360/cloud/flow360_requests.py +++ b/flow360/cloud/flow360_requests.py @@ -153,6 +153,21 @@ class NewVolumeMeshRequestV2(Flow360RequestsV2): format: Literal["cgns", "aflr3"] = pd_v2.Field(description="data format") +class NewGeometryDependencyRequest(Flow360RequestsV2): + """[Simulation V2] Import geometry dependency to a project's draft.""" + + name: str = pd_v2.Field(description="Geometry dependency name") + project_id: str = pd_v2.Field(alias="projectId") + files: List[GeometryFileMeta] = pd_v2.Field(description="list of files") + draft_id: str = pd_v2.Field(default="", alias="draftId") + tags: List[str] = pd_v2.Field(default=[], description="project tags") + length_unit: Literal["m", "mm", "cm", "inch", "ft"] = pd_v2.Field( + alias="lengthUnit", description="project length unit" + ) + description: str = pd_v2.Field(default="", description="geometry dependency description") + icon: str = pd_v2.Field(default="", description="project description") + + class _Resource(Flow360RequestsV2): type: Literal["Case", "Project"] id: str diff --git a/flow360/component/geometry.py b/flow360/component/geometry.py index 9151bb20b..d39f38739 100644 --- a/flow360/component/geometry.py +++ b/flow360/component/geometry.py @@ -14,6 +14,7 @@ from flow360.cloud.flow360_requests import ( GeometryFileMeta, LengthUnitType, + NewGeometryDependencyRequest, NewGeometryRequest, ) from flow360.cloud.heartbeat import post_upload_heartbeat @@ -23,6 +24,7 @@ AssetMetaBaseModelV2, Flow360Resource, ResourceDraft, + SubmissionMode, ) from flow360.component.simulation.entity_info import GeometryEntityInfo from flow360.component.simulation.folder import Folder @@ -77,36 +79,82 @@ class GeometryMeta(AssetMetaBaseModelV2): """ status: GeometryStatus = pd.Field() # Overshadowing to ensure correct is_final() method + dependency: Optional[bool] = pd.Field(False) class GeometryDraft(ResourceDraft): """ - Geometry Draft component + Unified Geometry Draft component for uploading geometry files. + + This class handles both: + - Creating a new project with geometry as the root asset + - Adding geometry as a dependency to an existing project + + The submission mode is determined by how the draft is created (via factory methods + on the Geometry class) and affects the behavior of the submit() method. + + All geometries are conceptually equivalent - they are components that can be used + to create the final geometry for simulation. The distinction between "root" and + "dependency" is only about where the geometry is uploaded (new project vs existing + project), not about any fundamental difference in the geometry itself. """ # pylint: disable=too-many-arguments def __init__( self, file_names: Union[List[str], str], - project_name: str = None, - solver_version: str = None, length_unit: LengthUnitType = "m", tags: List[str] = None, + # Project root context (for backward compatibility with existing API) + project_name: str = None, + solver_version: str = None, folder: Optional[Folder] = None, ): + """ + Initialize a GeometryDraft with common attributes. + + For creating a new project (root geometry): + Use Geometry.from_file() which sets project_name, solver_version, folder + + For adding to existing project (dependency geometry): + Use Geometry.from_file_for_project() which sets the dependency context + + Parameters + ---------- + file_names : Union[List[str], str] + Path(s) to the geometry file(s) + length_unit : LengthUnitType, optional + Unit of length (default is "m") + tags : List[str], optional + Tags to assign to the geometry (default is None) + project_name : str, optional + Name of the project (for project root mode) + solver_version : str, optional + Solver version (for project root mode) + folder : Optional[Folder], optional + Parent folder (for project root mode) + """ self._file_names = file_names - self.project_name = project_name self.tags = tags if tags is not None else [] self.length_unit = length_unit - self.solver_version = solver_version - self.folder = folder - self._validate() - ResourceDraft.__init__(self) - def _validate(self): + # Submission context - determines behavior of submit() + self._submission_mode: Optional[SubmissionMode] = None + self._submission_context: dict = {} + + # For backward compatibility: if project root params are provided, set context + if project_name is not None or solver_version is not None: + self.set_project_root_context( + project_name=project_name, + solver_version=solver_version, + folder=folder, + ) + self._validate_geometry() + ResourceDraft.__init__(self) def _validate_geometry(self): + """Validate geometry files and length unit.""" if not isinstance(self.file_names, list) or len(self.file_names) == 0: raise Flow360FileError("file_names field has to be a non-empty list.") @@ -119,54 +167,70 @@ def _validate_geometry(self): if not os.path.exists(geometry_file): raise Flow360FileError(f"{geometry_file} not found.") - if self.project_name is None: - self.project_name = os.path.splitext(os.path.basename(self.file_names[0]))[0] - log.warning( - "`project_name` is not provided. " - f"Using the first geometry file name {self.project_name} as project name." - ) - if self.length_unit not in LengthUnitType.__args__: raise Flow360ValueError( f"specified length_unit : {self.length_unit} is invalid. " f"Valid options are: {list(LengthUnitType.__args__)}" ) - if self.solver_version is None: - raise Flow360ValueError("solver_version field is required.") - @property def file_names(self) -> List[str]: - """geometry file""" + """Geometry file paths as a list.""" if isinstance(self._file_names, str): return [self._file_names] return self._file_names - # pylint: disable=protected-access - # pylint: disable=duplicate-code - def submit(self, description="", progress_callback=None, run_async=False) -> Geometry: + def set_project_root_context( + self, + project_name: str, + solver_version: str, + folder: Optional[Folder] = None, + ) -> None: """ - Submit geometry to cloud and create a new project + Configure this draft to create a new project with geometry as root. - Parameters - ---------- - description : str, optional - description of the project, by default "" - progress_callback : callback, optional - Use for custom progress bar, by default None - run_async : bool, optional - Whether to submit Geometry asynchronously (default is False). - - Returns - ------- - Geometry - Geometry object with id + Called internally by Geometry.from_file(). + """ + self._submission_mode = SubmissionMode.PROJECT_ROOT + self._submission_context = { + "project_name": project_name, + "solver_version": solver_version, + "folder": folder, + } + + def set_dependency_context( + self, + name: str, + project_id: str, + ) -> None: """ + Configure this draft to add geometry to an existing project. - self._validate() + Called internally by Geometry.from_file_for_project(). + """ + self._submission_mode = SubmissionMode.PROJECT_DEPENDENCY + self._submission_context = { + "name": name, + "project_id": project_id, + } + + def _validate_project_root_context(self): + """Validate context for project root submission.""" + project_name = self._submission_context.get("project_name") + solver_version = self._submission_context.get("solver_version") + + if project_name is None: + project_name = os.path.splitext(os.path.basename(self.file_names[0]))[0] + self._submission_context["project_name"] = project_name + log.warning( + "`project_name` is not provided. " + f"Using the first geometry file name {project_name} as project name." + ) + if solver_version is None: + raise Flow360ValueError("solver_version field is required.") - if not shared_account_confirm_proceed(): - raise Flow360ValueError("User aborted resource submit.") + def _preprocess_mapbc_files(self) -> List[str]: + """Find and return associated mapbc files for UGRID geometry files.""" mapbc_files = [] for file_path in self.file_names: mesh_parser = MeshNameParser(file_path) @@ -175,12 +239,19 @@ def submit(self, description="", progress_callback=None, run_async=False) -> Geo ): file_name_mapbc = mesh_parser.get_associated_mapbc_filename() mapbc_files.append(file_name_mapbc) + return mapbc_files + + def _create_project_root_resource( + self, mapbc_files: List[str], description: str = "" + ) -> GeometryMeta: + """Create a new geometry resource that will be the root of a new project.""" + project_name = self._submission_context["project_name"] + solver_version = self._submission_context["solver_version"] + folder = self._submission_context.get("folder") - # Files with 'main' type are treated as MASTER_FILES and are processed after uploading - # 'dependency' type files are uploaded only but not processed. req = NewGeometryRequest( - name=self.project_name, - solver_version=self.solver_version, + name=project_name, + solver_version=solver_version, tags=self.tags, files=[ GeometryFileMeta( @@ -189,38 +260,149 @@ def submit(self, description="", progress_callback=None, run_async=False) -> Geo ) for file_path in self.file_names + mapbc_files ], - parent_folder_id=self.folder.id if self.folder else "ROOT.FLOW360", + parent_folder_id=folder.id if folder else "ROOT.FLOW360", length_unit=self.length_unit, description=description, ) - ##:: Create new Geometry resource and project resp = RestApi(GeometryInterface.endpoint).post(req.dict()) - info = GeometryMeta(**resp) + return GeometryMeta(**resp) + + def _create_dependency_resource( + self, mapbc_files: List[str], description: str = "", draft_id: str = "", icon: str = "" + ) -> GeometryMeta: + """Create a geometry resource as a dependency in an existing project.""" + name = self._submission_context["name"] + project_id = self._submission_context["project_id"] + + req = NewGeometryDependencyRequest( + name=name, + project_id=project_id, + draft_id=draft_id, + files=[ + GeometryFileMeta( + name=os.path.basename(file_path), + type="main", + ) + for file_path in self.file_names + mapbc_files + ], + length_unit=self.length_unit, + tags=self.tags, + description=description, + icon=icon, + ) + + resp = RestApi(GeometryInterface.endpoint).post(req.dict(), method="dependency") + return GeometryMeta(**resp) - ##:: upload geometry files + def _upload_files( + self, + info: GeometryMeta, + mapbc_files: List[str], + progress_callback=None, + ) -> Geometry: + """Upload geometry files to the cloud.""" + # pylint: disable=protected-access geometry = Geometry(info.id) heartbeat_info = {"resourceId": info.id, "resourceType": "Geometry", "stop": False} + # Keep posting the heartbeat to keep server patient about uploading. heartbeat_thread = threading.Thread(target=post_upload_heartbeat, args=(heartbeat_info,)) heartbeat_thread.start() - for file_path in self.file_names + mapbc_files: - geometry._webapi._upload_file( - remote_file_name=os.path.basename(file_path), - file_name=file_path, - progress_callback=progress_callback, - ) - heartbeat_info["stop"] = True - heartbeat_thread.join() - ##:: kick off pipeline + + try: + for file_path in self.file_names + mapbc_files: + geometry._webapi._upload_file( + remote_file_name=os.path.basename(file_path), + file_name=file_path, + progress_callback=progress_callback, + ) + finally: + heartbeat_info["stop"] = True + heartbeat_thread.join() + + # Kick off pipeline geometry._webapi._complete_upload() - log.info(f"Geometry successfully submitted: {geometry.short_description()}") - # setting _id will disable "WARNING: You have not submitted..." warning message + + # Setting _id will disable "WARNING: You have not submitted..." warning message self._id = info.id + + return geometry + + # pylint: disable=duplicate-code + def submit( + self, + description: str = "", + progress_callback=None, + run_async: bool = False, + draft_id: str = "", + icon: str = "", + ) -> Geometry: + """ + Submit geometry to cloud. + + The behavior depends on how this draft was created: + - If created via Geometry.from_file(): Creates a new project with this geometry as root + - If created via Geometry.from_file_for_project(): Adds geometry to an existing project + + Parameters + ---------- + description : str, optional + Description of the geometry/project (default is "") + progress_callback : callback, optional + Use for custom progress bar (default is None) + run_async : bool, optional + Whether to return immediately after upload without waiting for processing + (default is False) + draft_id : str, optional + ID of the draft to add geometry to (only used for dependency mode, default is "") + icon : str, optional + Icon for the geometry (only used for dependency mode, default is "") + + Returns + ------- + Geometry + Geometry object with id + + Raises + ------ + Flow360ValueError + If submission context is not set or user aborts + """ + if self._submission_mode is None: + raise Flow360ValueError( + "Submission context not set. Use Geometry.from_file() or " + "Geometry.from_file_for_project() to create a properly configured draft." + ) + + self._validate_geometry() + + # Validate project root context if applicable + if self._submission_mode == SubmissionMode.PROJECT_ROOT: + self._validate_project_root_context() + + if not shared_account_confirm_proceed(): + raise Flow360ValueError("User aborted resource submit.") + + mapbc_files = self._preprocess_mapbc_files() + + # Create the geometry resource based on submission mode + if self._submission_mode == SubmissionMode.PROJECT_ROOT: + info = self._create_project_root_resource(mapbc_files, description) + log_message = "Geometry successfully submitted" + else: + info = self._create_dependency_resource(mapbc_files, description, draft_id, icon) + log_message = "Supplementary geometry resources successfully submitted" + + # Upload files + geometry = self._upload_files(info, mapbc_files, progress_callback) + + log.info(f"{log_message}: {geometry.short_description()}") + if run_async: return geometry + log.info("Waiting for geometry to be processed.") - # uses from_cloud to ensure all metadata is ready before yielding the object return Geometry.from_cloud(info.id) @@ -321,6 +503,48 @@ def from_file( file_names, project_name, solver_version, length_unit, tags, folder=folder ) + @classmethod + # pylint: disable=too-many-arguments + def from_file_for_project( + cls, + name: str, + file_names: Union[List[str], str], + project_id: str, + length_unit: LengthUnitType = "m", + tags: List[str] = None, + ) -> GeometryDraft: + """ + Create a geometry draft for adding to an existing project. + + This creates a geometry that will be added as a supplementary component + (dependency) to an existing project, rather than creating a new project. + + Parameters + ---------- + name : str + Name for the geometry component + file_names : Union[List[str], str] + Path(s) to the geometry file(s) + project_id : str + ID of the existing project to add this geometry to + length_unit : LengthUnitType, optional + Unit of length (default is "m") + tags : List[str], optional + Tags to assign to the geometry (default is None) + + Returns + ------- + GeometryDraft + A draft configured for submission to an existing project + """ + draft = GeometryDraft( + file_names=file_names, + length_unit=length_unit, + tags=tags, + ) + draft.set_dependency_context(name=name, project_id=project_id) + return draft + def show_available_groupings(self, verbose_mode: bool = False): """Display all the possible groupings for faces and edges""" self._show_available_entity_groups( diff --git a/flow360/component/project.py b/flow360/component/project.py index a7cd02232..5a18f31d9 100644 --- a/flow360/component/project.py +++ b/flow360/component/project.py @@ -1157,6 +1157,82 @@ def _detect_input_file_type(file: Union[str, list[str]]): run_async=run_async, ) + def _import_dependency_resource_from_file( + self, + *, + files: Union[GeometryFiles, SurfaceMeshFile], + name: str = None, + length_unit: LengthUnitType = "m", + tags: List[str] = None, + run_async: bool = False, + ): + # pylint:disable = protected-access + files._check_files_existence() + + if isinstance(files, GeometryFiles): + draft = Geometry.from_file_for_project( + name=name, + file_names=files.file_names, + project_id=self.id, + length_unit=length_unit, + tags=tags, + ) + else: + draft = None + + dependency_resource = draft.submit(run_async=run_async) + return dependency_resource + + def import_geometry_dependency_from_file( + self, + file: Union[str, list[str]], + /, + name: str = None, + length_unit: LengthUnitType = "m", + tags: List[str] = None, + run_async: bool = False, + ): + """ + Imports a geometry dependency resource from local geometry files into the project. + + Parameters + ---------- + file : Union[str, list[str]] (positional argument only) + Geometry file paths. + name : str, optional + Name of the geometry dependency resource (default is None). + length_unit : LengthUnitType, optional + Unit of length (default is "m"). + tags : list of str, optional + Tags to assign to the geometry dependency resource (default is None). + run_async : bool, optional + Whether to create the geometry dependency resource asynchronously (default is False). + + Returns + ------- + Geometry + An instance of the geometry resource added to the project. + + Raises + ------ + Flow360FileError + If the geometry dependency resource cannot be initialized from the file. + """ + + try: + validated_files = GeometryFiles(file_names=file) + except pd.ValidationError as err: + # pylint:disable = raise-missing-from + raise Flow360FileError(f"Geometry file error: {str(err)}") + + return self._import_dependency_resource_from_file( + files=validated_files, + name=name, + length_unit=length_unit, + tags=tags, + run_async=run_async, + ) + @classmethod def _get_user_requested_entity_info( cls, diff --git a/flow360/component/resource_base.py b/flow360/component/resource_base.py index 86520ea11..0ff9e840f 100644 --- a/flow360/component/resource_base.py +++ b/flow360/component/resource_base.py @@ -27,6 +27,23 @@ from flow360.log import LogLevel, log +class SubmissionMode(Enum): + """ + Enum to identify the submission mode for asset drafts (Geometry, SurfaceMesh, etc.). + + This is used to determine whether a draft should create a new project (as root asset) + or be added to an existing project (as a dependency/component). + + All assets are conceptually equivalent - they are components that can be used + to create the final simulation setup. The distinction between "root" and + "dependency" is only about where the asset is uploaded (new project vs existing + project), not about any fundamental difference in the asset itself. + """ + + PROJECT_ROOT = "project_root" # Creates a new project with this asset as root + PROJECT_DEPENDENCY = "project_dependency" # Adds asset to an existing project + + # pylint: disable=R0801 class Flow360Status(Enum): """ @@ -138,6 +155,8 @@ class AssetMetaBaseModelV2(pd_v2.BaseModel): updated_by: Optional[str] = pd_v2.Field(None, alias="updatedBy") deleted: bool cloud_path_prefix: Optional[str] = None + geometry_dependencies: Optional[List] = pd_v2.Field(None, alias="geometryDependencies") + surface_mesh_dependencies: Optional[List] = pd_v2.Field(None, alias="surfaceMeshDependencies") model_config = pd_v2.ConfigDict(extra="allow", frozen=True, populate_by_name=True) diff --git a/flow360/component/simulation/web/asset_base.py b/flow360/component/simulation/web/asset_base.py index 66a42f86a..f41c1877e 100644 --- a/flow360/component/simulation/web/asset_base.py +++ b/flow360/component/simulation/web/asset_base.py @@ -171,7 +171,19 @@ def _get_simulation_json(cls, asset: AssetBase, clean_front_end_keys: bool = Fal ##>> Check if the current asset is project's root item. ##>> If so then we need to wait for its pipeline to finish generating the simulation json. _resp = RestApi(ProjectInterface.endpoint, id=asset.project_id).get() - if asset.id == _resp["rootItemId"]: + dependency_ids = [] + # pylint: disable=protected-access + if asset._cloud_resource_type_name in ["Geometry", "SurfaceMesh"]: + _resp_dependency = RestApi(ProjectInterface.endpoint, id=asset.project_id).get( + method="dependency" + ) + _dependency_resources = ( + _resp_dependency["geometryDependencyResources"] + if asset._cloud_resource_type_name == "Geometry" + else _resp_dependency["surfaceMeshDependencyResources"] + ) + dependency_ids = [_item["id"] for _item in _dependency_resources] + if asset.id == _resp["rootItemId"] or asset.id in dependency_ids: log.debug("Current asset is project's root item. Waiting for pipeline to finish.") # pylint: disable=protected-access asset.wait() From 649e02ac219d98b2f063d520f8020d067f032fd4 Mon Sep 17 00:00:00 2001 From: Angran Date: Wed, 17 Dec 2025 21:52:00 +0000 Subject: [PATCH 05/38] Implementation for uploading surface mesh dependency --- flow360/cloud/flow360_requests.py | 15 ++ flow360/component/project.py | 80 +++++- flow360/component/surface_mesh_v2.py | 359 +++++++++++++++++++++------ 3 files changed, 376 insertions(+), 78 deletions(-) diff --git a/flow360/cloud/flow360_requests.py b/flow360/cloud/flow360_requests.py index 95197726e..79410ed38 100644 --- a/flow360/cloud/flow360_requests.py +++ b/flow360/cloud/flow360_requests.py @@ -168,6 +168,21 @@ class NewGeometryDependencyRequest(Flow360RequestsV2): icon: str = pd_v2.Field(default="", description="project description") +class NewSurfaceMeshDependencyRequest(Flow360RequestsV2): + """[Simulation V2] Import surface mesh dependency to a project's draft.""" + + name: str = pd_v2.Field(description="Surface mesh dependency name") + project_id: str = pd_v2.Field(alias="projectId") + draft_id: str = pd_v2.Field(default="", alias="draftId") + tags: List[str] = pd_v2.Field(default=[], description="project tags") + file_name: str = pd_v2.Field(alias="fileName", description="Surface mesh file name") + length_unit: Literal["m", "mm", "cm", "inch", "ft"] = pd_v2.Field( + alias="lengthUnit", description="project length unit" + ) + description: str = pd_v2.Field(default="", description="geometry dependency description") + icon: str = pd_v2.Field(default="", description="project description") + + class _Resource(Flow360RequestsV2): type: Literal["Case", "Project"] id: str diff --git a/flow360/component/project.py b/flow360/component/project.py index 5a18f31d9..8732b1b31 100644 --- a/flow360/component/project.py +++ b/flow360/component/project.py @@ -43,6 +43,7 @@ ) from flow360.component.simulation.entity_info import GeometryEntityInfo from flow360.component.simulation.folder import Folder +from flow360.component.simulation.primitives import ImportedSurface from flow360.component.simulation.simulation_params import SimulationParams from flow360.component.simulation.unit_system import LengthType from flow360.component.simulation.web.asset_base import AssetBase @@ -1161,7 +1162,7 @@ def _import_dependency_resource_from_file( self, *, files: Union[GeometryFiles, SurfaceMeshFile], - name: str = None, + name: str, length_unit: LengthUnitType = "m", tags: List[str] = None, run_async: bool = False, @@ -1177,8 +1178,16 @@ def _import_dependency_resource_from_file( length_unit=length_unit, tags=tags, ) + elif isinstance(files, SurfaceMeshFile): + draft = SurfaceMeshV2.from_file_for_project( + name=name, + file_name=files.file_names, + project_id=self.id, + length_unit=length_unit, + tags=tags, + ) else: - draft = None + raise Flow360ValueError(f"Unsupported file type: {type(files)}") dependency_resource = draft.submit(run_async=run_async) return dependency_resource @@ -1187,7 +1196,7 @@ def import_geometry_dependency_from_file( self, file: Union[str, list[str]], /, - name: str = None, + name: str, length_unit: LengthUnitType = "m", tags: List[str] = None, run_async: bool = False, @@ -1199,8 +1208,8 @@ def import_geometry_dependency_from_file( ---------- file : Union[str, list[str]] (positional argument only) Geometry file paths. - name : str, optional - Name of the geometry dependency resource (default is None). + name : str + Name of the geometry dependency resource. length_unit : LengthUnitType, optional Unit of length (default is "m"). tags : list of str, optional @@ -1233,6 +1242,67 @@ def import_geometry_dependency_from_file( run_async=run_async, ) + def import_surface_mesh_dependency_from_file( + self, + file: str, + /, + name: str, + length_unit: LengthUnitType = "m", + tags: List[str] = None, + run_async: bool = False, + ) -> ImportedSurface: + """ + Imports a surface mesh dependency resource from a local surface mesh file into the project. + + Parameters + ---------- + file : str (positional argument only) + Surface mesh file path. + name : str + Name of the surface mesh dependency resource. + length_unit : LengthUnitType, optional + Unit of length (default is "m"). + tags : list of str, optional + Tags to assign to the surface mesh dependency resource (default is None). + run_async : bool, optional + Whether to create the surface mesh dependency resource asynchronously (default is False). + + Returns + ------- + ImportedSurface + An ImportedSurface object with the file name and surface mesh ID. + + Raises + ------ + Flow360FileError + If the surface mesh dependency resource cannot be initialized from the file. + """ + + try: + validated_files = SurfaceMeshFile(file_names=file) + except pd.ValidationError as err: + # pylint:disable = raise-missing-from + raise Flow360FileError(f"Surface mesh file error: {str(err)}") + + surface_mesh = self._import_dependency_resource_from_file( + files=validated_files, + name=name, + length_unit=length_unit, + tags=tags, + run_async=run_async, + ) + + # Return an ImportedSurface object instead of SurfaceMeshV2 + # The name should be the same as the input file name + # The surface_mesh_id should be the ID of the surface mesh returned from the API + imported_surface = ImportedSurface( + name=name, + file_name=file, + surface_mesh_id=surface_mesh.id, + ) + + return imported_surface + @classmethod def _get_user_requested_entity_info( cls, diff --git a/flow360/component/surface_mesh_v2.py b/flow360/component/surface_mesh_v2.py index c71c39ea3..93a813558 100644 --- a/flow360/component/surface_mesh_v2.py +++ b/flow360/component/surface_mesh_v2.py @@ -11,7 +11,11 @@ import pydantic as pd -from flow360.cloud.flow360_requests import LengthUnitType, NewSurfaceMeshRequestV2 +from flow360.cloud.flow360_requests import ( + LengthUnitType, + NewSurfaceMeshDependencyRequest, + NewSurfaceMeshRequestV2, +) from flow360.cloud.heartbeat import post_upload_heartbeat from flow360.cloud.rest_api import RestApi from flow360.component.interfaces import SurfaceMeshInterfaceV2 @@ -19,6 +23,7 @@ AssetMetaBaseModelV2, Flow360Resource, ResourceDraft, + SubmissionMode, ) from flow360.component.simulation.entity_info import SurfaceMeshEntityInfo from flow360.component.simulation.folder import Folder @@ -76,138 +81,304 @@ class SurfaceMeshMetaV2(AssetMetaBaseModelV2): file_name: Optional[str] = pd.Field(None, alias="fileName") status: SurfaceMeshStatusV2 = pd.Field() # Overshadowing to ensure correct is_final() method + dependency: Optional[bool] = pd.Field(False) class SurfaceMeshDraftV2(ResourceDraft): """ - Surface mesh Draft component + Unified Surface Mesh Draft component for uploading surface mesh files. + + This class handles both: + - Creating a new project with surface mesh as the root asset + - Adding surface mesh as a dependency to an existing project + + The submission mode is determined by how the draft is created (via factory methods + on the SurfaceMeshV2 class) and affects the behavior of the submit() method. + + All surface meshes are conceptually equivalent - they are components that can be used + to create the final mesh for simulation. The distinction between "root" and + "dependency" is only about where the surface mesh is uploaded (new project vs existing + project), not about any fundamental difference in the surface mesh itself. """ # pylint: disable=too-many-arguments def __init__( self, file_names: str, - project_name: str = None, - solver_version: str = None, length_unit: LengthUnitType = "m", tags: List[str] = None, + # Project root context (for backward compatibility with existing API) + project_name: str = None, + solver_version: str = None, folder: Optional[Folder] = None, ): + """ + Initialize a SurfaceMeshDraftV2 with common attributes. + + For creating a new project (root surface mesh): + Use SurfaceMeshV2.from_file() which sets project_name, solver_version, folder + + For adding to existing project (dependency surface mesh): + Use SurfaceMeshV2.from_file_for_project() which sets the dependency context + + Parameters + ---------- + file_names : str + Path to the surface mesh file + length_unit : LengthUnitType, optional + Unit of length (default is "m") + tags : List[str], optional + Tags to assign to the surface mesh (default is None) + project_name : str, optional + Name of the project (for project root mode) + solver_version : str, optional + Solver version (for project root mode) + folder : Optional[Folder], optional + Parent folder (for project root mode) + """ self._file_name = file_names - self.project_name = project_name self.tags = tags if tags is not None else [] self.length_unit = length_unit - self.solver_version = solver_version - self.folder = folder - self._validate() - ResourceDraft.__init__(self) - def _validate(self): + # Submission context - determines behavior of submit() + self._submission_mode: Optional[SubmissionMode] = None + self._submission_context: dict = {} + + # For backward compatibility: if project root params are provided, set context + if project_name is not None or solver_version is not None: + self.set_project_root_context( + project_name=project_name, + solver_version=solver_version, + folder=folder, + ) + self._validate_surface_mesh() + ResourceDraft.__init__(self) def _validate_surface_mesh(self): + """Validate surface mesh file and length unit.""" if self._file_name is not None: try: SurfaceMeshFile(file_names=self._file_name) except pd.ValidationError as e: raise Flow360FileError(str(e)) from e - if self.project_name is None: - self.project_name = os.path.splitext(os.path.basename(self._file_name))[0] - log.warning( - "`project_name` is not provided. " - f"Using the file name {self.project_name} as project name." - ) - if self.length_unit not in LengthUnitType.__args__: raise Flow360ValueError( f"specified length_unit : {self.length_unit} is invalid. " f"Valid options are: {list(LengthUnitType.__args__)}" ) - if self.solver_version is None: + def set_project_root_context( + self, + project_name: str, + solver_version: str, + folder: Optional[Folder] = None, + ) -> None: + """ + Configure this draft to create a new project with surface mesh as root. + + Called internally by SurfaceMeshV2.from_file(). + """ + self._submission_mode = SubmissionMode.PROJECT_ROOT + self._submission_context = { + "project_name": project_name, + "solver_version": solver_version, + "folder": folder, + } + + def set_dependency_context( + self, + name: str, + project_id: str, + ) -> None: + """ + Configure this draft to add surface mesh to an existing project. + + Called internally by SurfaceMeshV2.from_file_for_project(). + """ + self._submission_mode = SubmissionMode.PROJECT_DEPENDENCY + self._submission_context = { + "name": name, + "project_id": project_id, + } + + def _validate_project_root_context(self): + """Validate context for project root submission.""" + project_name = self._submission_context.get("project_name") + solver_version = self._submission_context.get("solver_version") + + if project_name is None: + project_name = os.path.splitext(os.path.basename(self._file_name))[0] + self._submission_context["project_name"] = project_name + log.warning( + "`project_name` is not provided. " + f"Using the file name {project_name} as project name." + ) + if solver_version is None: raise Flow360ValueError("solver_version field is required.") + def _create_project_root_resource(self, description: str = "") -> SurfaceMeshMetaV2: + """Create a new surface mesh resource that will be the root of a new project.""" + project_name = self._submission_context["project_name"] + solver_version = self._submission_context["solver_version"] + folder = self._submission_context.get("folder") + + req = NewSurfaceMeshRequestV2( + name=project_name, + solver_version=solver_version, + tags=self.tags, + file_name=self._file_name, + parent_folder_id=folder.id if folder else "ROOT.FLOW360", + length_unit=self.length_unit, + description=description, + ) + + resp = RestApi(SurfaceMeshInterfaceV2.endpoint).post(req.dict()) + return SurfaceMeshMetaV2(**resp) + + def _create_dependency_resource( + self, description: str = "", draft_id: str = "", icon: str = "" + ) -> SurfaceMeshMetaV2: + """Create a surface mesh resource as a dependency in an existing project.""" + name = self._submission_context["name"] + project_id = self._submission_context["project_id"] + + req = NewSurfaceMeshDependencyRequest( + name=name, + project_id=project_id, + draft_id=draft_id, + file_name=self._file_name, + length_unit=self.length_unit, + tags=self.tags, + description=description, + icon=icon, + ) + + resp = RestApi(SurfaceMeshInterfaceV2.endpoint).post(req.dict(), method="dependency") + return SurfaceMeshMetaV2(**resp) + + def _upload_files( + self, + info: SurfaceMeshMetaV2, + progress_callback=None, + ) -> SurfaceMeshV2: + """Upload surface mesh files to the cloud.""" + # pylint: disable=protected-access + surface_mesh = SurfaceMeshV2(info.id) + heartbeat_info = {"resourceId": info.id, "resourceType": "SurfaceMesh", "stop": False} + + # Keep posting the heartbeat to keep server patient about uploading. + heartbeat_thread = threading.Thread(target=post_upload_heartbeat, args=(heartbeat_info,)) + heartbeat_thread.start() + + try: + surface_mesh._webapi._upload_file( + remote_file_name=info.file_name, + file_name=self._file_name, + progress_callback=progress_callback, + ) + + mesh_parser = MeshNameParser(self._file_name) + remote_mesh_parser = MeshNameParser(info.file_name) + if mesh_parser.is_ugrid(): + # Upload the mapbc file too. + expected_local_mapbc_file = mesh_parser.get_associated_mapbc_filename() + if os.path.isfile(expected_local_mapbc_file): + surface_mesh._webapi._upload_file( + remote_file_name=remote_mesh_parser.get_associated_mapbc_filename(), + file_name=mesh_parser.get_associated_mapbc_filename(), + progress_callback=progress_callback, + ) + else: + log.warning( + f"The expected mapbc file {expected_local_mapbc_file} specifying " + "user-specified boundary names doesn't exist." + ) + finally: + heartbeat_info["stop"] = True + heartbeat_thread.join() + + # Kick off pipeline + surface_mesh._webapi._complete_upload() + + # Setting _id will disable "WARNING: You have not submitted..." warning message + self._id = info.id + + return surface_mesh + # pylint: disable=protected-access # pylint: disable=duplicate-code - def submit(self, description="", progress_callback=None, run_async=False) -> SurfaceMeshV2: + def submit( + self, + description: str = "", + progress_callback=None, + run_async: bool = False, + draft_id: str = "", + icon: str = "", + ) -> SurfaceMeshV2: """ - Submit surface mesh file to cloud and create a new project + Submit surface mesh to cloud. + + The behavior depends on how this draft was created: + - If created via SurfaceMeshV2.from_file(): Creates a new project with this surface mesh as root + - If created via SurfaceMeshV2.from_file_for_project(): Adds surface mesh to an existing project Parameters ---------- description : str, optional - description of the project, by default "" + Description of the surface mesh/project (default is "") progress_callback : callback, optional - Use for custom progress bar, by default None + Use for custom progress bar (default is None) run_async : bool, optional - Whether to submit surface mesh asynchronously (default is False). + Whether to return immediately after upload without waiting for processing + (default is False) + draft_id : str, optional + ID of the draft to add surface mesh to (only used for dependency mode, default is "") + icon : str, optional + Icon for the surface mesh (only used for dependency mode, default is "") Returns ------- SurfaceMeshV2 SurfaceMeshV2 object with id + + Raises + ------ + Flow360ValueError + If submission context is not set or user aborts """ + if self._submission_mode is None: + raise Flow360ValueError( + "Submission context not set. Use SurfaceMeshV2.from_file() or " + "SurfaceMeshV2.from_file_for_project() to create a properly configured draft." + ) + + self._validate_surface_mesh() - self._validate() + # Validate project root context if applicable + if self._submission_mode == SubmissionMode.PROJECT_ROOT: + self._validate_project_root_context() if not shared_account_confirm_proceed(): raise Flow360ValueError("User aborted resource submit.") - # The first geometry is assumed to be the main one. - req = NewSurfaceMeshRequestV2( - name=self.project_name, - solver_version=self.solver_version, - tags=self.tags, - file_name=self._file_name, - parent_folder_id=self.folder.id if self.folder else "ROOT.FLOW360", - length_unit=self.length_unit, - description=description, - ) - - ##:: Create new Geometry resource and project - resp = RestApi(SurfaceMeshInterfaceV2.endpoint).post(req.dict()) - info = SurfaceMeshMetaV2(**resp) + # Create the surface mesh resource based on submission mode + if self._submission_mode == SubmissionMode.PROJECT_ROOT: + info = self._create_project_root_resource(description) + log_message = "Surface mesh successfully submitted" + else: + info = self._create_dependency_resource(description, draft_id, icon) + log_message = "Supplementary surface mesh resources successfully submitted" - ##:: upload surface mesh file - surface_mesh = SurfaceMeshV2(info.id) - heartbeat_info = {"resourceId": info.id, "resourceType": "SurfaceMesh", "stop": False} - # Keep posting the heartbeat to keep server patient about uploading. - heartbeat_thread = threading.Thread(target=post_upload_heartbeat, args=(heartbeat_info,)) - heartbeat_thread.start() + # Upload files + surface_mesh = self._upload_files(info, progress_callback) - surface_mesh._webapi._upload_file( - remote_file_name=info.file_name, - file_name=self._file_name, - progress_callback=progress_callback, - ) + log.info(f"{log_message}: {surface_mesh.short_description()}") - mesh_parser = MeshNameParser(self._file_name) - remote_mesh_parser = MeshNameParser(info.file_name) - if mesh_parser.is_ugrid(): - # Upload the mapbc file too. - expected_local_mapbc_file = mesh_parser.get_associated_mapbc_filename() - if os.path.isfile(expected_local_mapbc_file): - surface_mesh._webapi._upload_file( - remote_file_name=remote_mesh_parser.get_associated_mapbc_filename(), - file_name=mesh_parser.get_associated_mapbc_filename(), - progress_callback=progress_callback, - ) - else: - log.warning( - f"The expected mapbc file {expected_local_mapbc_file} specifying " - "user-specified boundary names doesn't exist." - ) - - heartbeat_info["stop"] = True - heartbeat_thread.join() - ##:: kick off pipeline - surface_mesh._webapi._complete_upload() - log.info(f"Surface mesh successfully submitted: {surface_mesh.short_description()}") - # setting _id will disable "WARNING: You have not submitted..." warning message - self._id = info.id if run_async: return surface_mesh + log.info("Waiting for surface mesh to be processed.") surface_mesh._webapi.get_info() # uses from_cloud to ensure all metadata is ready before yielding the object @@ -311,6 +482,48 @@ def from_file( folder=folder, ) + @classmethod + # pylint: disable=too-many-arguments + def from_file_for_project( + cls, + name: str, + file_name: str, + project_id: str, + length_unit: LengthUnitType = "m", + tags: List[str] = None, + ) -> SurfaceMeshDraftV2: + """ + Create a surface mesh draft for adding to an existing project. + + This creates a surface mesh that will be added as a supplementary component + (dependency) to an existing project, rather than creating a new project. + + Parameters + ---------- + name : str + Name for the surface mesh component + file_name : str + Path to the surface mesh file (*.cgns, *.ugrid) + project_id : str + ID of the existing project to add this surface mesh to + length_unit : LengthUnitType, optional + Unit of length (default is "m") + tags : List[str], optional + Tags to assign to the surface mesh (default is None) + + Returns + ------- + SurfaceMeshDraftV2 + A draft configured for submission to an existing project + """ + draft = SurfaceMeshDraftV2( + file_names=file_name, + length_unit=length_unit, + tags=tags, + ) + draft.set_dependency_context(name=name, project_id=project_id) + return draft + # pylint: disable=useless-parent-delegation def get_dynamic_default_settings(self, simulation_dict: dict): """Get the default surface mesh settings from the simulation dict""" From 9aac754043dbc620d15c48fef36a2ce6a896a200 Mon Sep 17 00:00:00 2001 From: Angran Date: Wed, 17 Dec 2025 22:44:15 +0000 Subject: [PATCH 06/38] Add project interface to fetch imported dependency resources --- flow360/component/project.py | 63 ++++++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 6 deletions(-) diff --git a/flow360/component/project.py b/flow360/component/project.py index 8732b1b31..f474abecf 100644 --- a/flow360/component/project.py +++ b/flow360/component/project.py @@ -1292,16 +1292,67 @@ def import_surface_mesh_dependency_from_file( run_async=run_async, ) - # Return an ImportedSurface object instead of SurfaceMeshV2 - # The name should be the same as the input file name - # The surface_mesh_id should be the ID of the surface mesh returned from the API - imported_surface = ImportedSurface( + return ImportedSurface( name=name, - file_name=file, surface_mesh_id=surface_mesh.id, ) - return imported_surface + def _get_imported_depedency_resources_from_cloud( + self, resource_type: Literal["Geometry", "SurfaceMesh"] + ): + """ + Get all imported dependency resources of a given type in the project. + + Parameters + ---------- + resource_type : Literal["Geometry", "SurfaceMesh"] + The type of dependency resource to retrieve. + + """ + + resp = self._project_webapi.get(method="dependency") + if resource_type == "Geometry": + imported_resources = [ + Geometry.from_cloud(item["id"]) for item in resp["geometryDependencyResources"] + ] + elif resource_type == "SurfaceMesh": + imported_resources = [ + ImportedSurface( + name=item["name"], + surface_mesh_id=item["id"], + ) + for item in resp["surfaceMeshDependencyResources"] + ] + else: + raise Flow360ValueError(f"Unsupported imported resource type: {resource_type}") + + return imported_resources + + @property + def imported_geometry_dependencies(self) -> List[Geometry]: + """ + Get all imported geometry dependency resources in the project. + + Returns + ------- + List[Geometry] + A list of Geometry objects representing the imported geometry dependencies. + """ + + return self._get_imported_depedency_resources_from_cloud(resource_type="Geometry") + + @property + def imported_surface_mesh_dependencies(self) -> List[ImportedSurface]: + """ + Get all imported surface mesh dependency resources in the project. + + Returns + ------- + List[ImportedSurface] + A list of ImportedSurface objects representing the imported surface mesh dependencies. + """ + + return self._get_imported_depedency_resources_from_cloud(resource_type="SurfaceMesh") @classmethod def _get_user_requested_entity_info( From 2598a738309d3073c2c2ec3fac82fb0a5c2d1e35 Mon Sep 17 00:00:00 2001 From: Angran Date: Thu, 18 Dec 2025 16:20:40 +0000 Subject: [PATCH 07/38] Add validation to check if the dependency resource with the same name already exists. --- flow360/component/project.py | 48 +++++++++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/flow360/component/project.py b/flow360/component/project.py index f474abecf..c7e78fa87 100644 --- a/flow360/component/project.py +++ b/flow360/component/project.py @@ -93,6 +93,22 @@ class RootType(Enum): VOLUME_MESH = "VolumeMesh" +class ProjectResourceType(Enum): + """ + Enum for dependency resource types in the project. + + Attributes + ---------- + GEOMETRY : str + Represents a geometry dependency resource. + SURFACE_MESH : str + Represents a surface mesh dependency resource. + """ + + GEOMETRY = "Geometry" + SURFACE_MESH = "SurfaceMesh" + + def create_draft( *, new_run_from: Union[Geometry, SurfaceMeshV2, VolumeMeshV2], @@ -1158,6 +1174,22 @@ def _detect_input_file_type(file: Union[str, list[str]]): run_async=run_async, ) + def _check_conflicts_with_existing_dependency_resources( + self, name: str, resource_type: ProjectResourceType + ): + resp = self._project_webapi.post(method="dependency/namecheck", json={"name": name}) + if ( + resource_type == ProjectResourceType.GEOMETRY + and resp["conflictResourceId"].startswith("geo") + ) or ( + resource_type == ProjectResourceType.SURFACE_MESH + and resp["conflictResourceId"].startswith("sm") + ): + raise Flow360ValueError( + f"A {resource_type.value} with the name '{name}' already exists in the project. " + "Please use a different name." + ) + def _import_dependency_resource_from_file( self, *, @@ -1171,6 +1203,9 @@ def _import_dependency_resource_from_file( files._check_files_existence() if isinstance(files, GeometryFiles): + self._check_conflicts_with_existing_dependency_resources( + name=name, resource_type=ProjectResourceType.GEOMETRY + ) draft = Geometry.from_file_for_project( name=name, file_names=files.file_names, @@ -1179,6 +1214,9 @@ def _import_dependency_resource_from_file( tags=tags, ) elif isinstance(files, SurfaceMeshFile): + self._check_conflicts_with_existing_dependency_resources( + name=name, resource_type=ProjectResourceType.SURFACE_MESH + ) draft = SurfaceMeshV2.from_file_for_project( name=name, file_name=files.file_names, @@ -1297,25 +1335,23 @@ def import_surface_mesh_dependency_from_file( surface_mesh_id=surface_mesh.id, ) - def _get_imported_depedency_resources_from_cloud( - self, resource_type: Literal["Geometry", "SurfaceMesh"] - ): + def _get_imported_depedency_resources_from_cloud(self, resource_type: ProjectResourceType): """ Get all imported dependency resources of a given type in the project. Parameters ---------- - resource_type : Literal["Geometry", "SurfaceMesh"] + resource_type : ProjectResourceType The type of dependency resource to retrieve. """ resp = self._project_webapi.get(method="dependency") - if resource_type == "Geometry": + if resource_type == ProjectResourceType.GEOMETRY: imported_resources = [ Geometry.from_cloud(item["id"]) for item in resp["geometryDependencyResources"] ] - elif resource_type == "SurfaceMesh": + elif resource_type == ProjectResourceType.SURFACE_MESH: imported_resources = [ ImportedSurface( name=item["name"], From 67f8bcec034126260e4c55df96146eadeb71acc9 Mon Sep 17 00:00:00 2001 From: Angran Date: Mon, 22 Dec 2025 20:18:28 +0000 Subject: [PATCH 08/38] Fix Geometry and SurfaceMesh Draft interface to keep backward compatibility --- flow360/component/geometry.py | 114 ++++++++++----------------- flow360/component/surface_mesh_v2.py | 109 ++++++++++--------------- 2 files changed, 82 insertions(+), 141 deletions(-) diff --git a/flow360/component/geometry.py b/flow360/component/geometry.py index d39f38739..5097fd9cc 100644 --- a/flow360/component/geometry.py +++ b/flow360/component/geometry.py @@ -99,15 +99,14 @@ class GeometryDraft(ResourceDraft): project), not about any fundamental difference in the geometry itself. """ - # pylint: disable=too-many-arguments + # pylint: disable=too-many-arguments, too-many-instance-attributes def __init__( self, file_names: Union[List[str], str], - length_unit: LengthUnitType = "m", - tags: List[str] = None, - # Project root context (for backward compatibility with existing API) project_name: str = None, solver_version: str = None, + length_unit: LengthUnitType = "m", + tags: List[str] = None, folder: Optional[Folder] = None, ): """ @@ -135,22 +134,18 @@ def __init__( Parent folder (for project root mode) """ self._file_names = file_names + self.project_name = project_name self.tags = tags if tags is not None else [] self.length_unit = length_unit + self.solver_version = solver_version + self.folder = folder - # Submission context - determines behavior of submit() - self._submission_mode: Optional[SubmissionMode] = None - self._submission_context: dict = {} - - # For backward compatibility: if project root params are provided, set context - if project_name is not None or solver_version is not None: - self.set_project_root_context( - project_name=project_name, - solver_version=solver_version, - folder=folder, - ) + self.dependency_name = None + self.dependency_project_id = None + self._submission_mode: SubmissionMode = SubmissionMode.PROJECT_ROOT self._validate_geometry() + self._set_default_project_name() ResourceDraft.__init__(self) def _validate_geometry(self): @@ -173,6 +168,30 @@ def _validate_geometry(self): f"Valid options are: {list(LengthUnitType.__args__)}" ) + def _set_default_project_name(self): + """Set default project name if not provided for project creation.""" + if self.project_name is None: + self.project_name = os.path.splitext(os.path.basename(self.file_names[0]))[0] + log.warning( + "`project_name` is not provided. " + f"Using the first geometry file name {self.project_name} as project name." + ) + + def _validate_submission_context(self): + """Validate context for submission based on mode.""" + if self._submission_mode is None: + raise Flow360ValueError( + "Submission context not set. Use Geometry.from_file() or " + "Geometry.from_file_for_project() to create a properly configured draft." + ) + if self._submission_mode == SubmissionMode.PROJECT_ROOT and self.solver_version is None: + raise Flow360ValueError("solver_version field is required.") + if self._submission_mode == SubmissionMode.PROJECT_DEPENDENCY: + if self.dependency_name is None or self.dependency_project_id is None: + raise Flow360ValueError( + "Dependency name and project ID must be set for geometry dependency submission." + ) + @property def file_names(self) -> List[str]: """Geometry file paths as a list.""" @@ -180,24 +199,6 @@ def file_names(self) -> List[str]: return [self._file_names] return self._file_names - def set_project_root_context( - self, - project_name: str, - solver_version: str, - folder: Optional[Folder] = None, - ) -> None: - """ - Configure this draft to create a new project with geometry as root. - - Called internally by Geometry.from_file(). - """ - self._submission_mode = SubmissionMode.PROJECT_ROOT - self._submission_context = { - "project_name": project_name, - "solver_version": solver_version, - "folder": folder, - } - def set_dependency_context( self, name: str, @@ -209,25 +210,8 @@ def set_dependency_context( Called internally by Geometry.from_file_for_project(). """ self._submission_mode = SubmissionMode.PROJECT_DEPENDENCY - self._submission_context = { - "name": name, - "project_id": project_id, - } - - def _validate_project_root_context(self): - """Validate context for project root submission.""" - project_name = self._submission_context.get("project_name") - solver_version = self._submission_context.get("solver_version") - - if project_name is None: - project_name = os.path.splitext(os.path.basename(self.file_names[0]))[0] - self._submission_context["project_name"] = project_name - log.warning( - "`project_name` is not provided. " - f"Using the first geometry file name {project_name} as project name." - ) - if solver_version is None: - raise Flow360ValueError("solver_version field is required.") + self.dependency_name = name + self.dependency_project_id = project_id def _preprocess_mapbc_files(self) -> List[str]: """Find and return associated mapbc files for UGRID geometry files.""" @@ -245,13 +229,9 @@ def _create_project_root_resource( self, mapbc_files: List[str], description: str = "" ) -> GeometryMeta: """Create a new geometry resource that will be the root of a new project.""" - project_name = self._submission_context["project_name"] - solver_version = self._submission_context["solver_version"] - folder = self._submission_context.get("folder") - req = NewGeometryRequest( - name=project_name, - solver_version=solver_version, + name=self.project_name, + solver_version=self.solver_version, tags=self.tags, files=[ GeometryFileMeta( @@ -260,7 +240,7 @@ def _create_project_root_resource( ) for file_path in self.file_names + mapbc_files ], - parent_folder_id=folder.id if folder else "ROOT.FLOW360", + parent_folder_id=self.folder.id if self.folder else "ROOT.FLOW360", length_unit=self.length_unit, description=description, ) @@ -272,12 +252,10 @@ def _create_dependency_resource( self, mapbc_files: List[str], description: str = "", draft_id: str = "", icon: str = "" ) -> GeometryMeta: """Create a geometry resource as a dependency in an existing project.""" - name = self._submission_context["name"] - project_id = self._submission_context["project_id"] req = NewGeometryDependencyRequest( - name=name, - project_id=project_id, + name=self.dependency_name, + project_id=self.dependency_project_id, draft_id=draft_id, files=[ GeometryFileMeta( @@ -369,17 +347,9 @@ def submit( Flow360ValueError If submission context is not set or user aborts """ - if self._submission_mode is None: - raise Flow360ValueError( - "Submission context not set. Use Geometry.from_file() or " - "Geometry.from_file_for_project() to create a properly configured draft." - ) self._validate_geometry() - - # Validate project root context if applicable - if self._submission_mode == SubmissionMode.PROJECT_ROOT: - self._validate_project_root_context() + self._validate_submission_context() if not shared_account_confirm_proceed(): raise Flow360ValueError("User aborted resource submit.") diff --git a/flow360/component/surface_mesh_v2.py b/flow360/component/surface_mesh_v2.py index 93a813558..8a5011797 100644 --- a/flow360/component/surface_mesh_v2.py +++ b/flow360/component/surface_mesh_v2.py @@ -101,15 +101,14 @@ class SurfaceMeshDraftV2(ResourceDraft): project), not about any fundamental difference in the surface mesh itself. """ - # pylint: disable=too-many-arguments + # pylint: disable=too-many-arguments, too-many-instance-attributes def __init__( self, file_names: str, - length_unit: LengthUnitType = "m", - tags: List[str] = None, - # Project root context (for backward compatibility with existing API) project_name: str = None, solver_version: str = None, + length_unit: LengthUnitType = "m", + tags: List[str] = None, folder: Optional[Folder] = None, ): """ @@ -137,22 +136,18 @@ def __init__( Parent folder (for project root mode) """ self._file_name = file_names + self.project_name = project_name self.tags = tags if tags is not None else [] self.length_unit = length_unit + self.solver_version = solver_version + self.folder = folder - # Submission context - determines behavior of submit() - self._submission_mode: Optional[SubmissionMode] = None - self._submission_context: dict = {} - - # For backward compatibility: if project root params are provided, set context - if project_name is not None or solver_version is not None: - self.set_project_root_context( - project_name=project_name, - solver_version=solver_version, - folder=folder, - ) + self.dependency_name = None + self.dependency_project_id = None + self._submission_mode: SubmissionMode = SubmissionMode.PROJECT_ROOT self._validate_surface_mesh() + self._set_default_project_name() ResourceDraft.__init__(self) def _validate_surface_mesh(self): @@ -169,23 +164,29 @@ def _validate_surface_mesh(self): f"Valid options are: {list(LengthUnitType.__args__)}" ) - def set_project_root_context( - self, - project_name: str, - solver_version: str, - folder: Optional[Folder] = None, - ) -> None: - """ - Configure this draft to create a new project with surface mesh as root. + def _set_default_project_name(self): + """Set default project name if not provided for project creation.""" + if self.project_name is None: + self.project_name = os.path.splitext(os.path.basename(self._file_name))[0] + log.warning( + "`project_name` is not provided. " + f"Using the file name {self.project_name} as project name." + ) - Called internally by SurfaceMeshV2.from_file(). - """ - self._submission_mode = SubmissionMode.PROJECT_ROOT - self._submission_context = { - "project_name": project_name, - "solver_version": solver_version, - "folder": folder, - } + def _validate_submission_context(self): + """Validate context for submission based on mode.""" + if self._submission_mode is None: + raise Flow360ValueError( + "Submission context not set. Use SurfaceMeshV2.from_file() or " + "SurfaceMeshV2.from_file_for_project() to create a properly configured draft." + ) + if self._submission_mode == SubmissionMode.PROJECT_ROOT and self.solver_version is None: + raise Flow360ValueError("solver_version field is required.") + if self._submission_mode == SubmissionMode.PROJECT_DEPENDENCY: + if self.dependency_name is None or self.dependency_project_id is None: + raise Flow360ValueError( + "Dependency name and project ID must be set for surface mesh dependency submission." + ) def set_dependency_context( self, @@ -198,38 +199,18 @@ def set_dependency_context( Called internally by SurfaceMeshV2.from_file_for_project(). """ self._submission_mode = SubmissionMode.PROJECT_DEPENDENCY - self._submission_context = { - "name": name, - "project_id": project_id, - } - - def _validate_project_root_context(self): - """Validate context for project root submission.""" - project_name = self._submission_context.get("project_name") - solver_version = self._submission_context.get("solver_version") - - if project_name is None: - project_name = os.path.splitext(os.path.basename(self._file_name))[0] - self._submission_context["project_name"] = project_name - log.warning( - "`project_name` is not provided. " - f"Using the file name {project_name} as project name." - ) - if solver_version is None: - raise Flow360ValueError("solver_version field is required.") + self.dependency_name = name + self.dependency_project_id = project_id def _create_project_root_resource(self, description: str = "") -> SurfaceMeshMetaV2: """Create a new surface mesh resource that will be the root of a new project.""" - project_name = self._submission_context["project_name"] - solver_version = self._submission_context["solver_version"] - folder = self._submission_context.get("folder") req = NewSurfaceMeshRequestV2( - name=project_name, - solver_version=solver_version, + name=self.project_name, + solver_version=self.solver_version, tags=self.tags, file_name=self._file_name, - parent_folder_id=folder.id if folder else "ROOT.FLOW360", + parent_folder_id=self.folder.id if self.folder else "ROOT.FLOW360", length_unit=self.length_unit, description=description, ) @@ -241,12 +222,10 @@ def _create_dependency_resource( self, description: str = "", draft_id: str = "", icon: str = "" ) -> SurfaceMeshMetaV2: """Create a surface mesh resource as a dependency in an existing project.""" - name = self._submission_context["name"] - project_id = self._submission_context["project_id"] req = NewSurfaceMeshDependencyRequest( - name=name, - project_id=project_id, + name=self.dependency_name, + project_id=self.dependency_project_id, draft_id=draft_id, file_name=self._file_name, length_unit=self.length_unit, @@ -348,17 +327,9 @@ def submit( Flow360ValueError If submission context is not set or user aborts """ - if self._submission_mode is None: - raise Flow360ValueError( - "Submission context not set. Use SurfaceMeshV2.from_file() or " - "SurfaceMeshV2.from_file_for_project() to create a properly configured draft." - ) self._validate_surface_mesh() - - # Validate project root context if applicable - if self._submission_mode == SubmissionMode.PROJECT_ROOT: - self._validate_project_root_context() + self._validate_submission_context() if not shared_account_confirm_proceed(): raise Flow360ValueError("User aborted resource submit.") From 758f34446587268d70399bdb302f383aaebe0610 Mon Sep 17 00:00:00 2001 From: Angran Date: Mon, 22 Dec 2025 20:19:46 +0000 Subject: [PATCH 09/38] Fix dependency name check --- flow360/component/project.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flow360/component/project.py b/flow360/component/project.py index c7e78fa87..3423d415d 100644 --- a/flow360/component/project.py +++ b/flow360/component/project.py @@ -1178,6 +1178,8 @@ def _check_conflicts_with_existing_dependency_resources( self, name: str, resource_type: ProjectResourceType ): resp = self._project_webapi.post(method="dependency/namecheck", json={"name": name}) + if resp.get("status") == "success": + return if ( resource_type == ProjectResourceType.GEOMETRY and resp["conflictResourceId"].startswith("geo") From b3cdda3fe0c42529e3297b18826f598ee043f32a Mon Sep 17 00:00:00 2001 From: Angran Date: Mon, 22 Dec 2025 20:22:16 +0000 Subject: [PATCH 10/38] Update the workflow to run case with dependency resources --- flow360/component/project.py | 2 +- flow360/component/project_utils.py | 122 +++++----------------- flow360/component/simulation/web/draft.py | 41 ++++---- 3 files changed, 46 insertions(+), 119 deletions(-) diff --git a/flow360/component/project.py b/flow360/component/project.py index 3423d415d..518d5ef65 100644 --- a/flow360/component/project.py +++ b/flow360/component/project.py @@ -1825,7 +1825,7 @@ def _run( params.pre_submit_summary() - upload_imported_surfaces_to_draft(params, draft, fork_from) + draft.enable_dependency_resources(active_draft) draft.update_simulation_params(params) if draft_only: diff --git a/flow360/component/project_utils.py b/flow360/component/project_utils.py index 914544d35..978c9db89 100644 --- a/flow360/component/project_utils.py +++ b/flow360/component/project_utils.py @@ -3,7 +3,6 @@ """ import datetime -import os from typing import List, Literal, Optional, get_args import pydantic as pd @@ -36,7 +35,6 @@ from flow360.component.simulation.unit_system import LengthType from flow360.component.simulation.user_code.core.types import save_user_variables from flow360.component.simulation.utils import model_attribute_unlock -from flow360.component.surface_mesh_v2 import SurfaceMeshV2 from flow360.component.utils import parse_datetime from flow360.exceptions import Flow360ConfigurationError, Flow360ValueError from flow360.log import log @@ -275,6 +273,30 @@ def _set_up_params_non_persistent_entity_info(entity_info, params: SimulationPar return entity_info +def _set_up_params_imported_surfaces(params: SimulationParams): + """ + Setting up imported_surfaces in params. + Add the ones used to the outputs. + """ + + if not params.outputs: + return params + + imported_surfaces = {} + + for output in params.outputs: + if not isinstance(output, (SurfaceOutput, SurfaceIntegralOutput)): + continue + for surface in output.entities.stored_entities: + if isinstance(surface, ImportedSurface) and surface.name not in imported_surfaces: + imported_surfaces[surface.name] = surface + + with model_attribute_unlock(params.private_attribute_asset_cache, "imported_surfaces"): + params.private_attribute_asset_cache.imported_surfaces = list(imported_surfaces.values()) + + return params + + def _merge_draft_entities_from_params( entity_info: EntityInfoModel, params: SimulationParams, @@ -526,6 +548,9 @@ def set_up_params_for_uploading( # pylint: disable=too-many-arguments # Convert all reference of UserVariables to VariableToken params = save_user_variables(params) + # Set up imported surfaces in params + params = _set_up_params_imported_surfaces(params) + # Strip selector-matched entities from stored_entities before upload so that hand-picked # entities remain distinguishable on the UI side. strip_selector_matches_inplace(params) @@ -549,96 +574,3 @@ def validate_params_with_context(params, root_item_type, up_to): ) return params, errors - - -def _get_imported_surface_file_names(params, basename_only=False): - if params is None or params.outputs is None: - return [] - imported_surface_files = [] - for output in params.outputs: - if isinstance(output, (SurfaceOutput, SurfaceIntegralOutput)): - for surface in output.entities.stored_entities: - if isinstance(surface, ImportedSurface): - if basename_only: - imported_surface_files.append(os.path.basename(surface.file_name)) - else: - imported_surface_files.append(surface.file_name) - return imported_surface_files - - -def upload_imported_surfaces_to_draft(params, draft, parent_case): - """ - Upload imported surfaces to draft, excluding duplicates from parent case. - - Note: - - If parent_case is None, all surfaces from params will be uploaded. - - Only surfaces not present in the parent case are uploaded. - """ - - parent_existing_imported_file_basenames = [] - if parent_case is not None: - parent_existing_imported_file_basenames = _get_imported_surface_file_names( - parent_case.params, basename_only=True - ) - current_draft_surface_file_paths_to_import = _get_imported_surface_file_names(params) - deduplicated_surface_file_paths_to_import = [] - for file_path_to_import in current_draft_surface_file_paths_to_import: - file_basename = os.path.basename(file_path_to_import) - if file_basename not in parent_existing_imported_file_basenames: - deduplicated_surface_file_paths_to_import.append( - {"path": file_path_to_import, "basename": file_basename} - ) - - if not deduplicated_surface_file_paths_to_import: - _normalize_imported_surface_entities(params) - return - - response = draft.post( - json={ - "filenames": [entry["basename"] for entry in deduplicated_surface_file_paths_to_import] - }, - method="imported-surfaces", - ) - - basename_to_surface_mesh_ids: dict[str, list[str]] = {} - for entry, resp in zip(deduplicated_surface_file_paths_to_import, response): - surface_mesh_id = resp.get("surfaceMeshId") - _upload_surface_mesh_resource( - surface_mesh_id=surface_mesh_id, - local_file_path=entry["path"], - ) - basename_to_surface_mesh_ids.setdefault(entry["basename"], []).append(surface_mesh_id) - - _normalize_imported_surface_entities(params, basename_to_surface_mesh_ids) - - -def _upload_surface_mesh_resource(surface_mesh_id: str, local_file_path: str): - if not surface_mesh_id: - raise Flow360ConfigurationError("Surface mesh metadata missing for imported surface upload.") - surface_mesh = SurfaceMeshV2(surface_mesh_id) - remote_file_name = surface_mesh.info.file_name - surface_mesh._webapi._upload_file(remote_file_name=remote_file_name, file_name=local_file_path) - surface_mesh._webapi._complete_upload() - - -def _normalize_imported_surface_entities( - params: SimulationParams, basename_to_surface_mesh_ids: dict[str, list[str]] | None = None -): - if params is None or params.outputs is None: - return - - basename_to_iter = {} - if basename_to_surface_mesh_ids: - basename_to_iter = { - basename: iter(mesh_ids) for basename, mesh_ids in basename_to_surface_mesh_ids.items() - } - - for output in params.outputs: - if isinstance(output, (SurfaceOutput, SurfaceIntegralOutput)): - for surface in output.entities.stored_entities: - if isinstance(surface, ImportedSurface): - file_basename = os.path.basename(surface.file_name) - surface.file_name = file_basename - iterator = basename_to_iter.get(file_basename) - if iterator is not None: - surface.surface_mesh_id = next(iterator, surface.surface_mesh_id) diff --git a/flow360/component/simulation/web/draft.py b/flow360/component/simulation/web/draft.py index 4878c8293..41b7dcba4 100644 --- a/flow360/component/simulation/web/draft.py +++ b/flow360/component/simulation/web/draft.py @@ -4,9 +4,8 @@ import ast import json -import os from functools import cached_property -from typing import List, Literal, Union +from typing import Literal, Union from pydantic import BaseModel, ConfigDict, Field @@ -22,12 +21,7 @@ from flow360.component.simulation.framework.entity_selector import ( collect_and_tokenize_selectors_in_place, ) -from flow360.component.utils import ( - check_existence_of_one_file, - check_read_access_of_one_file, - formatting_validation_errors, - validate_type, -) +from flow360.component.utils import formatting_validation_errors, validate_type from flow360.environment import Env from flow360.exceptions import Flow360RuntimeError, Flow360WebError from flow360.log import log @@ -149,22 +143,23 @@ def update_simulation_params(self, params): method="simulation/file", ) - def upload_imported_surfaces(self, file_paths): - """upload imported surfaces to draft""" - - if len(file_paths) == 0: - return - file_names = [] - for file_path in file_paths: - file_names.append(os.path.basename(file_path)) - resp: List = self.post( - json={"filenames": file_names}, - method="imported-surfaces", + def enable_dependency_resources(self, active_draft): + """Enable dependency resources for the draft""" + + geometry_dependencies = [] + surface_mesh_dependencies = [] + for geometry in active_draft.imported_geometry_components.values(): + geometry_dependencies.append(geometry.id) + for surface in active_draft.imported_surface_components.values(): + surface_mesh_dependencies.append(surface.surface_mesh_id) + + self.put( + json={ + "geometryDependencies": geometry_dependencies, + "surfaceMeshDependencies": surface_mesh_dependencies, + }, + method="dependency-resource", ) - for index, local_file_path in enumerate(file_paths): - check_existence_of_one_file(local_file_path) - check_read_access_of_one_file(local_file_path) - self._upload_file(resp[index]["filename"], local_file_path) def get_simulation_dict(self) -> dict: """retrieve the SimulationParams of the draft""" From 50cea3c0ac270a0f121d1f54e99f2f498a4c92d2 Mon Sep 17 00:00:00 2001 From: Angran Date: Mon, 22 Dec 2025 20:23:02 +0000 Subject: [PATCH 11/38] Fix formatting --- flow360/component/simulation/entity_info.py | 1 - flow360/component/simulation/primitives.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/flow360/component/simulation/entity_info.py b/flow360/component/simulation/entity_info.py index 3c8b21faf..e4113e6ae 100644 --- a/flow360/component/simulation/entity_info.py +++ b/flow360/component/simulation/entity_info.py @@ -155,7 +155,6 @@ class GeometryEntityInfo(EntityInfoModel): description="The default value based on uploaded geometry for geometry_accuracy.", ) - def group_in_registry( self, entity_type_name: Literal["face", "edge", "body", "snappy_body"], diff --git a/flow360/component/simulation/primitives.py b/flow360/component/simulation/primitives.py index 6bb173b34..8b8348189 100644 --- a/flow360/component/simulation/primitives.py +++ b/flow360/component/simulation/primitives.py @@ -634,6 +634,7 @@ class ImportedSurface(EntityBase): file_name: Optional[str] = None surface_mesh_id: Optional[str] = None + class GhostSurface(_SurfaceEntityBase): """ Represents a boundary surface that may or may not be generated therefore may or may not exist. From fea24a7752676455c02b7a57b709d2c288e17fc8 Mon Sep 17 00:00:00 2001 From: Angran Date: Mon, 22 Dec 2025 20:24:05 +0000 Subject: [PATCH 12/38] Interface change for uploading and getting dependency resources from project --- flow360/component/project.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/flow360/component/project.py b/flow360/component/project.py index 518d5ef65..dff4b95ca 100644 --- a/flow360/component/project.py +++ b/flow360/component/project.py @@ -1232,7 +1232,7 @@ def _import_dependency_resource_from_file( dependency_resource = draft.submit(run_async=run_async) return dependency_resource - def import_geometry_dependency_from_file( + def import_geometry_from_file( self, file: Union[str, list[str]], /, @@ -1282,7 +1282,7 @@ def import_geometry_dependency_from_file( run_async=run_async, ) - def import_surface_mesh_dependency_from_file( + def import_surface_mesh_from_file( self, file: str, /, @@ -1337,7 +1337,7 @@ def import_surface_mesh_dependency_from_file( surface_mesh_id=surface_mesh.id, ) - def _get_imported_depedency_resources_from_cloud(self, resource_type: ProjectResourceType): + def _get_dependency_resources_from_cloud(self, resource_type: ProjectResourceType): """ Get all imported dependency resources of a given type in the project. @@ -1367,30 +1367,32 @@ def _get_imported_depedency_resources_from_cloud(self, resource_type: ProjectRes return imported_resources @property - def imported_geometry_dependencies(self) -> List[Geometry]: + def imported_geometry_components(self) -> List[Geometry]: """ - Get all imported geometry dependency resources in the project. + Get all imported geometry components in the project. Returns ------- List[Geometry] - A list of Geometry objects representing the imported geometry dependencies. + A list of Geometry objects representing the imported geometry components. """ - return self._get_imported_depedency_resources_from_cloud(resource_type="Geometry") + return self._get_dependency_resources_from_cloud(resource_type=ProjectResourceType.GEOMETRY) @property - def imported_surface_mesh_dependencies(self) -> List[ImportedSurface]: + def imported_surface_components(self) -> List[ImportedSurface]: """ - Get all imported surface mesh dependency resources in the project. + Get all imported surface components in the project. Returns ------- List[ImportedSurface] - A list of ImportedSurface objects representing the imported surface mesh dependencies. + A list of ImportedSurface objects representing the imported surface components. """ - return self._get_imported_depedency_resources_from_cloud(resource_type="SurfaceMesh") + return self._get_dependency_resources_from_cloud( + resource_type=ProjectResourceType.SURFACE_MESH + ) @classmethod def _get_user_requested_entity_info( From 0261a45a80707b32b0c2990ccaec2e9741e53237 Mon Sep 17 00:00:00 2001 From: Angran Date: Mon, 22 Dec 2025 22:17:20 +0000 Subject: [PATCH 13/38] Update the tag validation logic to also check current tag settings after merging multiple geometry entitiy info --- flow360/component/project_utils.py | 32 +++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/flow360/component/project_utils.py b/flow360/component/project_utils.py index 978c9db89..b8118d4b0 100644 --- a/flow360/component/project_utils.py +++ b/flow360/component/project_utils.py @@ -47,18 +47,36 @@ def apply_geometry_grouping_overrides( ) -> dict[str, Optional[str]]: """Apply explicit face/edge grouping overrides onto geometry entity info.""" - def _validate_tag(tag: str, available: list[str], kind: str) -> str: - if available and tag not in available: # pylint:disable=unsupported-membership-test + def _validate_tag(new_tag: str, current_tag: str, available: list[str], kind: str) -> str: + + if not available: + raise Flow360ValueError( + f"The updated geometry does not have any {kind} groupings. Please check geometry components." + ) + + override = False if new_tag is not None else False + tag = new_tag if new_tag is not None else current_tag + + if not override and (tag is None or tag not in available): + raise Flow360ValueError( + f"The current {kind} grouping '{tag}' is not valid in the updated geometry. " + f"Please specify a {kind}_grouping when creating draft. " + f"Available tags: {available}." + ) + if override and tag not in available: # pylint:disable=unsupported-membership-test raise Flow360ValueError( f"Invalid {kind} grouping tag '{tag}'. Available tags: {available}." ) return tag - if face_grouping is not None: - face_tag = _validate_tag(face_grouping, entity_info.face_attribute_names, "face") - entity_info._group_entity_by_tag("face", face_tag) # pylint:disable=protected-access - if edge_grouping is not None and entity_info.edge_attribute_names: - edge_tag = _validate_tag(edge_grouping, entity_info.edge_attribute_names, "edge") + face_tag = _validate_tag( + face_grouping, entity_info.face_group_tag, entity_info.face_attribute_names, "face" + ) + entity_info._group_entity_by_tag("face", face_tag) # pylint:disable=protected-access + if entity_info.edge_attribute_names: + edge_tag = _validate_tag( + edge_grouping, entity_info.edge_group_tag, entity_info.edge_attribute_names, "edge" + ) entity_info._group_entity_by_tag("edge", edge_tag) # pylint:disable=protected-access return { From 855329083bf62398c37be01faf1f61f8979587cc Mon Sep 17 00:00:00 2001 From: Angran Date: Mon, 22 Dec 2025 22:18:14 +0000 Subject: [PATCH 14/38] Update create_draft function to support include/exclude geometry components and specify imported surface components --- flow360/component/project.py | 82 +++++++- .../simulation/draft_context/context.py | 29 ++- flow360/component/simulation/entity_info.py | 194 ++++++++++++++++++ flow360/component/simulation/web/draft.py | 4 +- 4 files changed, 303 insertions(+), 6 deletions(-) diff --git a/flow360/component/project.py b/flow360/component/project.py index dff4b95ca..de1c367c1 100644 --- a/flow360/component/project.py +++ b/flow360/component/project.py @@ -7,7 +7,7 @@ import json from enum import Enum -from typing import Iterable, List, Literal, Optional, Union +from typing import Dict, Iterable, List, Literal, Optional, Union import pydantic as pd import typing_extensions @@ -33,7 +33,6 @@ get_project_records, set_up_params_for_uploading, show_projects_with_keyword_filter, - upload_imported_surfaces_to_draft, validate_params_with_context, ) from flow360.component.resource_base import Flow360Resource @@ -41,7 +40,10 @@ DraftContext, get_active_draft, ) -from flow360.component.simulation.entity_info import GeometryEntityInfo +from flow360.component.simulation.entity_info import ( + GeometryEntityInfo, + update_geometry_entity_info, +) from flow360.component.simulation.folder import Folder from flow360.component.simulation.primitives import ImportedSurface from flow360.component.simulation.simulation_params import SimulationParams @@ -109,11 +111,15 @@ class ProjectResourceType(Enum): SURFACE_MESH = "SurfaceMesh" +# pylint: disable=too-many-arguments def create_draft( *, new_run_from: Union[Geometry, SurfaceMeshV2, VolumeMeshV2], face_grouping: Optional[str] = None, edge_grouping: Optional[str] = None, + include_geometries: Optional[List[Geometry]] = None, + exclude_geometries: Optional[List[Geometry]] = None, + imported_surface_components: Optional[List[ImportedSurface]] = None, ) -> DraftContext: """Factory helper used by end users (`with fl.create_draft() as draft`). @@ -169,6 +175,61 @@ def _inform_grouping_selections(entity_info) -> None: "Grouping override ignored: only geometry assets support face/edge/body regrouping." ) + def _resolve_imported_geometry_components( + entity_info, + current_geometry_dependencies: Optional[List] = None, + include_geometries: Optional[List[Geometry]] = None, + exclude_geometries: Optional[List[Geometry]] = None, + ) -> Dict[str, Geometry]: + if not isinstance(entity_info, GeometryEntityInfo): + if include_geometries or exclude_geometries: + log.warning( + "Editting geometry components ignored: " + "only project with a non-geometry root asset supports this feature." + ) + return {} + current_imported_geometry_components = ( + { + geometry_dependency["id"]: Geometry.from_cloud(geometry_dependency["id"]) + for geometry_dependency in current_geometry_dependencies + } + if current_geometry_dependencies + else {} + ) + + if include_geometries: + for geometry in include_geometries: + if geometry.id not in current_imported_geometry_components: + current_imported_geometry_components[geometry.id] = geometry + + if exclude_geometries: + for geometry in exclude_geometries: + excluded_geometry = current_imported_geometry_components.pop(geometry.id, None) + if excluded_geometry is None: + log.warning( + f"Geometry {geometry.name} not found among current dependencies. Ignoring its exclusion." + ) + + return current_imported_geometry_components + + def _update_geometry_entity_info( + entity_info, new_run_from, imported_geometry_components: Dict[str, Geometry] + ): + """Update the geometry entity info based on the root and imported geometries.""" + if not isinstance(entity_info, GeometryEntityInfo): + return entity_info + project = Project.from_cloud(new_run_from.info.project_id) + root_geometry = project.geometry + all_geometry_components = {**imported_geometry_components, root_geometry.id: root_geometry} + entity_info_components = [ + geometry.entity_info for geometry in all_geometry_components.values() + ] + updated_entity_info = update_geometry_entity_info( + current_entity_info=entity_info, + entity_info_components=entity_info_components, + ) + return updated_entity_info + # endregion ------------------------------------------------------------------------------------ if not isinstance(new_run_from, AssetBase): @@ -177,11 +238,26 @@ def _inform_grouping_selections(entity_info) -> None: # Deep copy entity_info for draft isolation entity_info_copy = _deep_copy_entity_info(new_run_from.entity_info) + imported_geometry_components = _resolve_imported_geometry_components( + entity_info=entity_info_copy, + current_geometry_dependencies=new_run_from.info.geometry_dependencies, + include_geometries=include_geometries, + exclude_geometries=exclude_geometries, + ) + + entity_info_copy = _update_geometry_entity_info( + entity_info=entity_info_copy, + new_run_from=new_run_from, + imported_geometry_components=imported_geometry_components, + ) + # Apply grouping overrides to the copy (not the original) _inform_grouping_selections(entity_info_copy) return DraftContext( entity_info=entity_info_copy, + imported_surface_components=imported_surface_components, + imported_geometry_components=imported_geometry_components, ) diff --git a/flow360/component/simulation/draft_context/context.py b/flow360/component/simulation/draft_context/context.py index 7d4d675f0..9505c61f4 100644 --- a/flow360/component/simulation/draft_context/context.py +++ b/flow360/component/simulation/draft_context/context.py @@ -4,7 +4,7 @@ from contextlib import AbstractContextManager from contextvars import ContextVar, Token -from typing import Optional, get_args +from typing import List, Optional, get_args from flow360.component.simulation.entity_info import DraftEntityTypes, EntityInfoModel from flow360.component.simulation.framework.entity_base import EntityBase @@ -16,6 +16,7 @@ Edge, GenericVolume, GeometryBodyGroup, + ImportedSurface, Surface, ) from flow360.exceptions import Flow360RuntimeError @@ -49,6 +50,8 @@ class DraftContext( # pylint: disable=too-many-instance-attributes __slots__ = ( "_entity_info", "_entity_registry", + "_imported_surface_components", + "_imported_geometry_components", "_token", ) @@ -56,6 +59,8 @@ def __init__( self, *, entity_info: EntityInfoModel, + imported_geometry_components: List, + imported_surface_components: List[ImportedSurface], ) -> None: """ Data members: @@ -82,6 +87,14 @@ def __init__( # This builds the registry by referencing entities from our copied entity_info. self._entity_registry: EntityRegistry = EntityRegistry.from_entity_info(entity_info) + self._imported_surface_components = imported_surface_components + known_frozen_hashes = set() + for imported_surface in self._imported_surface_components: + known_frozen_hashes = self._entity_registry.fast_register( + imported_surface, known_frozen_hashes + ) + self._imported_geometry_components = imported_geometry_components + def __enter__(self) -> DraftContext: if get_active_draft() is not None: raise Flow360RuntimeError("Nested draft contexts are not allowed.") @@ -162,4 +175,18 @@ def cylinders(self) -> EntityRegistryView: return self._entity_registry.view(Cylinder) + @property + def imported_geometry_components(self) -> List: + """ + Return the list of imported surface components in the draft. + """ + return self._imported_geometry_components + + @property + def imported_surface_components(self) -> EntityRegistryView: + """ + Return the list of imported surface components in the draft. + """ + return self._entity_registry.view(ImportedSurface) + # endregion ------------------------------------------------------------------------------------ diff --git a/flow360/component/simulation/entity_info.py b/flow360/component/simulation/entity_info.py index e4113e6ae..8230e96a0 100644 --- a/flow360/component/simulation/entity_info.py +++ b/flow360/component/simulation/entity_info.py @@ -628,3 +628,197 @@ def parse_entity_info_model(data) -> EntityInfoUnion: # TODO: Add a fast mode by popping entities that are not needed due to wrong grouping tags before deserialization. """ return pd.TypeAdapter(EntityInfoUnion).validate_python(data) + + +def update_geometry_entity_info( + current_entity_info: GeometryEntityInfo, + entity_info_components: List[GeometryEntityInfo], +) -> GeometryEntityInfo: + """ + Update a GeometryEntityInfo by including/merging data from a list of other GeometryEntityInfo objects. + + Args: + current_entity_info: Used as reference to preserve user settings such as group tags, + mesh_exterior, attribute name order. + entity_info_components: List of GeometryEntityInfo objects that contain all data for the new entity info + + Returns: + A new GeometryEntityInfo with merged data from entity_info_components, preserving user settings from current + + The merge logic: + 1. IDs: Union of body_ids, face_ids, edge_ids from entity_info_components + 2. Attribute names: Intersection of attribute_names from entity_info_components + 3. Group tags: Use tags from current_entity_info + 4. Bounding box: Merge global bounding boxes from entity_info_components + 5. Grouped entities: Merge from entity_info_components, + preserving mesh_exterior from current_entity_info for grouped_bodies + 6. Draft and Ghost entities: Preserve from current_entity_info. + """ + # pylint: disable=too-many-locals, too-many-statements + if not entity_info_components: + raise ValueError("entity_info_components cannot be empty") + + # 1. Compute union of IDs from entity_info_components + all_body_ids = set() + all_face_ids = set() + all_edge_ids = set() + + for entity_info in entity_info_components: + all_body_ids.update(entity_info.body_ids) + all_face_ids.update(entity_info.face_ids) + all_edge_ids.update(entity_info.edge_ids) + + # 2. Compute intersection of attribute names from entity_info_components + body_attr_sets = [set(ei.body_attribute_names) for ei in entity_info_components] + face_attr_sets = [set(ei.face_attribute_names) for ei in entity_info_components] + edge_attr_sets = [set(ei.edge_attribute_names) for ei in entity_info_components] + + body_attr_intersection = set.intersection(*body_attr_sets) if body_attr_sets else set() + face_attr_intersection = set.intersection(*face_attr_sets) if face_attr_sets else set() + edge_attr_intersection = set.intersection(*edge_attr_sets) if edge_attr_sets else set() + + # Preserve order from current_entity_info, but include all attributes from intersection + def ordered_intersection(reference_list: List[str], intersection_set: set) -> List[str]: + """Return all attributes from intersection_set, preserving order from reference_list where possible.""" + # First, add attributes that exist in reference_list (in order) + result = [attr for attr in reference_list if attr in intersection_set] + # Then, add remaining attributes from intersection_set that weren't in reference_list (sorted) + remaining = sorted(intersection_set - set(result)) + return result + remaining + + result_body_attribute_names = ordered_intersection( + current_entity_info.body_attribute_names, body_attr_intersection + ) + result_face_attribute_names = ordered_intersection( + current_entity_info.face_attribute_names, face_attr_intersection + ) + result_edge_attribute_names = ordered_intersection( + current_entity_info.edge_attribute_names, edge_attr_intersection + ) + + # 3. Update group tags: preserve from current if exists in intersection, otherwise use first + def select_tag( + current_tag: Optional[str], result_attrs: List[str], entity_type: str + ) -> Optional[str]: + if not result_attrs: + raise ValueError(f"No attribute names available to select {entity_type} group tag.") + log.info(f"Preserving {entity_type} group tag: {current_tag}") + return current_tag + + result_body_group_tag = select_tag( + current_entity_info.body_group_tag, result_body_attribute_names, "body" + ) + result_face_group_tag = select_tag( + current_entity_info.face_group_tag, result_face_attribute_names, "face" + ) + result_edge_group_tag = select_tag( + current_entity_info.edge_group_tag, result_edge_attribute_names, "edge" + ) + + # 4. Merge global bounding boxes from entity_info_components + result_bounding_box = None + for entity_info in entity_info_components: + if entity_info.global_bounding_box is not None: + if result_bounding_box is None: + result_bounding_box = entity_info.global_bounding_box + else: + result_bounding_box = result_bounding_box.expand(entity_info.global_bounding_box) + + # Build mapping of body group ID to mesh_exterior from current_entity_info + current_body_mesh_exterior_map = {} + for body_group_idx, body_group_name in enumerate(current_entity_info.body_attribute_names): + current_body_mesh_exterior_map[body_group_name] = {} + for body in current_entity_info.grouped_bodies[body_group_idx]: + body_id = body.private_attribute_id + current_body_mesh_exterior_map[body_group_name][body_id] = body.mesh_exterior + + # 5. Merge grouped entities from entity_info_components + def merge_grouped_entities( + entity_type: Literal["body", "face", "edge"], + result_attr_names: List[str], + ): + """Helper to merge grouped entities (bodies, faces, or edges) from entity_info_components""" + + # Determine which attributes to access based on entity type + def get_attrs(entity_info): + if entity_type == "body": + return entity_info.body_attribute_names + if entity_type == "face": + return entity_info.face_attribute_names + return entity_info.edge_attribute_names + + def get_groups(entity_info): + if entity_type == "body": + return entity_info.grouped_bodies + if entity_type == "face": + return entity_info.grouped_faces + return entity_info.grouped_edges + + result_grouped = [] + + # For each attribute name in the result intersection + for attr_name in result_attr_names: + # Dictionary to accumulate entities by their unique ID + entity_map = {} + + # Process all include entity infos + for entity_info in entity_info_components: + entity_attrs = get_attrs(entity_info) + if attr_name not in entity_attrs: + continue + idx = entity_attrs.index(attr_name) + entity_groups = get_groups(entity_info) + for entity in entity_groups[idx]: + # Use private_attribute_id as the unique identifier + entity_id = entity.private_attribute_id + if entity_type == "body": + print(entity_id) + if entity_id not in entity_map: + # For bodies, check if we need to preserve mesh_exterior + if ( + entity_type == "body" + and attr_name in current_body_mesh_exterior_map + and entity_id in current_body_mesh_exterior_map[attr_name] + ): + # Create a copy with preserved mesh_exterior + entity_data = entity.model_dump() + entity_data["mesh_exterior"] = current_body_mesh_exterior_map[ + attr_name + ][entity_id] + entity_map[entity_id] = GeometryBodyGroup.model_validate(entity_data) + else: + entity_map[entity_id] = entity + + # Convert map to list, maintaining a stable order (sorted by entity ID) + result_grouped.append(sorted(entity_map.values(), key=lambda e: e.private_attribute_id)) + + return result_grouped + + result_grouped_bodies = merge_grouped_entities("body", result_body_attribute_names) + result_grouped_faces = merge_grouped_entities("face", result_face_attribute_names) + result_grouped_edges = merge_grouped_entities("edge", result_edge_attribute_names) + + # Use default_geometry_accuracy from first include_entity_info + result_default_geometry_accuracy = entity_info_components[0].default_geometry_accuracy + + # Create the result GeometryEntityInfo + result = GeometryEntityInfo( + body_ids=sorted(all_body_ids), + body_attribute_names=result_body_attribute_names, + grouped_bodies=result_grouped_bodies, + face_ids=sorted(all_face_ids), + face_attribute_names=result_face_attribute_names, + grouped_faces=result_grouped_faces, + edge_ids=sorted(all_edge_ids), + edge_attribute_names=result_edge_attribute_names, + grouped_edges=result_grouped_edges, + body_group_tag=result_body_group_tag, + face_group_tag=result_face_group_tag, + edge_group_tag=result_edge_group_tag, + global_bounding_box=result_bounding_box, + default_geometry_accuracy=result_default_geometry_accuracy, + draft_entities=current_entity_info.draft_entities, + ghost_entities=current_entity_info.ghost_entities, + ) + + return result diff --git a/flow360/component/simulation/web/draft.py b/flow360/component/simulation/web/draft.py index 41b7dcba4..18935f381 100644 --- a/flow360/component/simulation/web/draft.py +++ b/flow360/component/simulation/web/draft.py @@ -148,9 +148,9 @@ def enable_dependency_resources(self, active_draft): geometry_dependencies = [] surface_mesh_dependencies = [] - for geometry in active_draft.imported_geometry_components.values(): + for geometry in active_draft.imported_geometry_components: geometry_dependencies.append(geometry.id) - for surface in active_draft.imported_surface_components.values(): + for surface in active_draft.imported_surface_components: surface_mesh_dependencies.append(surface.surface_mesh_id) self.put( From cbd6507b4b820f7818abafe372118c7ea9ce5db7 Mon Sep 17 00:00:00 2001 From: Angran Date: Tue, 23 Dec 2025 14:52:44 +0000 Subject: [PATCH 15/38] Fix bug after schema change --- flow360/component/project.py | 2 +- .../component/simulation/draft_context/context.py | 8 ++++++-- flow360/component/simulation/entity_info.py | 2 -- flow360/component/simulation/web/draft.py | 13 +++++++------ 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/flow360/component/project.py b/flow360/component/project.py index de1c367c1..d5b8a2150 100644 --- a/flow360/component/project.py +++ b/flow360/component/project.py @@ -257,7 +257,7 @@ def _update_geometry_entity_info( return DraftContext( entity_info=entity_info_copy, imported_surface_components=imported_surface_components, - imported_geometry_components=imported_geometry_components, + imported_geometry_components=list(imported_geometry_components.values()), ) diff --git a/flow360/component/simulation/draft_context/context.py b/flow360/component/simulation/draft_context/context.py index 9505c61f4..097eff36e 100644 --- a/flow360/component/simulation/draft_context/context.py +++ b/flow360/component/simulation/draft_context/context.py @@ -87,13 +87,17 @@ def __init__( # This builds the registry by referencing entities from our copied entity_info. self._entity_registry: EntityRegistry = EntityRegistry.from_entity_info(entity_info) - self._imported_surface_components = imported_surface_components + self._imported_surface_components: List = ( + imported_surface_components if imported_surface_components else [] + ) known_frozen_hashes = set() for imported_surface in self._imported_surface_components: known_frozen_hashes = self._entity_registry.fast_register( imported_surface, known_frozen_hashes ) - self._imported_geometry_components = imported_geometry_components + self._imported_geometry_components: List = ( + imported_geometry_components if imported_geometry_components else [] + ) def __enter__(self) -> DraftContext: if get_active_draft() is not None: diff --git a/flow360/component/simulation/entity_info.py b/flow360/component/simulation/entity_info.py index 8230e96a0..fdeac1e08 100644 --- a/flow360/component/simulation/entity_info.py +++ b/flow360/component/simulation/entity_info.py @@ -771,8 +771,6 @@ def get_groups(entity_info): for entity in entity_groups[idx]: # Use private_attribute_id as the unique identifier entity_id = entity.private_attribute_id - if entity_type == "body": - print(entity_id) if entity_id not in entity_map: # For bodies, check if we need to preserve mesh_exterior if ( diff --git a/flow360/component/simulation/web/draft.py b/flow360/component/simulation/web/draft.py index 18935f381..4172a282c 100644 --- a/flow360/component/simulation/web/draft.py +++ b/flow360/component/simulation/web/draft.py @@ -146,12 +146,13 @@ def update_simulation_params(self, params): def enable_dependency_resources(self, active_draft): """Enable dependency resources for the draft""" - geometry_dependencies = [] - surface_mesh_dependencies = [] - for geometry in active_draft.imported_geometry_components: - geometry_dependencies.append(geometry.id) - for surface in active_draft.imported_surface_components: - surface_mesh_dependencies.append(surface.surface_mesh_id) + geometry_dependencies = [ + geometry.id for geometry in active_draft.imported_geometry_components + ] + + surface_mesh_dependencies = [ + surface.surface_mesh_id for surface in active_draft.imported_surface_components + ] self.put( json={ From 69241a32178c830cba564bc2219e05fc35567558 Mon Sep 17 00:00:00 2001 From: Angran Date: Tue, 23 Dec 2025 16:57:21 +0000 Subject: [PATCH 16/38] Fix the bug introduced after resolving conflicts --- flow360/component/project.py | 83 ++++++------------- flow360/component/project_utils.py | 75 +++++++++-------- .../simulation/draft_context/context.py | 1 + 3 files changed, 65 insertions(+), 94 deletions(-) diff --git a/flow360/component/project.py b/flow360/component/project.py index a8b705f34..319a28fed 100644 --- a/flow360/component/project.py +++ b/flow360/component/project.py @@ -38,7 +38,10 @@ validate_params_with_context, ) from flow360.component.resource_base import Flow360Resource -from flow360.component.simulation.draft_context.context import DraftContext +from flow360.component.simulation.draft_context.context import ( + DraftContext, + get_active_draft, +) from flow360.component.simulation.draft_context.coordinate_system_manager import ( CoordinateSystemStatus, ) @@ -134,44 +137,9 @@ def create_draft( # region -----------------------------Private implementations Below----------------------------- - def _inform_grouping_selections(entity_info) -> None: - """Inform the user about the grouping selections made on the entity provider cloud asset.""" - - if isinstance(entity_info, GeometryEntityInfo): - applied_grouping = { - "face": entity_info.face_group_tag, - "edge": entity_info.edge_group_tag, - "body": entity_info.body_group_tag, - } - if face_grouping is not None or edge_grouping is not None: - applied_grouping = apply_geometry_grouping_overrides( - entity_info, face_grouping, edge_grouping - ) - # If tags were None, fall back to defaults for logging purposes. - # pylint:disable = protected-access - face_tag = applied_grouping.get("face") or entity_info._get_default_grouping_tag("face") - edge_tag = applied_grouping.get("edge") - if edge_tag is None and entity_info.edge_attribute_names: - edge_tag = entity_info._get_default_grouping_tag("edge") - - log.info( - "Creating draft with geometry grouping:\n" - " faces: %s\n" - " edges: %s\n" - "To change grouping, call:\n" - " fl.create_draft(face_grouping='%s', edge_grouping='%s', ...)", - face_tag, - edge_tag, - face_tag, - edge_tag, - ) - elif face_grouping is not None or edge_grouping is not None: - log.info( - "Grouping override ignored: only geometry assets support face/edge/body regrouping." - ) - - def _resolve_imported_geometry_components( + def _resolve_geometry_components( entity_info, + new_run_from, current_geometry_dependencies: Optional[List] = None, include_geometries: Optional[List[Geometry]] = None, exclude_geometries: Optional[List[Geometry]] = None, @@ -179,11 +147,11 @@ def _resolve_imported_geometry_components( if not isinstance(entity_info, GeometryEntityInfo): if include_geometries or exclude_geometries: log.warning( - "Editting geometry components ignored: " + "Editing geometry components ignored: " "only project with a non-geometry root asset supports this feature." ) return {} - current_imported_geometry_components = ( + current_geometry_components = ( { geometry_dependency["id"]: Geometry.from_cloud(geometry_dependency["id"]) for geometry_dependency in current_geometry_dependencies @@ -191,37 +159,34 @@ def _resolve_imported_geometry_components( if current_geometry_dependencies else {} ) + project = Project.from_cloud(new_run_from.info.project_id) + root_geometry = project.geometry + current_geometry_components.update({root_geometry.id: root_geometry}) if include_geometries: for geometry in include_geometries: - if geometry.id not in current_imported_geometry_components: - current_imported_geometry_components[geometry.id] = geometry + if geometry.id not in current_geometry_components: + current_geometry_components[geometry.id] = geometry if exclude_geometries: for geometry in exclude_geometries: - excluded_geometry = current_imported_geometry_components.pop(geometry.id, None) + excluded_geometry = current_geometry_components.pop(geometry.id, None) if excluded_geometry is None: log.warning( f"Geometry {geometry.name} not found among current dependencies. Ignoring its exclusion." ) - return current_imported_geometry_components + return current_geometry_components - def _update_geometry_entity_info( - entity_info, new_run_from, imported_geometry_components: Dict[str, Geometry] - ): + def _update_geometry_entity_info(entity_info, geometry_components: Dict[str, Geometry]): """Update the geometry entity info based on the root and imported geometries.""" if not isinstance(entity_info, GeometryEntityInfo): return entity_info - project = Project.from_cloud(new_run_from.info.project_id) - root_geometry = project.geometry - all_geometry_components = {**imported_geometry_components, root_geometry.id: root_geometry} - entity_info_components = [ - geometry.entity_info for geometry in all_geometry_components.values() - ] updated_entity_info = update_geometry_entity_info( current_entity_info=entity_info, - entity_info_components=entity_info_components, + entity_info_components=[ + geometry.entity_info for geometry in geometry_components.values() + ], ) return updated_entity_info @@ -233,17 +198,16 @@ def _update_geometry_entity_info( # Deep copy entity_info for draft isolation entity_info_copy = deep_copy_entity_info(new_run_from.entity_info) - imported_geometry_components = _resolve_imported_geometry_components( + geometry_components = _resolve_geometry_components( entity_info=entity_info_copy, + new_run_from=new_run_from, current_geometry_dependencies=new_run_from.info.geometry_dependencies, include_geometries=include_geometries, exclude_geometries=exclude_geometries, ) - entity_info_copy = _update_geometry_entity_info( entity_info=entity_info_copy, - new_run_from=new_run_from, - imported_geometry_components=imported_geometry_components, + geometry_components=geometry_components, ) apply_and_inform_grouping_selections( @@ -268,7 +232,7 @@ def _update_geometry_entity_info( mirror_status=mirror_status, coordinate_system_status=coordinate_system_status, imported_surface_components=imported_surface_components, - imported_geometry_components=list(imported_geometry_components.values()), + imported_geometry_components=list(geometry_components.values()), ) @@ -1902,6 +1866,7 @@ def _run( params.pre_submit_summary() + active_draft = get_active_draft() draft.enable_dependency_resources(active_draft) draft.update_simulation_params(params) diff --git a/flow360/component/project_utils.py b/flow360/component/project_utils.py index 2d2d935e9..8a4d028a1 100644 --- a/flow360/component/project_utils.py +++ b/flow360/component/project_utils.py @@ -3,7 +3,6 @@ """ import datetime -import os from typing import List, Literal, Optional, Type, TypeVar, get_args import pydantic as pd @@ -57,37 +56,54 @@ def _apply_geometry_grouping_overrides( ) -> dict[str, Optional[str]]: """Apply explicit face/edge grouping overrides onto geometry entity info.""" - def _validate_tag(new_tag: str, current_tag: str, available: list[str], kind: str) -> str: + # >>> 1. Select groupings to use, either from overrides or entity_info defaults. - if not available: - raise Flow360ValueError( - f"The updated geometry does not have any {kind} groupings. Please check geometry components." + def _select_tag(new_tag, default_tag, kind): + if new_tag is not None: + tag = new_tag + else: + log.info( + f"No {kind} grouping specified when creating draft; " + f"using {kind} grouping: {default_tag} from `new_run_from`." ) + tag = default_tag + return tag - override = False if new_tag is not None else False - tag = new_tag if new_tag is not None else current_tag + face_tag = _select_tag(face_grouping, entity_info.face_group_tag, "face") + edge_tag = _select_tag(edge_grouping, entity_info.edge_group_tag, "edge") + body_group_tag = ( + "groupByFile" + if "groupByFile" in entity_info.body_attribute_names + else entity_info.body_group_tag + ) - if not override and (tag is None or tag not in available): + # >>> 2. Validate groupings + def _validate_tag(tag, available: list[str], kind: str) -> str: + if not available: raise Flow360ValueError( - f"The current {kind} grouping '{tag}' is not valid in the updated geometry. " - f"Please specify a {kind}_grouping when creating draft. " - f"Available tags: {available}." + f"The geometry does not have any {kind} groupings. Please check geometry components." ) - if override and tag not in available: # pylint:disable=unsupported-membership-test + if tag not in available: raise Flow360ValueError( - f"Invalid {kind} grouping tag '{tag}'. Available tags: {available}." + f"The current {kind} grouping '{tag}' is not valid in the geometry. " + f"Please specify a {kind}_grouping when creating draft. " + f"Available tags: {available}." ) return tag - face_tag = _validate_tag( - face_grouping, entity_info.face_group_tag, entity_info.face_attribute_names, "face" + face_tag = _validate_tag(face_tag, entity_info.face_attribute_names, "face") + # face_tag must be specified either from override or entity_info default + assert face_tag is not None, print( + "[Internal] Default face grouping should be set, face tag to be applied: ", face_tag ) - entity_info._group_entity_by_tag("face", face_tag) # pylint:disable=protected-access + # edge_tag can be None if the geometry asset created with surface mesh if entity_info.edge_attribute_names: - edge_tag = _validate_tag( - edge_grouping, entity_info.edge_group_tag, entity_info.edge_attribute_names, "edge" - ) - entity_info._group_entity_by_tag("edge", edge_tag) # pylint:disable=protected-access + edge_tag = _validate_tag(edge_tag, entity_info.edge_attribute_names, "edge") + + # >>> 3. Apply groupings + entity_info._group_entity_by_tag("face", face_tag) # pylint:disable=protected-access + entity_info._group_entity_by_tag("edge", edge_tag) # pylint:disable=protected-access + entity_info._group_entity_by_tag("body", body_group_tag) # pylint:disable=protected-access return { "face": entity_info.face_group_tag, @@ -174,25 +190,14 @@ def apply_and_inform_grouping_selections( return applied_grouping = _apply_geometry_grouping_overrides(entity_info, face_grouping, edge_grouping) - # - # pylint:disable = protected-access - ############## DEBUG only ############## - face_tag = applied_grouping.get("face") - edge_tag = applied_grouping.get("edge") - - # edge_tag can be None if the geometry asset created with surface mesh - - assert face_tag is not None, print( - "[Internal] Default should have already been set, applied_grouping: ", applied_grouping - ) - ########################################## # 1. Print out the grouping used for user's convenience. log.info( - "Creating draft with geometry grouping:\n faces: %s\n edges: %s\n", - face_tag, - edge_tag, + "Creating draft with geometry grouping:\n faces: %s\n edges: %s\n bodies: %s\n", + applied_grouping.get("face"), + applied_grouping.get("edge"), + applied_grouping.get("body"), ) missing_groupings = [] diff --git a/flow360/component/simulation/draft_context/context.py b/flow360/component/simulation/draft_context/context.py index 2326064fd..cacf0375e 100644 --- a/flow360/component/simulation/draft_context/context.py +++ b/flow360/component/simulation/draft_context/context.py @@ -71,6 +71,7 @@ class DraftContext( # pylint: disable=too-many-instance-attributes "_token", ) + # pylint: disable=too-many-arguments def __init__( self, *, From e2d8e052337df0964091fa99b44021c320fb51d9 Mon Sep 17 00:00:00 2001 From: Angran Date: Tue, 23 Dec 2025 17:50:07 +0000 Subject: [PATCH 17/38] Add if check to enable geometry edit feature if applicable --- flow360/component/project.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/flow360/component/project.py b/flow360/component/project.py index 319a28fed..e53093062 100644 --- a/flow360/component/project.py +++ b/flow360/component/project.py @@ -198,17 +198,19 @@ def _update_geometry_entity_info(entity_info, geometry_components: Dict[str, Geo # Deep copy entity_info for draft isolation entity_info_copy = deep_copy_entity_info(new_run_from.entity_info) - geometry_components = _resolve_geometry_components( - entity_info=entity_info_copy, - new_run_from=new_run_from, - current_geometry_dependencies=new_run_from.info.geometry_dependencies, - include_geometries=include_geometries, - exclude_geometries=exclude_geometries, - ) - entity_info_copy = _update_geometry_entity_info( - entity_info=entity_info_copy, - geometry_components=geometry_components, - ) + # Edit geometry dependencies if applicable + if new_run_from.info.geometry_dependencies or include_geometries or exclude_geometries: + geometry_components = _resolve_geometry_components( + entity_info=entity_info_copy, + new_run_from=new_run_from, + current_geometry_dependencies=new_run_from.info.geometry_dependencies, + include_geometries=include_geometries, + exclude_geometries=exclude_geometries, + ) + entity_info_copy = _update_geometry_entity_info( + entity_info=entity_info_copy, + geometry_components=geometry_components, + ) apply_and_inform_grouping_selections( entity_info=entity_info_copy, From d2413edd87b299d3e828f01221192a2a65492d95 Mon Sep 17 00:00:00 2001 From: Angran Date: Tue, 23 Dec 2025 20:33:38 +0000 Subject: [PATCH 18/38] Add merge geometry entity info API --- flow360/component/simulation/entity_info.py | 23 +++++++--- flow360/component/simulation/services.py | 49 +++++++++++++++++++++ 2 files changed, 65 insertions(+), 7 deletions(-) diff --git a/flow360/component/simulation/entity_info.py b/flow360/component/simulation/entity_info.py index eba77462c..4a74c9e4b 100644 --- a/flow360/component/simulation/entity_info.py +++ b/flow360/component/simulation/entity_info.py @@ -730,11 +730,13 @@ def update_geometry_entity_info( all_body_ids = set() all_face_ids = set() all_edge_ids = set() + all_bodies_face_edge_ids = {} for entity_info in entity_info_components: all_body_ids.update(entity_info.body_ids) all_face_ids.update(entity_info.face_ids) all_edge_ids.update(entity_info.edge_ids) + all_bodies_face_edge_ids.update(entity_info.bodies_face_edge_ids or {}) # 2. Compute intersection of attribute names from entity_info_components body_attr_sets = [set(ei.body_attribute_names) for ei in entity_info_components] @@ -793,12 +795,15 @@ def select_tag( result_bounding_box = result_bounding_box.expand(entity_info.global_bounding_box) # Build mapping of body group ID to mesh_exterior from current_entity_info - current_body_mesh_exterior_map = {} + current_body_user_settings_map = {} for body_group_idx, body_group_name in enumerate(current_entity_info.body_attribute_names): - current_body_mesh_exterior_map[body_group_name] = {} + current_body_user_settings_map[body_group_name] = {} for body in current_entity_info.grouped_bodies[body_group_idx]: body_id = body.private_attribute_id - current_body_mesh_exterior_map[body_group_name][body_id] = body.mesh_exterior + current_body_user_settings_map[body_group_name][body_id] = { + "mesh_exterior": body.mesh_exterior, + "name": body.name, + } # 5. Merge grouped entities from entity_info_components def merge_grouped_entities( @@ -843,14 +848,17 @@ def get_groups(entity_info): # For bodies, check if we need to preserve mesh_exterior if ( entity_type == "body" - and attr_name in current_body_mesh_exterior_map - and entity_id in current_body_mesh_exterior_map[attr_name] + and attr_name in current_body_user_settings_map + and entity_id in current_body_user_settings_map[attr_name] ): # Create a copy with preserved mesh_exterior entity_data = entity.model_dump() - entity_data["mesh_exterior"] = current_body_mesh_exterior_map[ + entity_data["mesh_exterior"] = current_body_user_settings_map[ attr_name - ][entity_id] + ][entity_id]["mesh_exterior"] + entity_data["name"] = current_body_user_settings_map[attr_name][ + entity_id + ]["name"] entity_map[entity_id] = GeometryBodyGroup.model_validate(entity_data) else: entity_map[entity_id] = entity @@ -869,6 +877,7 @@ def get_groups(entity_info): # Create the result GeometryEntityInfo result = GeometryEntityInfo( + bodies_face_edge_ids=all_bodies_face_edge_ids, body_ids=sorted(all_body_ids), body_attribute_names=result_body_attribute_names, grouped_bodies=result_grouped_bodies, diff --git a/flow360/component/simulation/services.py b/flow360/component/simulation/services.py index 629662064..2f130bb0d 100644 --- a/flow360/component/simulation/services.py +++ b/flow360/component/simulation/services.py @@ -18,6 +18,7 @@ from flow360.component.simulation.framework.multi_constructor_model_base import ( parse_model_dict, ) +from flow360.component.simulation.entity_info import update_geometry_entity_info, GeometryEntityInfo from flow360.component.simulation.meshing_param.params import MeshingParams from flow360.component.simulation.meshing_param.volume_params import AutomatedFarfield from flow360.component.simulation.models.bet.bet_translator_interface import ( @@ -204,6 +205,7 @@ def get_default_params( params = _store_project_length_unit(project_length_unit, params) return params.model_dump( + mode="json", exclude_none=True, exclude={ "operating_condition": {"velocity_magnitude": True}, @@ -235,6 +237,7 @@ def get_default_params( params = _store_project_length_unit(project_length_unit, params) return params.model_dump( + mode="json", exclude_none=True, exclude={ "operating_condition": {"velocity_magnitude": True}, @@ -1177,3 +1180,49 @@ def _parse_root_item_type_from_simulation_json(*, param_as_dict: dict): except KeyError: # pylint:disable = raise-missing-from raise ValueError("[INTERNAL] Failed to get the root item from the simulation.json!!!") + + +def merge_geometry_entity_info( + draft_param_as_dict: dict, geometry_dependencies_param_as_dict: list[dict] +): + """ + Merge the geometry entity info from geometry dependencies into the draft simulation param dict. + + Parameters + ---------- + draft_param_as_dict : dict + The draft simulation parameters dictionary. + geometry_dependencies_param_as_dict : list of dict + The list of geometry dependencies simulation parameters dictionaries. + + Returns + ------- + dict + The updated draft simulation parameters dictionary with merged geometry entity info. + """ + draft_param_entity_info_dict = draft_param_as_dict.get("private_attribute_asset_cache", {}).get( + "project_entity_info", {} + ) + if draft_param_entity_info_dict.get("type_name") != "GeometryEntityInfo": + return draft_param_as_dict + + current_entity_info = GeometryEntityInfo.model_validate(draft_param_entity_info_dict) + + entity_info_components = [] + for geometry_param_as_dict in geometry_dependencies_param_as_dict: + dependency_entity_info_dict = geometry_param_as_dict.get( + "private_attribute_asset_cache", {} + ).get("project_entity_info", {}) + if dependency_entity_info_dict.get("type_name") != "GeometryEntityInfo": + continue + entity_info_components.append( + GeometryEntityInfo.model_validate(dependency_entity_info_dict) + ) + + merged_entity_info = update_geometry_entity_info( + current_entity_info=current_entity_info, + entity_info_components=entity_info_components, + ) + merged_entity_info_dict = merged_entity_info.model_dump(mode="json", exclude_none=True) + + return merged_entity_info_dict From ebddc6dfe5c94da10af6d4e1a8cc76e08ed1ec72 Mon Sep 17 00:00:00 2001 From: Feilin Jia Date: Wed, 24 Dec 2025 02:57:31 +0000 Subject: [PATCH 19/38] add subcomponents --- flow360/component/simulation/primitives.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/flow360/component/simulation/primitives.py b/flow360/component/simulation/primitives.py index 5445d3f98..2e162043b 100644 --- a/flow360/component/simulation/primitives.py +++ b/flow360/component/simulation/primitives.py @@ -775,6 +775,10 @@ class ImportedSurface(EntityBase): private_attribute_entity_type_name: Literal["ImportedSurface"] = pd.Field( "ImportedSurface", frozen=True ) + + private_attribute_sub_components: List[str] = pd.Field( + description="A list of sub components" + ) file_name: Optional[str] = None surface_mesh_id: Optional[str] = None From 3558b8449de888317bda6e6eb792ea866846dbec Mon Sep 17 00:00:00 2001 From: Angran Date: Wed, 24 Dec 2025 12:27:50 +0000 Subject: [PATCH 20/38] Fix empty bodies_face_edge_ids returned by entity merge --- flow360/component/simulation/entity_info.py | 2 +- flow360/component/simulation/services.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/flow360/component/simulation/entity_info.py b/flow360/component/simulation/entity_info.py index 4a74c9e4b..3bd05f1c9 100644 --- a/flow360/component/simulation/entity_info.py +++ b/flow360/component/simulation/entity_info.py @@ -877,7 +877,7 @@ def get_groups(entity_info): # Create the result GeometryEntityInfo result = GeometryEntityInfo( - bodies_face_edge_ids=all_bodies_face_edge_ids, + bodies_face_edge_ids=all_bodies_face_edge_ids if all_bodies_face_edge_ids else None, body_ids=sorted(all_body_ids), body_attribute_names=result_body_attribute_names, grouped_bodies=result_grouped_bodies, diff --git a/flow360/component/simulation/services.py b/flow360/component/simulation/services.py index 2f130bb0d..5b787b4cf 100644 --- a/flow360/component/simulation/services.py +++ b/flow360/component/simulation/services.py @@ -11,6 +11,10 @@ # Required for correct global scope initialization from flow360.component.simulation.blueprint.core.dependency_graph import DependencyGraph +from flow360.component.simulation.entity_info import ( + GeometryEntityInfo, + update_geometry_entity_info, +) from flow360.component.simulation.exposed_units import supported_units_by_front_end from flow360.component.simulation.framework.entity_materializer import ( materialize_entities_and_selectors_in_place, @@ -18,7 +22,6 @@ from flow360.component.simulation.framework.multi_constructor_model_base import ( parse_model_dict, ) -from flow360.component.simulation.entity_info import update_geometry_entity_info, GeometryEntityInfo from flow360.component.simulation.meshing_param.params import MeshingParams from flow360.component.simulation.meshing_param.volume_params import AutomatedFarfield from flow360.component.simulation.models.bet.bet_translator_interface import ( From 603719fcfd89ddf6f14bcfd1fcca3817305dee88 Mon Sep 17 00:00:00 2001 From: Angran Date: Wed, 24 Dec 2025 15:52:44 +0000 Subject: [PATCH 21/38] Fix error captured by unit test and formatter --- flow360/component/project.py | 1 + flow360/component/simulation/draft_context/context.py | 4 ++-- flow360/component/simulation/primitives.py | 4 +--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/flow360/component/project.py b/flow360/component/project.py index e53093062..7d64bb24b 100644 --- a/flow360/component/project.py +++ b/flow360/component/project.py @@ -199,6 +199,7 @@ def _update_geometry_entity_info(entity_info, geometry_components: Dict[str, Geo entity_info_copy = deep_copy_entity_info(new_run_from.entity_info) # Edit geometry dependencies if applicable + geometry_components = {} if new_run_from.info.geometry_dependencies or include_geometries or exclude_geometries: geometry_components = _resolve_geometry_components( entity_info=entity_info_copy, diff --git a/flow360/component/simulation/draft_context/context.py b/flow360/component/simulation/draft_context/context.py index cacf0375e..5408b3de3 100644 --- a/flow360/component/simulation/draft_context/context.py +++ b/flow360/component/simulation/draft_context/context.py @@ -76,8 +76,8 @@ def __init__( self, *, entity_info: EntityInfoModel, - imported_geometry_components: List, - imported_surface_components: List[ImportedSurface], + imported_geometry_components: Optional[List] = None, + imported_surface_components: Optional[List[ImportedSurface]] = None, mirror_status: Optional[MirrorStatus] = None, coordinate_system_status: Optional[CoordinateSystemStatus] = None, ) -> None: diff --git a/flow360/component/simulation/primitives.py b/flow360/component/simulation/primitives.py index 2e162043b..c5abad082 100644 --- a/flow360/component/simulation/primitives.py +++ b/flow360/component/simulation/primitives.py @@ -776,9 +776,7 @@ class ImportedSurface(EntityBase): "ImportedSurface", frozen=True ) - private_attribute_sub_components: List[str] = pd.Field( - description="A list of sub components" - ) + private_attribute_sub_components: List[str] = pd.Field(description="A list of sub components") file_name: Optional[str] = None surface_mesh_id: Optional[str] = None From baf4035e798142c949ffe3a6ddc8d8a67d863598 Mon Sep 17 00:00:00 2001 From: Angran Date: Thu, 25 Dec 2025 02:13:07 +0000 Subject: [PATCH 22/38] Add validation for improper surface field usage for imported surface --- .../component/simulation/outputs/outputs.py | 19 ++ .../simulation/validation/validation_utils.py | 62 ++++++ .../params/test_validators_output.py | 202 ++++++++++++++++++ 3 files changed, 283 insertions(+) diff --git a/flow360/component/simulation/outputs/outputs.py b/flow360/component/simulation/outputs/outputs.py index a5f97c0aa..618ee15ba 100644 --- a/flow360/component/simulation/outputs/outputs.py +++ b/flow360/component/simulation/outputs/outputs.py @@ -67,6 +67,7 @@ ) from flow360.component.simulation.validation.validation_utils import ( validate_entity_list_surface_existence, + validate_improper_surface_field_usage_for_imported_surface, ) from flow360.component.types import Axis @@ -345,6 +346,15 @@ def ensure_surface_existence(cls, value, param_info: ParamsValidationInfo): """Ensure all boundaries will be present after mesher""" return validate_entity_list_surface_existence(value, param_info) + @contextual_model_validator(mode="after") + def validate_imported_surface_output_fields(self, param_info: ParamsValidationInfo): + """Validate output fields when using imported surfaces""" + expanded_entities = param_info.expand_entity_list(self.entities) + validate_improper_surface_field_usage_for_imported_surface( + expanded_entities, self.output_fields + ) + return self + class TimeAverageSurfaceOutput(SurfaceOutput): """ @@ -689,6 +699,15 @@ def allow_only_simulation_surfaces_or_imported_surfaces( ) return value + @contextual_model_validator(mode="after") + def validate_imported_surface_output_fields(self, param_info: ParamsValidationInfo): + """Validate output fields when using imported surfaces""" + expanded_entities = param_info.expand_entity_list(self.entities) + validate_improper_surface_field_usage_for_imported_surface( + expanded_entities, self.output_fields + ) + return self + class RenderOutputGroup(Flow360BaseModel): """ diff --git a/flow360/component/simulation/validation/validation_utils.py b/flow360/component/simulation/validation/validation_utils.py index 1c08c98a7..69638d24c 100644 --- a/flow360/component/simulation/validation/validation_utils.py +++ b/flow360/component/simulation/validation/validation_utils.py @@ -9,11 +9,14 @@ from pydantic_core import InitErrorDetails from flow360.component.simulation.entity_info import DraftEntityTypes +from flow360.component.simulation.outputs.output_fields import CommonFieldNames from flow360.component.simulation.primitives import ( + ImportedSurface, Surface, _SurfaceEntityBase, _VolumeEntityBase, ) +from flow360.component.simulation.user_code.core.types import Expression, UserVariable def _validator_append_instance_name(func): @@ -420,3 +423,62 @@ def has_mirroring_usage(asset_cache) -> bool: if mirror_status.mirrored_geometry_body_groups or mirror_status.mirrored_surfaces: return True return False + + +def validate_improper_surface_field_usage_for_imported_surface( + expanded_entities: list, output_fields +): + """ + Validate output fields when using imported surfaces. + Ensures that: + - String format output fields are only CommonFieldNames + - UserVariable expressions only contain Volume type solver variables + + Parameters + ---------- + expanded_entities : list + List of expanded entities (surfaces) + output_fields : UniqueItemList + List of output fields to validate + + Raises + ------ + ValueError + If any output field is not compatible with imported surfaces + """ + + # Check if any entity is an ImportedSurface + has_imported_surface = any(isinstance(entity, ImportedSurface) for entity in expanded_entities) + + if not has_imported_surface: + return + + # Get valid common field names + valid_common_fields = get_args(CommonFieldNames) + + # Validate each output field + for output_item in output_fields.items: + # Check string fields + if isinstance(output_item, str): + if output_item not in valid_common_fields: + raise ValueError( + f"Output field '{output_item}' is not allowed for imported surfaces. " + "Only non-Surface field names are allowed for string format output fields " + "when using imported surfaces." + ) + # Check UserVariable fields + elif isinstance(output_item, UserVariable) and isinstance(output_item.value, Expression): + surface_solver_variable_names = output_item.value.solver_variable_names( + recursive=True, variable_type="Surface" + ) + # Allow node_unit_normal surface variable for imported surfaces + disallowed_surface_vars = [ + var for var in surface_solver_variable_names if var != "solution.node_unit_normal" + ] + if len(disallowed_surface_vars) > 0: + raise ValueError( + f"Variable `{output_item}` cannot be used with imported surfaces " + f"since it contains Surface type solver variable(s): " + f"{', '.join(sorted(disallowed_surface_vars))}. " + "Only Volume type solver variables and 'solution.node_unit_normal' are allowed." + ) diff --git a/tests/simulation/params/test_validators_output.py b/tests/simulation/params/test_validators_output.py index a55c5326b..9e6d48e7e 100644 --- a/tests/simulation/params/test_validators_output.py +++ b/tests/simulation/params/test_validators_output.py @@ -612,6 +612,208 @@ def test_surface_integral_entity_types(mock_validation_context): ) +def test_imported_surface_output_fields_validation(mock_validation_context): + """Test that imported surfaces only allow CommonFieldNames and Volume solver variables""" + imported_surface = ImportedSurface(name="imported", file_name="imported.stl") + surface = Surface(name="fluid/body") + + # Test 1: Surface-specific field name (not in CommonFieldNames) should fail with imported surface + with mock_validation_context, pytest.raises( + ValueError, + match=re.escape( + "Output field 'Cf' is not allowed for imported surfaces. " + "Only non-Surface field names are allowed for string format output fields when using imported surfaces." + ), + ): + with imperial_unit_system: + SimulationParams( + outputs=[ + SurfaceOutput( + entities=imported_surface, + output_fields=["Cf"], # Cf is surface-specific, not in CommonFieldNames + ) + ], + ) + + # Test 2: UserVariable with Surface solver variables should fail with imported surface + uv_surface = UserVariable(name="uv_surface", value=math.dot(solution.velocity, solution.CfVec)) + with mock_validation_context, pytest.raises( + ValueError, + match=re.escape( + "Variable `uv_surface` cannot be used with imported surfaces " + "since it contains Surface type solver variable(s): solution.CfVec. " + "Only Volume type solver variables and 'solution.node_unit_normal' are allowed." + ), + ): + with imperial_unit_system: + SimulationParams( + outputs=[ + SurfaceOutput( + entities=imported_surface, + output_fields=[uv_surface], + ) + ], + ) + + # Test 3: Multiple Surface solver variables in UserVariable should fail with imported surface + uv_multiple_surface = UserVariable( + name="uv_multiple", + value=solution.node_forces_per_unit_area[0] * solution.Cp * solution.Cf, + ) + with mock_validation_context, pytest.raises( + ValueError, + match=re.escape( + "Variable `uv_multiple` cannot be used with imported surfaces " + "since it contains Surface type solver variable(s): solution.Cf, solution.node_forces_per_unit_area. " + "Only Volume type solver variables and 'solution.node_unit_normal' are allowed." + ), + ): + with imperial_unit_system: + SimulationParams( + outputs=[ + SurfaceOutput( + entities=imported_surface, + output_fields=[uv_multiple_surface], + ) + ], + ) + + # Test 4: CommonFieldNames should work with imported surface + with mock_validation_context, imperial_unit_system: + SimulationParams( + outputs=[ + SurfaceOutput( + entities=imported_surface, + output_fields=["Cp", "Mach"], # These are CommonFieldNames + ) + ], + ) + + # Test 5: UserVariable with only Volume solver variables should work with imported surface + uv_volume = UserVariable( + name="uv_volume", value=math.dot(solution.velocity, solution.vorticity) + ) + with mock_validation_context, imperial_unit_system: + SimulationParams( + outputs=[ + SurfaceOutput( + entities=imported_surface, + output_fields=[uv_volume], + ) + ], + ) + # Test 5.5: UserVariable with node_unit_normal should work with imported surface + uv_node_normal = UserVariable( + name="uv_node_normal", value=math.dot(solution.velocity, solution.node_unit_normal) + ) + with mock_validation_context, imperial_unit_system: + SimulationParams( + outputs=[ + SurfaceOutput( + entities=imported_surface, + output_fields=[uv_node_normal], + ) + ], + ) + + # Test 6: Regular surfaces should not be affected - surface-specific fields should work + with mock_validation_context, imperial_unit_system: + SimulationParams( + outputs=[ + SurfaceOutput( + entities=surface, + output_fields=[ + "Cf" + ], # Surface-specific fields should work for regular surfaces + ) + ], + ) + + # Test 7: Mixed entities (imported + regular surfaces) should trigger validation + with mock_validation_context, pytest.raises( + ValueError, + match=re.escape( + "Output field 'Cf' is not allowed for imported surfaces. " + "Only non-Surface field names are allowed for string format output fields when using imported surfaces." + ), + ): + with imperial_unit_system: + SimulationParams( + outputs=[ + SurfaceOutput( + entities=[surface, imported_surface], + output_fields=["Cf"], + ) + ], + ) + + +def test_imported_surface_output_fields_validation_surface_integral(mock_validation_context): + """Test that imported surfaces in SurfaceIntegralOutput only allow CommonFieldNames and Volume solver variables""" + imported_surface = ImportedSurface(name="imported", file_name="imported.stl") + surface = Surface(name="fluid/body") + + # Test 1: UserVariable with Surface solver variables should fail with imported surface + uv_surface = UserVariable(name="uv_surface", value=math.dot(solution.velocity, solution.CfVec)) + with mock_validation_context, pytest.raises( + ValueError, + match=re.escape( + "Variable `uv_surface` cannot be used with imported surfaces " + "since it contains Surface type solver variable(s): solution.CfVec. " + "Only Volume type solver variables and 'solution.node_unit_normal' are allowed." + ), + ): + with imperial_unit_system: + SimulationParams( + outputs=[ + SurfaceIntegralOutput( + entities=imported_surface, + output_fields=[uv_surface], + ) + ], + ) + + # Test 2: UserVariable with only Volume solver variables should work with imported surface + uv_volume = UserVariable( + name="uv_volume", value=math.dot(solution.velocity, solution.vorticity) + ) + with mock_validation_context, imperial_unit_system: + SimulationParams( + outputs=[ + SurfaceIntegralOutput( + entities=imported_surface, + output_fields=[uv_volume], + ) + ], + ) + + # Test 2.5: UserVariable with node_unit_normal should work with imported surface (MassFluxProjected use case) + uv_mass_flux_projected = UserVariable( + name="MassFluxProjected", + value=math.dot(solution.density * solution.velocity, solution.node_unit_normal), + ) + with mock_validation_context, imperial_unit_system: + SimulationParams( + outputs=[ + SurfaceIntegralOutput( + entities=imported_surface, + output_fields=[uv_mass_flux_projected], + ) + ], + ) + + # Test 3: Regular surfaces should not be affected - surface variables should work + with mock_validation_context, imperial_unit_system: + SimulationParams( + outputs=[ + SurfaceIntegralOutput( + entities=surface, + output_fields=[uv_surface], + ) + ], + ) + + def test_output_frequency_settings_in_steady_simulation(): volume_mesh = VolumeMeshV2.from_local_storage( mesh_id=None, From 4b847a9511ac1ed7eedebc42f2fd6317ffa8b299 Mon Sep 17 00:00:00 2001 From: Angran Date: Thu, 25 Dec 2025 02:13:31 +0000 Subject: [PATCH 23/38] make private_attribute_sub_components an optional field in ImportedSurface --- flow360/component/simulation/primitives.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/flow360/component/simulation/primitives.py b/flow360/component/simulation/primitives.py index c5abad082..28879682f 100644 --- a/flow360/component/simulation/primitives.py +++ b/flow360/component/simulation/primitives.py @@ -776,7 +776,9 @@ class ImportedSurface(EntityBase): "ImportedSurface", frozen=True ) - private_attribute_sub_components: List[str] = pd.Field(description="A list of sub components") + private_attribute_sub_components: Optional[List[str]] = pd.Field( + None, description="A list of sub components" + ) file_name: Optional[str] = None surface_mesh_id: Optional[str] = None From 9d05f3d144220224291f86083ab6754a358deafc Mon Sep 17 00:00:00 2001 From: Angran Date: Thu, 25 Dec 2025 02:13:59 +0000 Subject: [PATCH 24/38] Add fallback logic to enable_dependency_resources --- flow360/component/simulation/web/draft.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/flow360/component/simulation/web/draft.py b/flow360/component/simulation/web/draft.py index 4172a282c..fdf7d8c9f 100644 --- a/flow360/component/simulation/web/draft.py +++ b/flow360/component/simulation/web/draft.py @@ -146,6 +146,9 @@ def update_simulation_params(self, params): def enable_dependency_resources(self, active_draft): """Enable dependency resources for the draft""" + if active_draft is None: + return + geometry_dependencies = [ geometry.id for geometry in active_draft.imported_geometry_components ] From 3fb784fee55f4f5bb410dc293fd643e7910ee812 Mon Sep 17 00:00:00 2001 From: Angran Date: Thu, 25 Dec 2025 02:16:48 +0000 Subject: [PATCH 25/38] Fix legacy project integration test --- tests/conftest.py | 4 +++- tests/mock_server.py | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 7a54aef1b..0a52e3362 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -90,13 +90,14 @@ def mock_geometry(): def mock_surface_mesh(): surface_data = Path(__file__).parent / "simulation" / "service" / "data" surface_meta = local_metadata_builder( - id="surface-mesh", + id="sm-11111111-1111-1111-1111-111111111111", name="surface-mesh", cloud_path_prefix="--", status="processed", ) surface_mesh = SurfaceMeshV2.from_local_storage( + mesh_id=surface_meta["id"], local_storage_path=surface_data, meta_data=SurfaceMeshMetaV2(**surface_meta), ) @@ -114,6 +115,7 @@ def mock_volume_mesh(): cloud_path_prefix="--", ) volume_mesh = VolumeMeshV2.from_local_storage( + mesh_id=volume_meta["id"], local_storage_path=data_root / volume_meta["id"], meta_data=VolumeMeshMetaV2(**volume_meta), ) diff --git a/tests/mock_server.py b/tests/mock_server.py index b07ef7fac..8a1af9f7e 100644 --- a/tests/mock_server.py +++ b/tests/mock_server.py @@ -289,6 +289,19 @@ def json(): return res +class MockResponseProjectEmptyDependency(MockResponse): + """response for Project.from_cloud(id="prj-41d2333b-85fd-4bed-ae13-15dcb6da519e")'s meta json""" + + @staticmethod + def json(): + return { + "data": { + "geometryDependencyResources": [], + "surfaceMeshDependencyResources": [], + } + } + + class MockResponseProjectFromVMTree(MockResponse): """response for Project.from_cloud(id="prj-99cc6f96-15d3-4170-973c-a0cced6bf36b")'s tree json""" @@ -541,6 +554,7 @@ def json(): "/volumemeshes/00000000-0000-0000-0000-000000000000": MockResponseVolumeMesh, "/v2/geometries/00000000-0000-0000-0000-000000000000": MockResponseGeometryV2, "/v2/projects/prj-29e35434-2148-47c8-b548-58b479c37b99": MockResponseGeometryProjectV2, + "/v2/projects/prj-29e35434-2148-47c8-b548-58b479c37b99/dependency": MockResponseProjectEmptyDependency, "/v2/geometries/00000000-0000-0000-0000-000000000000/simulation/file": MockResponseGeometrySimConfigV2, "/cases/00000000-0000-0000-0000-000000000000/runtimeParams": MockResponseCaseRuntimeParams, "/v2/cases/00000000-0000-0000-0000-000000000000/file?filename=simulation.json": MockResponseSimulationJsonFile, @@ -553,7 +567,9 @@ def json(): "/folders/items/folder-3834758b-3d39-4a4a-ad85-710b7652267c/metadata": MockResponseFolderRootMetadata, "/folders/items/folder-4da3cdd0-c5b6-4130-9ca1-196237322ab9/metadata": MockResponseFolderNestedMetadata, "/v2/projects/prj-41d2333b-85fd-4bed-ae13-15dcb6da519e": MockResponseProject, + "/v2/projects/prj-41d2333b-85fd-4bed-ae13-15dcb6da519e/dependency": MockResponseProjectEmptyDependency, "/v2/projects/prj-99cc6f96-15d3-4170-973c-a0cced6bf36b": MockResponseProjectFromVM, + "/v2/projects/prj-99cc6f96-15d3-4170-973c-a0cced6bf36b/dependency": MockResponseProjectEmptyDependency, "/v2/projects/prj-41d2333b-85fd-4bed-ae13-15dcb6da519e/tree": MockResponseProjectTree, "/v2/projects/prj-99cc6f96-15d3-4170-973c-a0cced6bf36b/tree": MockResponseProjectFromVMTree, "/v2/geometries/geo-2877e124-96ff-473d-864b-11eec8648d42": MockResponseProjectGeometry, @@ -562,6 +578,7 @@ def json(): "/v2/surface-meshes/sm-1f1f2753-fe31-47ea-b3ab-efb2313ab65a/simulation/file": MockResponseProjectSurfaceMeshSimConfig, "/v2/volume-meshes/vm-7c3681cd-8c6c-4db7-a62c-1742d825e9d3": MockResponseProjectVolumeMesh, "/v2/volume-meshes/vm-7c3681cd-8c6c-4db7-a62c-1742d825e9d3/simulation/file": MockResponseProjectVolumeMeshSimConfig, + "/v2/drafts/vm-7c3681cd-8c6c-4db7-a62c-1742d825e9d3": MockResponseProjectVolumeMesh, "/v2/volume-meshes/vm-bff35714-41b1-4251-ac74-46a40b95a330": MockResponseProjectFromVMVolumeMeshMeta, "/v2/volume-meshes/vm-bff35714-41b1-4251-ac74-46a40b95a330/simulation/file": MockResponseProjectFromVMVolumeMeshSimConfig, "/cases/case-69b8c249-fce5-412a-9927-6a79049deebb": MockResponseProjectCase, From 7b4e36b7f54d5bfe7521f9fa7c466159ce0adde3 Mon Sep 17 00:00:00 2001 From: Angran Date: Thu, 25 Dec 2025 02:25:34 +0000 Subject: [PATCH 26/38] add context reset to avoid interference from other test --- tests/simulation/params/test_validators_params.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/simulation/params/test_validators_params.py b/tests/simulation/params/test_validators_params.py index f7fb5cfb8..627e4d316 100644 --- a/tests/simulation/params/test_validators_params.py +++ b/tests/simulation/params/test_validators_params.py @@ -115,7 +115,11 @@ Surface, SurfacePrivateAttributes, ) -from flow360.component.simulation.services import ValidationCalledBy, validate_model +from flow360.component.simulation.services import ( + ValidationCalledBy, + clear_context, + validate_model, +) from flow360.component.simulation.simulation_params import SimulationParams from flow360.component.simulation.time_stepping.time_stepping import Steady, Unsteady from flow360.component.simulation.unit_system import SI_unit_system @@ -141,6 +145,11 @@ assertions = unittest.TestCase("__init__") +@pytest.fixture(autouse=True) +def reset_context(): + clear_context() + + @pytest.fixture() def surface_output_with_wall_metric(): surface_output = SurfaceOutput( From 77e0109a62216284b2dd6b71712dae4090bb9b93 Mon Sep 17 00:00:00 2001 From: Angran Date: Thu, 25 Dec 2025 02:40:31 +0000 Subject: [PATCH 27/38] Fix all existing unit test --- flow360/component/project_utils.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/flow360/component/project_utils.py b/flow360/component/project_utils.py index 8a4d028a1..4620ad0ea 100644 --- a/flow360/component/project_utils.py +++ b/flow360/component/project_utils.py @@ -96,13 +96,12 @@ def _validate_tag(tag, available: list[str], kind: str) -> str: assert face_tag is not None, print( "[Internal] Default face grouping should be set, face tag to be applied: ", face_tag ) + entity_info._group_entity_by_tag("face", face_tag) # pylint:disable=protected-access # edge_tag can be None if the geometry asset created with surface mesh - if entity_info.edge_attribute_names: + if edge_grouping is not None and entity_info.edge_attribute_names: edge_tag = _validate_tag(edge_tag, entity_info.edge_attribute_names, "edge") + entity_info._group_entity_by_tag("edge", edge_tag) # pylint:disable=protected-access - # >>> 3. Apply groupings - entity_info._group_entity_by_tag("face", face_tag) # pylint:disable=protected-access - entity_info._group_entity_by_tag("edge", edge_tag) # pylint:disable=protected-access entity_info._group_entity_by_tag("body", body_group_tag) # pylint:disable=protected-access return { From 3ab7ae31cd85b62790ab89208b62508e8c8287ab Mon Sep 17 00:00:00 2001 From: Angran Date: Thu, 25 Dec 2025 23:30:02 +0000 Subject: [PATCH 28/38] [SCFD-7181] Allow empty edge tag and attribute names --- flow360/component/simulation/entity_info.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flow360/component/simulation/entity_info.py b/flow360/component/simulation/entity_info.py index 3bd05f1c9..b1f477f91 100644 --- a/flow360/component/simulation/entity_info.py +++ b/flow360/component/simulation/entity_info.py @@ -770,7 +770,7 @@ def ordered_intersection(reference_list: List[str], intersection_set: set) -> Li def select_tag( current_tag: Optional[str], result_attrs: List[str], entity_type: str ) -> Optional[str]: - if not result_attrs: + if entity_type != "edge" and not result_attrs: raise ValueError(f"No attribute names available to select {entity_type} group tag.") log.info(f"Preserving {entity_type} group tag: {current_tag}") return current_tag From 7d5b690e86f1bd4ba90e3cc69c831480cf3e471e Mon Sep 17 00:00:00 2001 From: Angran Date: Fri, 26 Dec 2025 02:37:51 +0000 Subject: [PATCH 29/38] [SCFD-7128] Preserve user's face name settings after entity merge --- flow360/component/simulation/entity_info.py | 120 +++++++++++++++----- 1 file changed, 89 insertions(+), 31 deletions(-) diff --git a/flow360/component/simulation/entity_info.py b/flow360/component/simulation/entity_info.py index b1f477f91..dec3a75c0 100644 --- a/flow360/component/simulation/entity_info.py +++ b/flow360/component/simulation/entity_info.py @@ -2,7 +2,7 @@ from abc import ABCMeta, abstractmethod from collections import defaultdict -from typing import Annotated, Dict, List, Literal, Optional, Union +from typing import Annotated, Any, Dict, List, Literal, Optional, Union import pydantic as pd @@ -723,6 +723,7 @@ def update_geometry_entity_info( 6. Draft and Ghost entities: Preserve from current_entity_info. """ # pylint: disable=too-many-locals, too-many-statements + if not entity_info_components: raise ValueError("entity_info_components cannot be empty") @@ -794,18 +795,82 @@ def select_tag( else: result_bounding_box = result_bounding_box.expand(entity_info.global_bounding_box) - # Build mapping of body group ID to mesh_exterior from current_entity_info - current_body_user_settings_map = {} - for body_group_idx, body_group_name in enumerate(current_entity_info.body_attribute_names): - current_body_user_settings_map[body_group_name] = {} - for body in current_entity_info.grouped_bodies[body_group_idx]: - body_id = body.private_attribute_id - current_body_user_settings_map[body_group_name][body_id] = { - "mesh_exterior": body.mesh_exterior, - "name": body.name, - } - - # 5. Merge grouped entities from entity_info_components + # 5. Get current user settings from body group and face + def get_current_user_settings_map( + entity_info: GeometryEntityInfo, + entity_type: Literal["body", "face"], + ) -> Dict[str, Dict[str, Dict[str, Any]]]: + """ + Extract user settings (like mesh_exterior, name) from entity_info. + + Args: + entity_info: The GeometryEntityInfo to extract settings from + entity_type: Either "body" or "face" + + Returns: + A nested dictionary: {attribute_name: {entity_id: {setting_key: setting_value}}} + """ + user_settings_map = {} + + if entity_type == "body": + attribute_names = entity_info.body_attribute_names + grouped_entities = entity_info.grouped_bodies + settings_keys = ["mesh_exterior", "name"] + elif entity_type == "face": + attribute_names = entity_info.face_attribute_names + grouped_entities = entity_info.grouped_faces + settings_keys = ["name"] + else: + raise ValueError(f"Invalid entity_type: {entity_type}. Must be 'body' or 'face'.") + + for group_idx, group_name in enumerate(attribute_names): + user_settings_map[group_name] = {} + for entity in grouped_entities[group_idx]: + entity_id = entity.private_attribute_id + user_settings_map[group_name][entity_id] = { + key: getattr(entity, key) for key in settings_keys + } + + return user_settings_map + + current_body_user_settings_map = get_current_user_settings_map( + current_entity_info, entity_type="body" + ) + current_face_user_settings_map = get_current_user_settings_map( + current_entity_info, entity_type="face" + ) + + # 6. Merge grouped entities from entity_info_components + def apply_user_settings_to_entity( + entity: Union[GeometryBodyGroup, Surface, Edge], + attr_name: str, + user_settings_map: Optional[Dict[str, Dict[str, Dict[str, Any]]]], + ) -> Union[GeometryBodyGroup, Surface, Edge]: + """ + Apply user settings to an entity if available in the user_settings_map. + + Args: + entity: The entity to apply settings to + attr_name: The attribute name (group name) for this entity + user_settings_map: The user settings map from get_current_user_settings_map() + + Returns: + The entity with user settings applied, or the original entity if no settings found + """ + if user_settings_map is None: + return entity + + entity_id = entity.private_attribute_id + + # Check if we have user settings for this entity + if attr_name in user_settings_map and entity_id in user_settings_map[attr_name]: + # Create a copy with updated user settings + entity_data = entity.model_dump() + entity_data.update(user_settings_map[attr_name][entity_id]) + return entity.__class__.model_validate(entity_data) + + return entity + def merge_grouped_entities( entity_type: Literal["body", "face", "edge"], result_attr_names: List[str], @@ -844,24 +909,17 @@ def get_groups(entity_info): for entity in entity_groups[idx]: # Use private_attribute_id as the unique identifier entity_id = entity.private_attribute_id - if entity_id not in entity_map: - # For bodies, check if we need to preserve mesh_exterior - if ( - entity_type == "body" - and attr_name in current_body_user_settings_map - and entity_id in current_body_user_settings_map[attr_name] - ): - # Create a copy with preserved mesh_exterior - entity_data = entity.model_dump() - entity_data["mesh_exterior"] = current_body_user_settings_map[ - attr_name - ][entity_id]["mesh_exterior"] - entity_data["name"] = current_body_user_settings_map[attr_name][ - entity_id - ]["name"] - entity_map[entity_id] = GeometryBodyGroup.model_validate(entity_data) - else: - entity_map[entity_id] = entity + if entity_id in entity_map: + continue + # Apply user settings if available + user_settings_map = ( + current_body_user_settings_map + if entity_type == "body" + else current_face_user_settings_map if entity_type == "face" else None + ) + entity_map[entity_id] = apply_user_settings_to_entity( + entity, attr_name, user_settings_map + ) # Convert map to list, maintaining a stable order (sorted by entity ID) result_grouped.append(sorted(entity_map.values(), key=lambda e: e.private_attribute_id)) From 21a704fbd15b013ec93fbf09256a8e7cb2f789c8 Mon Sep 17 00:00:00 2001 From: Angran Date: Sat, 27 Dec 2025 03:27:26 +0000 Subject: [PATCH 30/38] Add unit test for merge_geometry_entity_info endpoint --- ...ependency_geometry_sphere1_simulation.json | 748 +++++++++++ ...ependency_geometry_sphere2_simulation.json | 748 +++++++++++ .../result_merged_geometry_entity_info1.json | 1051 +++++++++++++++ .../result_merged_geometry_entity_info2.json | 1051 +++++++++++++++ .../data/root_geometry_cube_simulation.json | 1156 +++++++++++++++++ tests/simulation/service/test_services_v2.py | 182 +++ 6 files changed, 4936 insertions(+) create mode 100644 tests/simulation/service/data/dependency_geometry_sphere1_simulation.json create mode 100644 tests/simulation/service/data/dependency_geometry_sphere2_simulation.json create mode 100644 tests/simulation/service/data/result_merged_geometry_entity_info1.json create mode 100644 tests/simulation/service/data/result_merged_geometry_entity_info2.json create mode 100644 tests/simulation/service/data/root_geometry_cube_simulation.json diff --git a/tests/simulation/service/data/dependency_geometry_sphere1_simulation.json b/tests/simulation/service/data/dependency_geometry_sphere1_simulation.json new file mode 100644 index 000000000..9870ec11a --- /dev/null +++ b/tests/simulation/service/data/dependency_geometry_sphere1_simulation.json @@ -0,0 +1,748 @@ +{ + "hash": "562ccb89a69df627df3a181b692b3467afdf05ae4e9586c828a98e86d765ce68", + "meshing": { + "defaults": { + "boundary_layer_growth_rate": 1.2, + "curvature_resolution_angle": { + "units": "degree", + "value": 12.0 + }, + "planar_face_tolerance": 1e-06, + "preserve_thin_geometry": false, + "remove_non_manifold_faces": false, + "resolve_face_boundaries": false, + "sealing_size": { + "units": "m", + "value": 0.0 + }, + "sliding_interface_tolerance": 0.01, + "surface_edge_growth_rate": 1.2, + "surface_max_adaptation_iterations": 50, + "surface_max_aspect_ratio": 10.0 + }, + "outputs": [], + "refinement_factor": 1.0, + "refinements": [], + "type_name": "MeshingParams", + "volume_zones": [ + { + "method": "auto", + "name": "Farfield", + "relative_size": 50.0, + "type": "AutomatedFarfield" + } + ] + }, + "models": [ + { + "entities": { + "stored_entities": [ + { + "name": "*", + "private_attribute_entity_type_name": "Surface", + "private_attribute_sub_components": [] + } + ] + }, + "heat_spec": { + "type_name": "HeatFlux", + "value": { + "units": "W/m**2", + "value": 0.0 + } + }, + "name": "Wall", + "private_attribute_id": "b7990356-c29b-44ee-aa5e-a163a18848d0", + "roughness_height": { + "units": "m", + "value": 0.0 + }, + "type": "Wall", + "use_wall_function": false + }, + { + "entities": { + "stored_entities": [ + { + "name": "farfield", + "private_attribute_entity_type_name": "GhostSurface", + "private_attribute_id": "farfield" + } + ] + }, + "name": "Freestream", + "private_attribute_id": "a00c9458-066e-4207-94c7-56474e62fe60", + "type": "Freestream" + }, + { + "initial_condition": { + "p": "p", + "rho": "rho", + "type_name": "NavierStokesInitialCondition", + "u": "u", + "v": "v", + "w": "w" + }, + "interface_interpolation_tolerance": 0.2, + "material": { + "dynamic_viscosity": { + "effective_temperature": { + "units": "K", + "value": 110.4 + }, + "reference_temperature": { + "units": "K", + "value": 273.15 + }, + "reference_viscosity": { + "units": "Pa*s", + "value": 1.716e-05 + } + }, + "name": "air", + "type": "air" + }, + "navier_stokes_solver": { + "CFL_multiplier": 1.0, + "absolute_tolerance": 1e-10, + "equation_evaluation_frequency": 1, + "kappa_MUSCL": -1.0, + "limit_pressure_density": false, + "limit_velocity": false, + "linear_solver": { + "max_iterations": 30 + }, + "low_mach_preconditioner": false, + "max_force_jac_update_physical_steps": 0, + "numerical_dissipation_factor": 1.0, + "order_of_accuracy": 2, + "relative_tolerance": 0.0, + "type_name": "Compressible", + "update_jacobian_frequency": 4 + }, + "private_attribute_id": "__default_fluid", + "transition_model_solver": { + "type_name": "None" + }, + "turbulence_model_solver": { + "CFL_multiplier": 2.0, + "absolute_tolerance": 1e-08, + "equation_evaluation_frequency": 4, + "linear_solver": { + "max_iterations": 20 + }, + "low_reynolds_correction": false, + "max_force_jac_update_physical_steps": 0, + "modeling_constants": { + "C_DES": 0.72, + "C_cb1": 0.1355, + "C_cb2": 0.622, + "C_d": 8.0, + "C_min_rd": 10.0, + "C_sigma": 0.6666666666666666, + "C_t3": 1.2, + "C_t4": 0.5, + "C_v1": 7.1, + "C_vonKarman": 0.41, + "C_w2": 0.3, + "C_w4": 0.21, + "C_w5": 1.5, + "type_name": "SpalartAllmarasConsts" + }, + "order_of_accuracy": 2, + "quadratic_constitutive_relation": false, + "reconstruction_gradient_limiter": 0.5, + "relative_tolerance": 0.0, + "rotation_correction": false, + "type_name": "SpalartAllmaras", + "update_jacobian_frequency": 4 + }, + "type": "Fluid" + } + ], + "operating_condition": { + "alpha": { + "units": "degree", + "value": 0.0 + }, + "beta": { + "units": "degree", + "value": 0.0 + }, + "private_attribute_constructor": "default", + "private_attribute_input_cache": { + "alpha": { + "units": "degree", + "value": 0.0 + }, + "beta": { + "units": "degree", + "value": 0.0 + }, + "thermal_state": { + "density": { + "units": "kg/m**3", + "value": 1.225 + }, + "material": { + "dynamic_viscosity": { + "effective_temperature": { + "units": "K", + "value": 110.4 + }, + "reference_temperature": { + "units": "K", + "value": 273.15 + }, + "reference_viscosity": { + "units": "Pa*s", + "value": 1.716e-05 + } + }, + "name": "air", + "type": "air" + }, + "private_attribute_constructor": "default", + "private_attribute_input_cache": {}, + "temperature": { + "units": "K", + "value": 288.15 + }, + "type_name": "ThermalState" + } + }, + "thermal_state": { + "density": { + "units": "kg/m**3", + "value": 1.225 + }, + "material": { + "dynamic_viscosity": { + "effective_temperature": { + "units": "K", + "value": 110.4 + }, + "reference_temperature": { + "units": "K", + "value": 273.15 + }, + "reference_viscosity": { + "units": "Pa*s", + "value": 1.716e-05 + } + }, + "name": "air", + "type": "air" + }, + "private_attribute_constructor": "default", + "private_attribute_input_cache": {}, + "temperature": { + "units": "K", + "value": 288.15 + }, + "type_name": "ThermalState" + }, + "type_name": "AerospaceCondition" + }, + "outputs": [ + { + "entities": { + "stored_entities": [ + { + "name": "*", + "private_attribute_entity_type_name": "Surface", + "private_attribute_sub_components": [] + } + ] + }, + "frequency": -1, + "frequency_offset": 0, + "name": "Surface output", + "output_fields": { + "items": [ + "Cp", + "yPlus", + "Cf", + "CfVec" + ] + }, + "output_format": "paraview", + "output_type": "SurfaceOutput", + "private_attribute_id": "39c6e6c8-9756-401f-bb12-8d037d829908", + "write_single_file": false + } + ], + "private_attribute_asset_cache": { + "project_entity_info": { + "bodies_face_edge_ids": { + "sphere1_body00001": { + "edge_ids": [ + "sphere1_body00001_edge00001", + "sphere1_body00001_edge00003" + ], + "face_ids": [ + "sphere1_body00001_face00001", + "sphere1_body00001_face00002" + ] + } + }, + "body_attribute_names": [ + "bodyId", + "groupByFile" + ], + "body_group_tag": "groupByFile", + "body_ids": [ + "sphere1_body00001" + ], + "default_geometry_accuracy": { + "units": "m", + "value": 0.0001 + }, + "draft_entities": [], + "edge_attribute_names": [ + "name", + "edgeId" + ], + "edge_group_tag": "name", + "edge_ids": [ + "sphere1_body00001_edge00001", + "sphere1_body00001_edge00003" + ], + "face_attribute_names": [ + "builtinName", + "groupByBodyId", + "name", + "faceId" + ], + "face_group_tag": "name", + "face_ids": [ + "sphere1_body00001_face00001", + "sphere1_body00001_face00002" + ], + "ghost_entities": [ + { + "center": [ + 0, + 0, + 0 + ], + "max_radius": 200.0, + "name": "farfield", + "private_attribute_entity_type_name": "GhostSphere", + "private_attribute_id": "farfield" + }, + { + "center": [ + 0.0, + -2.0, + 0.0 + ], + "max_radius": 200.0, + "name": "symmetric-1", + "normal_axis": [ + 0, + -1, + 0 + ], + "private_attribute_entity_type_name": "GhostCircularPlane", + "private_attribute_id": "symmetric-1" + }, + { + "center": [ + 0.0, + 2.0, + 0.0 + ], + "max_radius": 200.0, + "name": "symmetric-2", + "normal_axis": [ + 0, + 1, + 0 + ], + "private_attribute_entity_type_name": "GhostCircularPlane", + "private_attribute_id": "symmetric-2" + }, + { + "center": [ + 0.0, + 0, + 0.0 + ], + "max_radius": 200.0, + "name": "symmetric", + "normal_axis": [ + 0, + 1, + 0 + ], + "private_attribute_entity_type_name": "GhostCircularPlane", + "private_attribute_id": "symmetric" + }, + { + "name": "windTunnelInlet", + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelInlet", + "used_by": [ + "all" + ] + }, + { + "name": "windTunnelOutlet", + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelOutlet", + "used_by": [ + "all" + ] + }, + { + "name": "windTunnelCeiling", + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelCeiling", + "used_by": [ + "all" + ] + }, + { + "name": "windTunnelFloor", + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelFloor", + "used_by": [ + "all" + ] + }, + { + "name": "windTunnelLeft", + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelLeft", + "used_by": [ + "all" + ] + }, + { + "name": "windTunnelRight", + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelRight", + "used_by": [ + "all" + ] + }, + { + "name": "windTunnelFrictionPatch", + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelFrictionPatch", + "used_by": [ + "StaticFloor" + ] + }, + { + "name": "windTunnelCentralBelt", + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelCentralBelt", + "used_by": [ + "CentralBelt", + "WheelBelts" + ] + }, + { + "name": "windTunnelFrontWheelBelt", + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelFrontWheelBelt", + "used_by": [ + "WheelBelts" + ] + }, + { + "name": "windTunnelRearWheelBelt", + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelRearWheelBelt", + "used_by": [ + "WheelBelts" + ] + } + ], + "global_bounding_box": [ + [ + -2.0, + -2.0, + -2.0 + ], + [ + 2.0, + 2.0, + 2.0 + ] + ], + "grouped_bodies": [ + [ + { + "mesh_exterior": true, + "name": "sphere1_body00001", + "private_attribute_entity_type_name": "GeometryBodyGroup", + "private_attribute_id": "ddf34d3d-1354-46dd-9165-0c18f4010b31", + "private_attribute_sub_components": [ + "sphere1_body00001" + ], + "private_attribute_tag_key": "bodyId" + } + ], + [ + { + "mesh_exterior": true, + "name": "sphere_r2mm.step", + "private_attribute_entity_type_name": "GeometryBodyGroup", + "private_attribute_id": "442cc0e0-da72-4784-8036-fd5c1e643831", + "private_attribute_sub_components": [ + "sphere1_body00001" + ], + "private_attribute_tag_key": "groupByFile" + } + ] + ], + "grouped_edges": [ + [ + { + "name": "sphere1_body00001_edge00001", + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "9cbefe8c-82e0-486d-b805-6ee5ecd49c21", + "private_attribute_sub_components": [ + "sphere1_body00001_edge00001" + ], + "private_attribute_tag_key": "name" + }, + { + "name": "sphere1_body00001_edge00003", + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "48869a6b-c99e-46fd-a816-2833aa16283c", + "private_attribute_sub_components": [ + "sphere1_body00001_edge00003" + ], + "private_attribute_tag_key": "name" + } + ], + [ + { + "name": "sphere1_body00001_edge00001", + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "dc6dea8e-240d-4eb8-9c32-d4a42a0d99b7", + "private_attribute_sub_components": [ + "sphere1_body00001_edge00001" + ], + "private_attribute_tag_key": "edgeId" + }, + { + "name": "sphere1_body00001_edge00003", + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "adb4961e-0e43-400f-97a6-ffb07c16e472", + "private_attribute_sub_components": [ + "sphere1_body00001_edge00003" + ], + "private_attribute_tag_key": "edgeId" + } + ] + ], + "grouped_faces": [ + [ + { + "name": "No Name", + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "e8473b81-9840-441d-a45d-534320f3b330", + "private_attribute_sub_components": [ + "sphere1_body00001_face00001", + "sphere1_body00001_face00002" + ], + "private_attribute_tag_key": "builtinName", + "private_attributes": { + "bounding_box": [ + [ + -2.0, + -2.0, + -2.0 + ], + [ + 2.0, + 2.0, + 2.0 + ] + ], + "type_name": "SurfacePrivateAttributes" + } + } + ], + [ + { + "name": "sphere1_body00001", + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "3a150e35-76bc-4e47-84f1-238f9200d0ea", + "private_attribute_sub_components": [ + "sphere1_body00001_face00001", + "sphere1_body00001_face00002" + ], + "private_attribute_tag_key": "groupByBodyId", + "private_attributes": { + "bounding_box": [ + [ + -2.0, + -2.0, + -2.0 + ], + [ + 2.0, + 2.0, + 2.0 + ] + ], + "type_name": "SurfacePrivateAttributes" + } + } + ], + [ + { + "name": "sphere1_body00001_face00001", + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "d5922440-36fc-4308-aac4-b040ee31d950", + "private_attribute_sub_components": [ + "sphere1_body00001_face00001" + ], + "private_attribute_tag_key": "name", + "private_attributes": { + "bounding_box": [ + [ + -2.0, + -4.499279347985573e-32, + -2.0 + ], + [ + 2.0, + 2.0, + 2.0 + ] + ], + "type_name": "SurfacePrivateAttributes" + } + }, + { + "name": "sphere1_body00001_face00002", + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "238489c6-b158-44bb-b5b4-d6cc17a69b51", + "private_attribute_sub_components": [ + "sphere1_body00001_face00002" + ], + "private_attribute_tag_key": "name", + "private_attributes": { + "bounding_box": [ + [ + -2.0, + -2.0, + -2.0 + ], + [ + 2.0, + 2.4492935982947064e-16, + 2.0 + ] + ], + "type_name": "SurfacePrivateAttributes" + } + } + ], + [ + { + "name": "sphere1_body00001_face00001", + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "3d8cd3d8-4719-40db-9929-d683a504e670", + "private_attribute_sub_components": [ + "sphere1_body00001_face00001" + ], + "private_attribute_tag_key": "faceId", + "private_attributes": { + "bounding_box": [ + [ + -2.0, + -4.499279347985573e-32, + -2.0 + ], + [ + 2.0, + 2.0, + 2.0 + ] + ], + "type_name": "SurfacePrivateAttributes" + } + }, + { + "name": "sphere1_body00001_face00002", + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "a932d870-d169-49aa-83e1-be9fbffcbe2d", + "private_attribute_sub_components": [ + "sphere1_body00001_face00002" + ], + "private_attribute_tag_key": "faceId", + "private_attributes": { + "bounding_box": [ + [ + -2.0, + -2.0, + -2.0 + ], + [ + 2.0, + 2.4492935982947064e-16, + 2.0 + ] + ], + "type_name": "SurfacePrivateAttributes" + } + } + ] + ], + "type_name": "GeometryEntityInfo" + }, + "project_length_unit": { + "units": "m", + "value": 1.0 + }, + "use_geometry_AI": false, + "use_inhouse_mesher": false + }, + "reference_geometry": { + "area": { + "type_name": "number", + "units": "m**2", + "value": 1.0 + }, + "moment_center": { + "units": "m", + "value": [ + 0.0, + 0.0, + 0.0 + ] + }, + "moment_length": { + "units": "m", + "value": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + "time_stepping": { + "CFL": { + "convergence_limiting_factor": 0.25, + "max": 10000.0, + "max_relative_change": 1.0, + "min": 0.1, + "type": "adaptive" + }, + "max_steps": 2000, + "type_name": "Steady" + }, + "unit_system": { + "name": "SI" + }, + "user_defined_fields": [], + "version": "25.8.2b1" +} \ No newline at end of file diff --git a/tests/simulation/service/data/dependency_geometry_sphere2_simulation.json b/tests/simulation/service/data/dependency_geometry_sphere2_simulation.json new file mode 100644 index 000000000..f528d6105 --- /dev/null +++ b/tests/simulation/service/data/dependency_geometry_sphere2_simulation.json @@ -0,0 +1,748 @@ +{ + "hash": "800de3ec2de57facdf30ae67b66bda611402446426f2fd2882541995e92cac65", + "meshing": { + "defaults": { + "boundary_layer_growth_rate": 1.2, + "curvature_resolution_angle": { + "units": "degree", + "value": 12.0 + }, + "planar_face_tolerance": 1e-06, + "preserve_thin_geometry": false, + "remove_non_manifold_faces": false, + "resolve_face_boundaries": false, + "sealing_size": { + "units": "m", + "value": 0.0 + }, + "sliding_interface_tolerance": 0.01, + "surface_edge_growth_rate": 1.2, + "surface_max_adaptation_iterations": 50, + "surface_max_aspect_ratio": 10.0 + }, + "outputs": [], + "refinement_factor": 1.0, + "refinements": [], + "type_name": "MeshingParams", + "volume_zones": [ + { + "method": "auto", + "name": "Farfield", + "relative_size": 50.0, + "type": "AutomatedFarfield" + } + ] + }, + "models": [ + { + "entities": { + "stored_entities": [ + { + "name": "*", + "private_attribute_entity_type_name": "Surface", + "private_attribute_sub_components": [] + } + ] + }, + "heat_spec": { + "type_name": "HeatFlux", + "value": { + "units": "W/m**2", + "value": 0.0 + } + }, + "name": "Wall", + "private_attribute_id": "b7990356-c29b-44ee-aa5e-a163a18848d0", + "roughness_height": { + "units": "m", + "value": 0.0 + }, + "type": "Wall", + "use_wall_function": false + }, + { + "entities": { + "stored_entities": [ + { + "name": "farfield", + "private_attribute_entity_type_name": "GhostSurface", + "private_attribute_id": "farfield" + } + ] + }, + "name": "Freestream", + "private_attribute_id": "a00c9458-066e-4207-94c7-56474e62fe60", + "type": "Freestream" + }, + { + "initial_condition": { + "p": "p", + "rho": "rho", + "type_name": "NavierStokesInitialCondition", + "u": "u", + "v": "v", + "w": "w" + }, + "interface_interpolation_tolerance": 0.2, + "material": { + "dynamic_viscosity": { + "effective_temperature": { + "units": "K", + "value": 110.4 + }, + "reference_temperature": { + "units": "K", + "value": 273.15 + }, + "reference_viscosity": { + "units": "Pa*s", + "value": 1.716e-05 + } + }, + "name": "air", + "type": "air" + }, + "navier_stokes_solver": { + "CFL_multiplier": 1.0, + "absolute_tolerance": 1e-10, + "equation_evaluation_frequency": 1, + "kappa_MUSCL": -1.0, + "limit_pressure_density": false, + "limit_velocity": false, + "linear_solver": { + "max_iterations": 30 + }, + "low_mach_preconditioner": false, + "max_force_jac_update_physical_steps": 0, + "numerical_dissipation_factor": 1.0, + "order_of_accuracy": 2, + "relative_tolerance": 0.0, + "type_name": "Compressible", + "update_jacobian_frequency": 4 + }, + "private_attribute_id": "__default_fluid", + "transition_model_solver": { + "type_name": "None" + }, + "turbulence_model_solver": { + "CFL_multiplier": 2.0, + "absolute_tolerance": 1e-08, + "equation_evaluation_frequency": 4, + "linear_solver": { + "max_iterations": 20 + }, + "low_reynolds_correction": false, + "max_force_jac_update_physical_steps": 0, + "modeling_constants": { + "C_DES": 0.72, + "C_cb1": 0.1355, + "C_cb2": 0.622, + "C_d": 8.0, + "C_min_rd": 10.0, + "C_sigma": 0.6666666666666666, + "C_t3": 1.2, + "C_t4": 0.5, + "C_v1": 7.1, + "C_vonKarman": 0.41, + "C_w2": 0.3, + "C_w4": 0.21, + "C_w5": 1.5, + "type_name": "SpalartAllmarasConsts" + }, + "order_of_accuracy": 2, + "quadratic_constitutive_relation": false, + "reconstruction_gradient_limiter": 0.5, + "relative_tolerance": 0.0, + "rotation_correction": false, + "type_name": "SpalartAllmaras", + "update_jacobian_frequency": 4 + }, + "type": "Fluid" + } + ], + "operating_condition": { + "alpha": { + "units": "degree", + "value": 0.0 + }, + "beta": { + "units": "degree", + "value": 0.0 + }, + "private_attribute_constructor": "default", + "private_attribute_input_cache": { + "alpha": { + "units": "degree", + "value": 0.0 + }, + "beta": { + "units": "degree", + "value": 0.0 + }, + "thermal_state": { + "density": { + "units": "kg/m**3", + "value": 1.225 + }, + "material": { + "dynamic_viscosity": { + "effective_temperature": { + "units": "K", + "value": 110.4 + }, + "reference_temperature": { + "units": "K", + "value": 273.15 + }, + "reference_viscosity": { + "units": "Pa*s", + "value": 1.716e-05 + } + }, + "name": "air", + "type": "air" + }, + "private_attribute_constructor": "default", + "private_attribute_input_cache": {}, + "temperature": { + "units": "K", + "value": 288.15 + }, + "type_name": "ThermalState" + } + }, + "thermal_state": { + "density": { + "units": "kg/m**3", + "value": 1.225 + }, + "material": { + "dynamic_viscosity": { + "effective_temperature": { + "units": "K", + "value": 110.4 + }, + "reference_temperature": { + "units": "K", + "value": 273.15 + }, + "reference_viscosity": { + "units": "Pa*s", + "value": 1.716e-05 + } + }, + "name": "air", + "type": "air" + }, + "private_attribute_constructor": "default", + "private_attribute_input_cache": {}, + "temperature": { + "units": "K", + "value": 288.15 + }, + "type_name": "ThermalState" + }, + "type_name": "AerospaceCondition" + }, + "outputs": [ + { + "entities": { + "stored_entities": [ + { + "name": "*", + "private_attribute_entity_type_name": "Surface", + "private_attribute_sub_components": [] + } + ] + }, + "frequency": -1, + "frequency_offset": 0, + "name": "Surface output", + "output_fields": { + "items": [ + "Cp", + "yPlus", + "Cf", + "CfVec" + ] + }, + "output_format": "paraview", + "output_type": "SurfaceOutput", + "private_attribute_id": "39c6e6c8-9756-401f-bb12-8d037d829908", + "write_single_file": false + } + ], + "private_attribute_asset_cache": { + "project_entity_info": { + "bodies_face_edge_ids": { + "sphere2_body00001": { + "edge_ids": [ + "sphere2_body00001_edge00001", + "sphere2_body00001_edge00003" + ], + "face_ids": [ + "sphere2_body00001_face00001", + "sphere2_body00001_face00002" + ] + } + }, + "body_attribute_names": [ + "bodyId", + "groupByFile" + ], + "body_group_tag": "groupByFile", + "body_ids": [ + "sphere2_body00001" + ], + "default_geometry_accuracy": { + "units": "m", + "value": 0.0001 + }, + "draft_entities": [], + "edge_attribute_names": [ + "name", + "edgeId" + ], + "edge_group_tag": "name", + "edge_ids": [ + "sphere2_body00001_edge00001", + "sphere2_body00001_edge00003" + ], + "face_attribute_names": [ + "builtinName", + "groupByBodyId", + "name", + "faceId" + ], + "face_group_tag": "name", + "face_ids": [ + "sphere2_body00001_face00001", + "sphere2_body00001_face00002" + ], + "ghost_entities": [ + { + "center": [ + 0, + 0, + 0 + ], + "max_radius": 200.0, + "name": "farfield", + "private_attribute_entity_type_name": "GhostSphere", + "private_attribute_id": "farfield" + }, + { + "center": [ + 5.0, + -2.0, + 0.0 + ], + "max_radius": 200.0, + "name": "symmetric-1", + "normal_axis": [ + 0, + -1, + 0 + ], + "private_attribute_entity_type_name": "GhostCircularPlane", + "private_attribute_id": "symmetric-1" + }, + { + "center": [ + 5.0, + 2.0, + 0.0 + ], + "max_radius": 200.0, + "name": "symmetric-2", + "normal_axis": [ + 0, + 1, + 0 + ], + "private_attribute_entity_type_name": "GhostCircularPlane", + "private_attribute_id": "symmetric-2" + }, + { + "center": [ + 5.0, + 0, + 0.0 + ], + "max_radius": 200.0, + "name": "symmetric", + "normal_axis": [ + 0, + 1, + 0 + ], + "private_attribute_entity_type_name": "GhostCircularPlane", + "private_attribute_id": "symmetric" + }, + { + "name": "windTunnelInlet", + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelInlet", + "used_by": [ + "all" + ] + }, + { + "name": "windTunnelOutlet", + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelOutlet", + "used_by": [ + "all" + ] + }, + { + "name": "windTunnelCeiling", + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelCeiling", + "used_by": [ + "all" + ] + }, + { + "name": "windTunnelFloor", + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelFloor", + "used_by": [ + "all" + ] + }, + { + "name": "windTunnelLeft", + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelLeft", + "used_by": [ + "all" + ] + }, + { + "name": "windTunnelRight", + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelRight", + "used_by": [ + "all" + ] + }, + { + "name": "windTunnelFrictionPatch", + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelFrictionPatch", + "used_by": [ + "StaticFloor" + ] + }, + { + "name": "windTunnelCentralBelt", + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelCentralBelt", + "used_by": [ + "CentralBelt", + "WheelBelts" + ] + }, + { + "name": "windTunnelFrontWheelBelt", + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelFrontWheelBelt", + "used_by": [ + "WheelBelts" + ] + }, + { + "name": "windTunnelRearWheelBelt", + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelRearWheelBelt", + "used_by": [ + "WheelBelts" + ] + } + ], + "global_bounding_box": [ + [ + 3.0, + -2.0, + -2.0 + ], + [ + 7.0, + 2.0, + 2.0 + ] + ], + "grouped_bodies": [ + [ + { + "mesh_exterior": true, + "name": "sphere2_body00001", + "private_attribute_entity_type_name": "GeometryBodyGroup", + "private_attribute_id": "fad554fd-2de9-4064-a5b3-8488a235812a", + "private_attribute_sub_components": [ + "sphere2_body00001" + ], + "private_attribute_tag_key": "bodyId" + } + ], + [ + { + "mesh_exterior": true, + "name": "sphere_r2mm_center_5_0_0.step", + "private_attribute_entity_type_name": "GeometryBodyGroup", + "private_attribute_id": "0fb73bec-f430-4efd-8dc5-4b9955df849e", + "private_attribute_sub_components": [ + "sphere2_body00001" + ], + "private_attribute_tag_key": "groupByFile" + } + ] + ], + "grouped_edges": [ + [ + { + "name": "sphere2_body00001_edge00001", + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "96b21c6a-21b5-4387-acd2-83ae71f17413", + "private_attribute_sub_components": [ + "sphere2_body00001_edge00001" + ], + "private_attribute_tag_key": "name" + }, + { + "name": "sphere2_body00001_edge00003", + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "4b30a96b-b8c1-4de2-9a5f-f447bb42f4a9", + "private_attribute_sub_components": [ + "sphere2_body00001_edge00003" + ], + "private_attribute_tag_key": "name" + } + ], + [ + { + "name": "sphere2_body00001_edge00001", + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "3193fbf3-6379-42de-ac63-5ad9401dcce2", + "private_attribute_sub_components": [ + "sphere2_body00001_edge00001" + ], + "private_attribute_tag_key": "edgeId" + }, + { + "name": "sphere2_body00001_edge00003", + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "ac831004-5a5e-4b8b-b66a-475b78308ab9", + "private_attribute_sub_components": [ + "sphere2_body00001_edge00003" + ], + "private_attribute_tag_key": "edgeId" + } + ] + ], + "grouped_faces": [ + [ + { + "name": "No Name", + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "032d8c9b-9873-4557-b7c6-cde69f6507f7", + "private_attribute_sub_components": [ + "sphere2_body00001_face00001", + "sphere2_body00001_face00002" + ], + "private_attribute_tag_key": "builtinName", + "private_attributes": { + "bounding_box": [ + [ + 3.0, + -2.0, + -2.0 + ], + [ + 7.0, + 2.0, + 2.0 + ] + ], + "type_name": "SurfacePrivateAttributes" + } + } + ], + [ + { + "name": "sphere2_body00001", + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "6ce3c95d-1bd2-434c-be8b-d88151a099a0", + "private_attribute_sub_components": [ + "sphere2_body00001_face00001", + "sphere2_body00001_face00002" + ], + "private_attribute_tag_key": "groupByBodyId", + "private_attributes": { + "bounding_box": [ + [ + 3.0, + -2.0, + -2.0 + ], + [ + 7.0, + 2.0, + 2.0 + ] + ], + "type_name": "SurfacePrivateAttributes" + } + } + ], + [ + { + "name": "sphere2_body00001_face00001", + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "ee6f39e2-15ed-4d3b-a77a-8383831bad15", + "private_attribute_sub_components": [ + "sphere2_body00001_face00001" + ], + "private_attribute_tag_key": "name", + "private_attributes": { + "bounding_box": [ + [ + 3.0, + -4.499279347985573e-32, + -2.0 + ], + [ + 7.0, + 2.0, + 2.0 + ] + ], + "type_name": "SurfacePrivateAttributes" + } + }, + { + "name": "sphere2_body00001_face00002", + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "ad2779dd-c479-4b20-b753-054a988f6fac", + "private_attribute_sub_components": [ + "sphere2_body00001_face00002" + ], + "private_attribute_tag_key": "name", + "private_attributes": { + "bounding_box": [ + [ + 3.0, + -2.0, + -2.0 + ], + [ + 7.0, + 2.4492935982947064e-16, + 2.0 + ] + ], + "type_name": "SurfacePrivateAttributes" + } + } + ], + [ + { + "name": "sphere2_body00001_face00001", + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "33dc2ccd-4b46-4444-b094-76e5fdd2249f", + "private_attribute_sub_components": [ + "sphere2_body00001_face00001" + ], + "private_attribute_tag_key": "faceId", + "private_attributes": { + "bounding_box": [ + [ + 3.0, + -4.499279347985573e-32, + -2.0 + ], + [ + 7.0, + 2.0, + 2.0 + ] + ], + "type_name": "SurfacePrivateAttributes" + } + }, + { + "name": "sphere2_body00001_face00002", + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "8c3e316d-fbd5-45ef-a79f-e60f764abcca", + "private_attribute_sub_components": [ + "sphere2_body00001_face00002" + ], + "private_attribute_tag_key": "faceId", + "private_attributes": { + "bounding_box": [ + [ + 3.0, + -2.0, + -2.0 + ], + [ + 7.0, + 2.4492935982947064e-16, + 2.0 + ] + ], + "type_name": "SurfacePrivateAttributes" + } + } + ] + ], + "type_name": "GeometryEntityInfo" + }, + "project_length_unit": { + "units": "m", + "value": 1.0 + }, + "use_geometry_AI": false, + "use_inhouse_mesher": false + }, + "reference_geometry": { + "area": { + "type_name": "number", + "units": "m**2", + "value": 1.0 + }, + "moment_center": { + "units": "m", + "value": [ + 0.0, + 0.0, + 0.0 + ] + }, + "moment_length": { + "units": "m", + "value": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + "time_stepping": { + "CFL": { + "convergence_limiting_factor": 0.25, + "max": 10000.0, + "max_relative_change": 1.0, + "min": 0.1, + "type": "adaptive" + }, + "max_steps": 2000, + "type_name": "Steady" + }, + "unit_system": { + "name": "SI" + }, + "user_defined_fields": [], + "version": "25.8.2b1" +} \ No newline at end of file diff --git a/tests/simulation/service/data/result_merged_geometry_entity_info1.json b/tests/simulation/service/data/result_merged_geometry_entity_info1.json new file mode 100644 index 000000000..8749e12ea --- /dev/null +++ b/tests/simulation/service/data/result_merged_geometry_entity_info1.json @@ -0,0 +1,1051 @@ +{ + "draft_entities": [], + "ghost_entities": [ + { + "private_attribute_entity_type_name": "GhostSphere", + "private_attribute_id": "farfield", + "name": "farfield", + "center": [ + 0, + 0, + 0 + ], + "max_radius": 100.0 + }, + { + "private_attribute_entity_type_name": "GhostCircularPlane", + "private_attribute_id": "symmetric-1", + "name": "symmetric-1", + "center": [ + 12.0, + -1.0, + 0.0 + ], + "max_radius": 100.0, + "normal_axis": [ + 0, + -1, + 0 + ] + }, + { + "private_attribute_entity_type_name": "GhostCircularPlane", + "private_attribute_id": "symmetric-2", + "name": "symmetric-2", + "center": [ + 12.0, + 1.0, + 0.0 + ], + "max_radius": 100.0, + "normal_axis": [ + 0, + 1, + 0 + ] + }, + { + "private_attribute_entity_type_name": "GhostCircularPlane", + "private_attribute_id": "symmetric", + "name": "symmetric", + "center": [ + 12.0, + 0, + 0.0 + ], + "max_radius": 100.0, + "normal_axis": [ + 0, + 1, + 0 + ] + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelInlet", + "name": "windTunnelInlet", + "used_by": [ + "all" + ] + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelOutlet", + "name": "windTunnelOutlet", + "used_by": [ + "all" + ] + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelCeiling", + "name": "windTunnelCeiling", + "used_by": [ + "all" + ] + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelFloor", + "name": "windTunnelFloor", + "used_by": [ + "all" + ] + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelLeft", + "name": "windTunnelLeft", + "used_by": [ + "all" + ] + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelRight", + "name": "windTunnelRight", + "used_by": [ + "all" + ] + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelFrictionPatch", + "name": "windTunnelFrictionPatch", + "used_by": [ + "StaticFloor" + ] + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelCentralBelt", + "name": "windTunnelCentralBelt", + "used_by": [ + "CentralBelt", + "WheelBelts" + ] + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelFrontWheelBelt", + "name": "windTunnelFrontWheelBelt", + "used_by": [ + "WheelBelts" + ] + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelRearWheelBelt", + "name": "windTunnelRearWheelBelt", + "used_by": [ + "WheelBelts" + ] + } + ], + "type_name": "GeometryEntityInfo", + "bodies_face_edge_ids": { + "body00001": { + "face_ids": [ + "body00001_face00001", + "body00001_face00002", + "body00001_face00003", + "body00001_face00004", + "body00001_face00005", + "body00001_face00006" + ], + "edge_ids": [ + "body00001_edge00001", + "body00001_edge00002", + "body00001_edge00003", + "body00001_edge00004", + "body00001_edge00005", + "body00001_edge00006", + "body00001_edge00007", + "body00001_edge00008", + "body00001_edge00009", + "body00001_edge00010", + "body00001_edge00011", + "body00001_edge00012" + ] + }, + "sphere1_body00001": { + "face_ids": [ + "sphere1_body00001_face00001", + "sphere1_body00001_face00002" + ], + "edge_ids": [ + "sphere1_body00001_edge00001", + "sphere1_body00001_edge00003" + ] + } + }, + "body_ids": [ + "body00001", + "sphere1_body00001" + ], + "body_attribute_names": [ + "bodyId", + "groupByFile" + ], + "grouped_bodies": [ + [ + { + "private_attribute_entity_type_name": "GeometryBodyGroup", + "private_attribute_id": "b496086c-136d-4f01-998d-c9e1eea120ae", + "name": "body00001", + "private_attribute_tag_key": "bodyId", + "private_attribute_sub_components": [ + "body00001" + ], + "mesh_exterior": true + }, + { + "private_attribute_entity_type_name": "GeometryBodyGroup", + "private_attribute_id": "ddf34d3d-1354-46dd-9165-0c18f4010b31", + "name": "sphere1_body00001", + "private_attribute_tag_key": "bodyId", + "private_attribute_sub_components": [ + "sphere1_body00001" + ], + "mesh_exterior": true + } + ], + [ + { + "private_attribute_entity_type_name": "GeometryBodyGroup", + "private_attribute_id": "442cc0e0-da72-4784-8036-fd5c1e643831", + "name": "sphere_r2mm.step", + "private_attribute_tag_key": "groupByFile", + "private_attribute_sub_components": [ + "sphere1_body00001" + ], + "mesh_exterior": true + }, + { + "private_attribute_entity_type_name": "GeometryBodyGroup", + "private_attribute_id": "7d2b9129-f5f4-4f92-b9b6-a554b420f162", + "name": "cube_2mm_center", + "private_attribute_tag_key": "groupByFile", + "private_attribute_sub_components": [ + "body00001" + ], + "mesh_exterior": false + } + ] + ], + "face_ids": [ + "body00001_face00001", + "body00001_face00002", + "body00001_face00003", + "body00001_face00004", + "body00001_face00005", + "body00001_face00006", + "sphere1_body00001_face00001", + "sphere1_body00001_face00002" + ], + "face_attribute_names": [ + "builtinName", + "groupByBodyId", + "name", + "faceId" + ], + "grouped_faces": [ + [ + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "359eeb96-86b5-4952-ac23-192dfd3a40ac", + "name": "No Name", + "private_attribute_tag_key": "builtinName", + "private_attribute_sub_components": [ + "body00001_face00001", + "body00001_face00002", + "body00001_face00003", + "body00001_face00004", + "body00001_face00005", + "body00001_face00006" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + 11.0, + -1.0, + -1.0 + ], + [ + 13.0, + 1.0, + 1.0 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "e8473b81-9840-441d-a45d-534320f3b330", + "name": "No Name", + "private_attribute_tag_key": "builtinName", + "private_attribute_sub_components": [ + "sphere1_body00001_face00001", + "sphere1_body00001_face00002" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + -2.0, + -2.0, + -2.0 + ], + [ + 2.0, + 2.0, + 2.0 + ] + ] + } + } + ], + [ + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "3a150e35-76bc-4e47-84f1-238f9200d0ea", + "name": "sphere1_body00001", + "private_attribute_tag_key": "groupByBodyId", + "private_attribute_sub_components": [ + "sphere1_body00001_face00001", + "sphere1_body00001_face00002" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + -2.0, + -2.0, + -2.0 + ], + [ + 2.0, + 2.0, + 2.0 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "dc724906-9b8f-4e62-ac58-ec97e2a12c76", + "name": "cube_body", + "private_attribute_tag_key": "groupByBodyId", + "private_attribute_sub_components": [ + "body00001_face00001", + "body00001_face00002", + "body00001_face00003", + "body00001_face00004", + "body00001_face00005", + "body00001_face00006" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + 11.0, + -1.0, + -1.0 + ], + [ + 13.0, + 1.0, + 1.0 + ] + ] + } + } + ], + [ + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "084ec7b0-6ff1-4a3f-993d-06128efa49f8", + "name": "body00001_face00002", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "body00001_face00002" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + 11.0, + -1.0, + -1.0 + ], + [ + 13.0, + -1.0, + 1.0 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "238489c6-b158-44bb-b5b4-d6cc17a69b51", + "name": "sphere1_body00001_face00002", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "sphere1_body00001_face00002" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + -2.0, + -2.0, + -2.0 + ], + [ + 2.0, + 2.4492935982947064e-16, + 2.0 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "59eda58d-0dac-4f77-b4c4-2fcdf4361378", + "name": "body00001_face00003", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "body00001_face00003" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + 13.0, + -1.0, + -1.0 + ], + [ + 13.0, + 1.0, + 1.0 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "8277de84-8306-4547-9cb0-687d178b14b1", + "name": "body00001_face00005", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "body00001_face00005" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + 11.0, + -1.0, + -1.0 + ], + [ + 13.0, + 1.0, + -1.0 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "900a49b1-6d7b-44df-89f9-2cf48ac243f8", + "name": "body00001_face00001", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "body00001_face00001" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + 11.0, + -1.0, + -1.0 + ], + [ + 11.0, + 1.0, + 1.0 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "b4ca0f33-0d8f-4fc1-be3a-30aa7b2d22da", + "name": "body00001_face00004", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "body00001_face00004" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + 11.0, + 1.0, + -1.0 + ], + [ + 13.0, + 1.0, + 1.0 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "d4efa00f-dfdd-4461-b317-25fc6a775d4e", + "name": "body00001_face00006", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "body00001_face00006" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + 11.0, + -1.0, + 1.0 + ], + [ + 13.0, + 1.0, + 1.0 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "d5922440-36fc-4308-aac4-b040ee31d950", + "name": "sphere1_body00001_face00001", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "sphere1_body00001_face00001" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + -2.0, + -4.499279347985573e-32, + -2.0 + ], + [ + 2.0, + 2.0, + 2.0 + ] + ] + } + } + ], + [ + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "346806f8-3946-4a48-8616-56b538605814", + "name": "body00001_face00006", + "private_attribute_tag_key": "faceId", + "private_attribute_sub_components": [ + "body00001_face00006" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + 11.0, + -1.0, + 1.0 + ], + [ + 13.0, + 1.0, + 1.0 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "3d8cd3d8-4719-40db-9929-d683a504e670", + "name": "sphere1_body00001_face00001", + "private_attribute_tag_key": "faceId", + "private_attribute_sub_components": [ + "sphere1_body00001_face00001" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + -2.0, + -4.499279347985573e-32, + -2.0 + ], + [ + 2.0, + 2.0, + 2.0 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "3db752a1-2925-4fc6-89fe-100a490d141b", + "name": "body00001_face00003", + "private_attribute_tag_key": "faceId", + "private_attribute_sub_components": [ + "body00001_face00003" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + 13.0, + -1.0, + -1.0 + ], + [ + 13.0, + 1.0, + 1.0 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "a3c3b221-97e2-4368-ac13-addb3102b46c", + "name": "body00001_face00001", + "private_attribute_tag_key": "faceId", + "private_attribute_sub_components": [ + "body00001_face00001" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + 11.0, + -1.0, + -1.0 + ], + [ + 11.0, + 1.0, + 1.0 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "a932d870-d169-49aa-83e1-be9fbffcbe2d", + "name": "sphere1_body00001_face00002", + "private_attribute_tag_key": "faceId", + "private_attribute_sub_components": [ + "sphere1_body00001_face00002" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + -2.0, + -2.0, + -2.0 + ], + [ + 2.0, + 2.4492935982947064e-16, + 2.0 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "c236edf3-4d79-4fbe-908d-0420dfdf53cc", + "name": "body00001_face00005", + "private_attribute_tag_key": "faceId", + "private_attribute_sub_components": [ + "body00001_face00005" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + 11.0, + -1.0, + -1.0 + ], + [ + 13.0, + 1.0, + -1.0 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "c89e0cf2-777a-413f-8e59-48f387f564ee", + "name": "body00001_face00002", + "private_attribute_tag_key": "faceId", + "private_attribute_sub_components": [ + "body00001_face00002" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + 11.0, + -1.0, + -1.0 + ], + [ + 13.0, + -1.0, + 1.0 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "cb676b21-1ecd-4062-94f6-3eb5fb279c7e", + "name": "body00001_face00004", + "private_attribute_tag_key": "faceId", + "private_attribute_sub_components": [ + "body00001_face00004" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + 11.0, + 1.0, + -1.0 + ], + [ + 13.0, + 1.0, + 1.0 + ] + ] + } + } + ] + ], + "edge_ids": [ + "body00001_edge00001", + "body00001_edge00002", + "body00001_edge00003", + "body00001_edge00004", + "body00001_edge00005", + "body00001_edge00006", + "body00001_edge00007", + "body00001_edge00008", + "body00001_edge00009", + "body00001_edge00010", + "body00001_edge00011", + "body00001_edge00012", + "sphere1_body00001_edge00001", + "sphere1_body00001_edge00003" + ], + "edge_attribute_names": [ + "name", + "edgeId" + ], + "grouped_edges": [ + [ + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "0dcf993b-6ac4-4089-b4d6-2fd2d3a94d29", + "name": "body00001_edge00005", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "body00001_edge00005" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "1442e021-6026-4e98-a0a2-d45f14e017a5", + "name": "body00001_edge00009", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "body00001_edge00009" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "2e7e546a-753e-4b80-9d46-281f13070365", + "name": "body00001_edge00006", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "body00001_edge00006" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "48869a6b-c99e-46fd-a816-2833aa16283c", + "name": "sphere1_body00001_edge00003", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "sphere1_body00001_edge00003" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "596a214f-3b47-448e-b18e-7e7b2999a845", + "name": "body00001_edge00007", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "body00001_edge00007" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "67bf3b4f-b319-4cd2-a7b6-9c81281a7d2d", + "name": "body00001_edge00002", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "body00001_edge00002" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "6e06acde-5ff1-475d-a327-ea330f44b267", + "name": "body00001_edge00011", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "body00001_edge00011" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "75c1c7be-cbee-430e-9b0f-cf3fbf01ac61", + "name": "body00001_edge00008", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "body00001_edge00008" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "791f24ba-efdd-4912-aa3c-66e5d6b667f9", + "name": "body00001_edge00012", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "body00001_edge00012" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "99d61ea6-9f7e-4236-87c0-c6101dfb0ee0", + "name": "body00001_edge00001", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "body00001_edge00001" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "9cbefe8c-82e0-486d-b805-6ee5ecd49c21", + "name": "sphere1_body00001_edge00001", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "sphere1_body00001_edge00001" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "a27906f8-10ff-4820-a32b-41fa6c6cfa3c", + "name": "body00001_edge00003", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "body00001_edge00003" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "b60b9fa0-e08c-44f4-9869-0b80a92c3c7e", + "name": "body00001_edge00010", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "body00001_edge00010" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "be4f9f1f-e938-492a-aa73-d90b089619f8", + "name": "body00001_edge00004", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "body00001_edge00004" + ] + } + ], + [ + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "05742309-f8f3-439b-a3d1-6a60570a35ed", + "name": "body00001_edge00001", + "private_attribute_tag_key": "edgeId", + "private_attribute_sub_components": [ + "body00001_edge00001" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "177aac2c-d04b-4bd5-8e5f-758b472bf985", + "name": "body00001_edge00010", + "private_attribute_tag_key": "edgeId", + "private_attribute_sub_components": [ + "body00001_edge00010" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "2fe046d7-7d58-4b37-b4f5-b391335a520a", + "name": "body00001_edge00006", + "private_attribute_tag_key": "edgeId", + "private_attribute_sub_components": [ + "body00001_edge00006" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "4978befb-352c-4879-a1d5-c4dc0dc83a07", + "name": "body00001_edge00008", + "private_attribute_tag_key": "edgeId", + "private_attribute_sub_components": [ + "body00001_edge00008" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "5297031f-260c-4ff3-ba06-3e08018a51a4", + "name": "body00001_edge00002", + "private_attribute_tag_key": "edgeId", + "private_attribute_sub_components": [ + "body00001_edge00002" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "599a9e71-3d9c-4ae6-962b-6bb34e8ee961", + "name": "body00001_edge00011", + "private_attribute_tag_key": "edgeId", + "private_attribute_sub_components": [ + "body00001_edge00011" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "8e8d80ee-35ed-431a-bee6-460118ba75e9", + "name": "body00001_edge00004", + "private_attribute_tag_key": "edgeId", + "private_attribute_sub_components": [ + "body00001_edge00004" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "93549028-dbcd-4dd3-87f5-745d269f8243", + "name": "body00001_edge00007", + "private_attribute_tag_key": "edgeId", + "private_attribute_sub_components": [ + "body00001_edge00007" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "a6462fd9-871c-4ee7-b33c-f36e5788a965", + "name": "body00001_edge00003", + "private_attribute_tag_key": "edgeId", + "private_attribute_sub_components": [ + "body00001_edge00003" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "adb4961e-0e43-400f-97a6-ffb07c16e472", + "name": "sphere1_body00001_edge00003", + "private_attribute_tag_key": "edgeId", + "private_attribute_sub_components": [ + "sphere1_body00001_edge00003" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "dc6dea8e-240d-4eb8-9c32-d4a42a0d99b7", + "name": "sphere1_body00001_edge00001", + "private_attribute_tag_key": "edgeId", + "private_attribute_sub_components": [ + "sphere1_body00001_edge00001" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "e3c65004-eec0-469b-9dfe-bbd62705e412", + "name": "body00001_edge00012", + "private_attribute_tag_key": "edgeId", + "private_attribute_sub_components": [ + "body00001_edge00012" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "e5e5b6e3-2835-4c1b-a0ce-056617827745", + "name": "body00001_edge00005", + "private_attribute_tag_key": "edgeId", + "private_attribute_sub_components": [ + "body00001_edge00005" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "f0839250-331b-4d76-8409-824679adb5f5", + "name": "body00001_edge00009", + "private_attribute_tag_key": "edgeId", + "private_attribute_sub_components": [ + "body00001_edge00009" + ] + } + ] + ], + "body_group_tag": "groupByFile", + "face_group_tag": "groupByBodyId", + "edge_group_tag": "name", + "global_bounding_box": [ + [ + -2.0, + -2.0, + -2.0 + ], + [ + 13.0, + 2.0, + 2.0 + ] + ], + "default_geometry_accuracy": { + "value": 0.0001, + "units": "m" + } +} \ No newline at end of file diff --git a/tests/simulation/service/data/result_merged_geometry_entity_info2.json b/tests/simulation/service/data/result_merged_geometry_entity_info2.json new file mode 100644 index 000000000..d2aa21a18 --- /dev/null +++ b/tests/simulation/service/data/result_merged_geometry_entity_info2.json @@ -0,0 +1,1051 @@ +{ + "draft_entities": [], + "ghost_entities": [ + { + "private_attribute_entity_type_name": "GhostSphere", + "private_attribute_id": "farfield", + "name": "farfield", + "center": [ + 0, + 0, + 0 + ], + "max_radius": 100.0 + }, + { + "private_attribute_entity_type_name": "GhostCircularPlane", + "private_attribute_id": "symmetric-1", + "name": "symmetric-1", + "center": [ + 12.0, + -1.0, + 0.0 + ], + "max_radius": 100.0, + "normal_axis": [ + 0, + -1, + 0 + ] + }, + { + "private_attribute_entity_type_name": "GhostCircularPlane", + "private_attribute_id": "symmetric-2", + "name": "symmetric-2", + "center": [ + 12.0, + 1.0, + 0.0 + ], + "max_radius": 100.0, + "normal_axis": [ + 0, + 1, + 0 + ] + }, + { + "private_attribute_entity_type_name": "GhostCircularPlane", + "private_attribute_id": "symmetric", + "name": "symmetric", + "center": [ + 12.0, + 0, + 0.0 + ], + "max_radius": 100.0, + "normal_axis": [ + 0, + 1, + 0 + ] + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelInlet", + "name": "windTunnelInlet", + "used_by": [ + "all" + ] + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelOutlet", + "name": "windTunnelOutlet", + "used_by": [ + "all" + ] + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelCeiling", + "name": "windTunnelCeiling", + "used_by": [ + "all" + ] + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelFloor", + "name": "windTunnelFloor", + "used_by": [ + "all" + ] + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelLeft", + "name": "windTunnelLeft", + "used_by": [ + "all" + ] + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelRight", + "name": "windTunnelRight", + "used_by": [ + "all" + ] + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelFrictionPatch", + "name": "windTunnelFrictionPatch", + "used_by": [ + "StaticFloor" + ] + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelCentralBelt", + "name": "windTunnelCentralBelt", + "used_by": [ + "CentralBelt", + "WheelBelts" + ] + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelFrontWheelBelt", + "name": "windTunnelFrontWheelBelt", + "used_by": [ + "WheelBelts" + ] + }, + { + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelRearWheelBelt", + "name": "windTunnelRearWheelBelt", + "used_by": [ + "WheelBelts" + ] + } + ], + "type_name": "GeometryEntityInfo", + "bodies_face_edge_ids": { + "body00001": { + "face_ids": [ + "body00001_face00001", + "body00001_face00002", + "body00001_face00003", + "body00001_face00004", + "body00001_face00005", + "body00001_face00006" + ], + "edge_ids": [ + "body00001_edge00001", + "body00001_edge00002", + "body00001_edge00003", + "body00001_edge00004", + "body00001_edge00005", + "body00001_edge00006", + "body00001_edge00007", + "body00001_edge00008", + "body00001_edge00009", + "body00001_edge00010", + "body00001_edge00011", + "body00001_edge00012" + ] + }, + "sphere2_body00001": { + "face_ids": [ + "sphere2_body00001_face00001", + "sphere2_body00001_face00002" + ], + "edge_ids": [ + "sphere2_body00001_edge00001", + "sphere2_body00001_edge00003" + ] + } + }, + "body_ids": [ + "body00001", + "sphere2_body00001" + ], + "body_attribute_names": [ + "bodyId", + "groupByFile" + ], + "grouped_bodies": [ + [ + { + "private_attribute_entity_type_name": "GeometryBodyGroup", + "private_attribute_id": "b496086c-136d-4f01-998d-c9e1eea120ae", + "name": "body00001", + "private_attribute_tag_key": "bodyId", + "private_attribute_sub_components": [ + "body00001" + ], + "mesh_exterior": true + }, + { + "private_attribute_entity_type_name": "GeometryBodyGroup", + "private_attribute_id": "fad554fd-2de9-4064-a5b3-8488a235812a", + "name": "sphere2_body00001", + "private_attribute_tag_key": "bodyId", + "private_attribute_sub_components": [ + "sphere2_body00001" + ], + "mesh_exterior": true + } + ], + [ + { + "private_attribute_entity_type_name": "GeometryBodyGroup", + "private_attribute_id": "0fb73bec-f430-4efd-8dc5-4b9955df849e", + "name": "sphere_r2mm_center_5_0_0.step", + "private_attribute_tag_key": "groupByFile", + "private_attribute_sub_components": [ + "sphere2_body00001" + ], + "mesh_exterior": true + }, + { + "private_attribute_entity_type_name": "GeometryBodyGroup", + "private_attribute_id": "7d2b9129-f5f4-4f92-b9b6-a554b420f162", + "name": "cube_2mm_center", + "private_attribute_tag_key": "groupByFile", + "private_attribute_sub_components": [ + "body00001" + ], + "mesh_exterior": false + } + ] + ], + "face_ids": [ + "body00001_face00001", + "body00001_face00002", + "body00001_face00003", + "body00001_face00004", + "body00001_face00005", + "body00001_face00006", + "sphere2_body00001_face00001", + "sphere2_body00001_face00002" + ], + "face_attribute_names": [ + "builtinName", + "groupByBodyId", + "name", + "faceId" + ], + "grouped_faces": [ + [ + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "032d8c9b-9873-4557-b7c6-cde69f6507f7", + "name": "No Name", + "private_attribute_tag_key": "builtinName", + "private_attribute_sub_components": [ + "sphere2_body00001_face00001", + "sphere2_body00001_face00002" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + 3.0, + -2.0, + -2.0 + ], + [ + 7.0, + 2.0, + 2.0 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "359eeb96-86b5-4952-ac23-192dfd3a40ac", + "name": "No Name", + "private_attribute_tag_key": "builtinName", + "private_attribute_sub_components": [ + "body00001_face00001", + "body00001_face00002", + "body00001_face00003", + "body00001_face00004", + "body00001_face00005", + "body00001_face00006" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + 11.0, + -1.0, + -1.0 + ], + [ + 13.0, + 1.0, + 1.0 + ] + ] + } + } + ], + [ + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "6ce3c95d-1bd2-434c-be8b-d88151a099a0", + "name": "sphere2_body00001", + "private_attribute_tag_key": "groupByBodyId", + "private_attribute_sub_components": [ + "sphere2_body00001_face00001", + "sphere2_body00001_face00002" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + 3.0, + -2.0, + -2.0 + ], + [ + 7.0, + 2.0, + 2.0 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "dc724906-9b8f-4e62-ac58-ec97e2a12c76", + "name": "cube_body", + "private_attribute_tag_key": "groupByBodyId", + "private_attribute_sub_components": [ + "body00001_face00001", + "body00001_face00002", + "body00001_face00003", + "body00001_face00004", + "body00001_face00005", + "body00001_face00006" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + 11.0, + -1.0, + -1.0 + ], + [ + 13.0, + 1.0, + 1.0 + ] + ] + } + } + ], + [ + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "084ec7b0-6ff1-4a3f-993d-06128efa49f8", + "name": "body00001_face00002", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "body00001_face00002" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + 11.0, + -1.0, + -1.0 + ], + [ + 13.0, + -1.0, + 1.0 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "59eda58d-0dac-4f77-b4c4-2fcdf4361378", + "name": "body00001_face00003", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "body00001_face00003" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + 13.0, + -1.0, + -1.0 + ], + [ + 13.0, + 1.0, + 1.0 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "8277de84-8306-4547-9cb0-687d178b14b1", + "name": "body00001_face00005", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "body00001_face00005" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + 11.0, + -1.0, + -1.0 + ], + [ + 13.0, + 1.0, + -1.0 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "900a49b1-6d7b-44df-89f9-2cf48ac243f8", + "name": "body00001_face00001", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "body00001_face00001" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + 11.0, + -1.0, + -1.0 + ], + [ + 11.0, + 1.0, + 1.0 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "ad2779dd-c479-4b20-b753-054a988f6fac", + "name": "sphere2_body00001_face00002", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "sphere2_body00001_face00002" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + 3.0, + -2.0, + -2.0 + ], + [ + 7.0, + 2.4492935982947064e-16, + 2.0 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "b4ca0f33-0d8f-4fc1-be3a-30aa7b2d22da", + "name": "body00001_face00004", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "body00001_face00004" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + 11.0, + 1.0, + -1.0 + ], + [ + 13.0, + 1.0, + 1.0 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "d4efa00f-dfdd-4461-b317-25fc6a775d4e", + "name": "body00001_face00006", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "body00001_face00006" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + 11.0, + -1.0, + 1.0 + ], + [ + 13.0, + 1.0, + 1.0 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "ee6f39e2-15ed-4d3b-a77a-8383831bad15", + "name": "sphere2_body00001_face00001", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "sphere2_body00001_face00001" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + 3.0, + -4.499279347985573e-32, + -2.0 + ], + [ + 7.0, + 2.0, + 2.0 + ] + ] + } + } + ], + [ + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "33dc2ccd-4b46-4444-b094-76e5fdd2249f", + "name": "sphere2_body00001_face00001", + "private_attribute_tag_key": "faceId", + "private_attribute_sub_components": [ + "sphere2_body00001_face00001" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + 3.0, + -4.499279347985573e-32, + -2.0 + ], + [ + 7.0, + 2.0, + 2.0 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "346806f8-3946-4a48-8616-56b538605814", + "name": "body00001_face00006", + "private_attribute_tag_key": "faceId", + "private_attribute_sub_components": [ + "body00001_face00006" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + 11.0, + -1.0, + 1.0 + ], + [ + 13.0, + 1.0, + 1.0 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "3db752a1-2925-4fc6-89fe-100a490d141b", + "name": "body00001_face00003", + "private_attribute_tag_key": "faceId", + "private_attribute_sub_components": [ + "body00001_face00003" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + 13.0, + -1.0, + -1.0 + ], + [ + 13.0, + 1.0, + 1.0 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "8c3e316d-fbd5-45ef-a79f-e60f764abcca", + "name": "sphere2_body00001_face00002", + "private_attribute_tag_key": "faceId", + "private_attribute_sub_components": [ + "sphere2_body00001_face00002" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + 3.0, + -2.0, + -2.0 + ], + [ + 7.0, + 2.4492935982947064e-16, + 2.0 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "a3c3b221-97e2-4368-ac13-addb3102b46c", + "name": "body00001_face00001", + "private_attribute_tag_key": "faceId", + "private_attribute_sub_components": [ + "body00001_face00001" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + 11.0, + -1.0, + -1.0 + ], + [ + 11.0, + 1.0, + 1.0 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "c236edf3-4d79-4fbe-908d-0420dfdf53cc", + "name": "body00001_face00005", + "private_attribute_tag_key": "faceId", + "private_attribute_sub_components": [ + "body00001_face00005" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + 11.0, + -1.0, + -1.0 + ], + [ + 13.0, + 1.0, + -1.0 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "c89e0cf2-777a-413f-8e59-48f387f564ee", + "name": "body00001_face00002", + "private_attribute_tag_key": "faceId", + "private_attribute_sub_components": [ + "body00001_face00002" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + 11.0, + -1.0, + -1.0 + ], + [ + 13.0, + -1.0, + 1.0 + ] + ] + } + }, + { + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "cb676b21-1ecd-4062-94f6-3eb5fb279c7e", + "name": "body00001_face00004", + "private_attribute_tag_key": "faceId", + "private_attribute_sub_components": [ + "body00001_face00004" + ], + "private_attributes": { + "type_name": "SurfacePrivateAttributes", + "bounding_box": [ + [ + 11.0, + 1.0, + -1.0 + ], + [ + 13.0, + 1.0, + 1.0 + ] + ] + } + } + ] + ], + "edge_ids": [ + "body00001_edge00001", + "body00001_edge00002", + "body00001_edge00003", + "body00001_edge00004", + "body00001_edge00005", + "body00001_edge00006", + "body00001_edge00007", + "body00001_edge00008", + "body00001_edge00009", + "body00001_edge00010", + "body00001_edge00011", + "body00001_edge00012", + "sphere2_body00001_edge00001", + "sphere2_body00001_edge00003" + ], + "edge_attribute_names": [ + "name", + "edgeId" + ], + "grouped_edges": [ + [ + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "0dcf993b-6ac4-4089-b4d6-2fd2d3a94d29", + "name": "body00001_edge00005", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "body00001_edge00005" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "1442e021-6026-4e98-a0a2-d45f14e017a5", + "name": "body00001_edge00009", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "body00001_edge00009" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "2e7e546a-753e-4b80-9d46-281f13070365", + "name": "body00001_edge00006", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "body00001_edge00006" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "4b30a96b-b8c1-4de2-9a5f-f447bb42f4a9", + "name": "sphere2_body00001_edge00003", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "sphere2_body00001_edge00003" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "596a214f-3b47-448e-b18e-7e7b2999a845", + "name": "body00001_edge00007", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "body00001_edge00007" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "67bf3b4f-b319-4cd2-a7b6-9c81281a7d2d", + "name": "body00001_edge00002", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "body00001_edge00002" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "6e06acde-5ff1-475d-a327-ea330f44b267", + "name": "body00001_edge00011", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "body00001_edge00011" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "75c1c7be-cbee-430e-9b0f-cf3fbf01ac61", + "name": "body00001_edge00008", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "body00001_edge00008" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "791f24ba-efdd-4912-aa3c-66e5d6b667f9", + "name": "body00001_edge00012", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "body00001_edge00012" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "96b21c6a-21b5-4387-acd2-83ae71f17413", + "name": "sphere2_body00001_edge00001", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "sphere2_body00001_edge00001" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "99d61ea6-9f7e-4236-87c0-c6101dfb0ee0", + "name": "body00001_edge00001", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "body00001_edge00001" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "a27906f8-10ff-4820-a32b-41fa6c6cfa3c", + "name": "body00001_edge00003", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "body00001_edge00003" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "b60b9fa0-e08c-44f4-9869-0b80a92c3c7e", + "name": "body00001_edge00010", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "body00001_edge00010" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "be4f9f1f-e938-492a-aa73-d90b089619f8", + "name": "body00001_edge00004", + "private_attribute_tag_key": "name", + "private_attribute_sub_components": [ + "body00001_edge00004" + ] + } + ], + [ + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "05742309-f8f3-439b-a3d1-6a60570a35ed", + "name": "body00001_edge00001", + "private_attribute_tag_key": "edgeId", + "private_attribute_sub_components": [ + "body00001_edge00001" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "177aac2c-d04b-4bd5-8e5f-758b472bf985", + "name": "body00001_edge00010", + "private_attribute_tag_key": "edgeId", + "private_attribute_sub_components": [ + "body00001_edge00010" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "2fe046d7-7d58-4b37-b4f5-b391335a520a", + "name": "body00001_edge00006", + "private_attribute_tag_key": "edgeId", + "private_attribute_sub_components": [ + "body00001_edge00006" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "3193fbf3-6379-42de-ac63-5ad9401dcce2", + "name": "sphere2_body00001_edge00001", + "private_attribute_tag_key": "edgeId", + "private_attribute_sub_components": [ + "sphere2_body00001_edge00001" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "4978befb-352c-4879-a1d5-c4dc0dc83a07", + "name": "body00001_edge00008", + "private_attribute_tag_key": "edgeId", + "private_attribute_sub_components": [ + "body00001_edge00008" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "5297031f-260c-4ff3-ba06-3e08018a51a4", + "name": "body00001_edge00002", + "private_attribute_tag_key": "edgeId", + "private_attribute_sub_components": [ + "body00001_edge00002" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "599a9e71-3d9c-4ae6-962b-6bb34e8ee961", + "name": "body00001_edge00011", + "private_attribute_tag_key": "edgeId", + "private_attribute_sub_components": [ + "body00001_edge00011" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "8e8d80ee-35ed-431a-bee6-460118ba75e9", + "name": "body00001_edge00004", + "private_attribute_tag_key": "edgeId", + "private_attribute_sub_components": [ + "body00001_edge00004" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "93549028-dbcd-4dd3-87f5-745d269f8243", + "name": "body00001_edge00007", + "private_attribute_tag_key": "edgeId", + "private_attribute_sub_components": [ + "body00001_edge00007" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "a6462fd9-871c-4ee7-b33c-f36e5788a965", + "name": "body00001_edge00003", + "private_attribute_tag_key": "edgeId", + "private_attribute_sub_components": [ + "body00001_edge00003" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "ac831004-5a5e-4b8b-b66a-475b78308ab9", + "name": "sphere2_body00001_edge00003", + "private_attribute_tag_key": "edgeId", + "private_attribute_sub_components": [ + "sphere2_body00001_edge00003" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "e3c65004-eec0-469b-9dfe-bbd62705e412", + "name": "body00001_edge00012", + "private_attribute_tag_key": "edgeId", + "private_attribute_sub_components": [ + "body00001_edge00012" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "e5e5b6e3-2835-4c1b-a0ce-056617827745", + "name": "body00001_edge00005", + "private_attribute_tag_key": "edgeId", + "private_attribute_sub_components": [ + "body00001_edge00005" + ] + }, + { + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "f0839250-331b-4d76-8409-824679adb5f5", + "name": "body00001_edge00009", + "private_attribute_tag_key": "edgeId", + "private_attribute_sub_components": [ + "body00001_edge00009" + ] + } + ] + ], + "body_group_tag": "groupByFile", + "face_group_tag": "groupByBodyId", + "edge_group_tag": "name", + "global_bounding_box": [ + [ + 3.0, + -2.0, + -2.0 + ], + [ + 13.0, + 2.0, + 2.0 + ] + ], + "default_geometry_accuracy": { + "value": 0.0001, + "units": "m" + } +} \ No newline at end of file diff --git a/tests/simulation/service/data/root_geometry_cube_simulation.json b/tests/simulation/service/data/root_geometry_cube_simulation.json new file mode 100644 index 000000000..8c94baceb --- /dev/null +++ b/tests/simulation/service/data/root_geometry_cube_simulation.json @@ -0,0 +1,1156 @@ +{ + "hash": "4da4256d0f07d14933d36243edede09ade1cf4894b99c3eeb7145146f6b32752", + "meshing": { + "defaults": { + "boundary_layer_growth_rate": 1.2, + "curvature_resolution_angle": { + "units": "degree", + "value": 12.0 + }, + "planar_face_tolerance": 1e-06, + "preserve_thin_geometry": false, + "remove_non_manifold_faces": false, + "resolve_face_boundaries": false, + "sealing_size": { + "units": "m", + "value": 0.0 + }, + "sliding_interface_tolerance": 0.01, + "surface_edge_growth_rate": 1.2, + "surface_max_adaptation_iterations": 50, + "surface_max_aspect_ratio": 10.0 + }, + "outputs": [], + "refinement_factor": 1.0, + "refinements": [], + "type_name": "MeshingParams", + "volume_zones": [ + { + "method": "auto", + "name": "Farfield", + "relative_size": 50.0, + "type": "AutomatedFarfield" + } + ] + }, + "models": [ + { + "entities": { + "stored_entities": [ + { + "name": "*", + "private_attribute_entity_type_name": "Surface", + "private_attribute_sub_components": [] + } + ] + }, + "heat_spec": { + "type_name": "HeatFlux", + "value": { + "units": "W/m**2", + "value": 0.0 + } + }, + "name": "Wall", + "private_attribute_id": "b7990356-c29b-44ee-aa5e-a163a18848d0", + "roughness_height": { + "units": "m", + "value": 0.0 + }, + "type": "Wall", + "use_wall_function": false + }, + { + "entities": { + "stored_entities": [ + { + "name": "farfield", + "private_attribute_entity_type_name": "GhostSurface", + "private_attribute_id": "farfield" + } + ] + }, + "name": "Freestream", + "private_attribute_id": "a00c9458-066e-4207-94c7-56474e62fe60", + "type": "Freestream" + }, + { + "initial_condition": { + "p": "p", + "rho": "rho", + "type_name": "NavierStokesInitialCondition", + "u": "u", + "v": "v", + "w": "w" + }, + "interface_interpolation_tolerance": 0.2, + "material": { + "dynamic_viscosity": { + "effective_temperature": { + "units": "K", + "value": 110.4 + }, + "reference_temperature": { + "units": "K", + "value": 273.15 + }, + "reference_viscosity": { + "units": "Pa*s", + "value": 1.716e-05 + } + }, + "name": "air", + "type": "air" + }, + "navier_stokes_solver": { + "CFL_multiplier": 1.0, + "absolute_tolerance": 1e-10, + "equation_evaluation_frequency": 1, + "kappa_MUSCL": -1.0, + "limit_pressure_density": false, + "limit_velocity": false, + "linear_solver": { + "max_iterations": 30 + }, + "low_mach_preconditioner": false, + "max_force_jac_update_physical_steps": 0, + "numerical_dissipation_factor": 1.0, + "order_of_accuracy": 2, + "relative_tolerance": 0.0, + "type_name": "Compressible", + "update_jacobian_frequency": 4 + }, + "private_attribute_id": "__default_fluid", + "transition_model_solver": { + "type_name": "None" + }, + "turbulence_model_solver": { + "CFL_multiplier": 2.0, + "absolute_tolerance": 1e-08, + "equation_evaluation_frequency": 4, + "linear_solver": { + "max_iterations": 20 + }, + "low_reynolds_correction": false, + "max_force_jac_update_physical_steps": 0, + "modeling_constants": { + "C_DES": 0.72, + "C_cb1": 0.1355, + "C_cb2": 0.622, + "C_d": 8.0, + "C_min_rd": 10.0, + "C_sigma": 0.6666666666666666, + "C_t3": 1.2, + "C_t4": 0.5, + "C_v1": 7.1, + "C_vonKarman": 0.41, + "C_w2": 0.3, + "C_w4": 0.21, + "C_w5": 1.5, + "type_name": "SpalartAllmarasConsts" + }, + "order_of_accuracy": 2, + "quadratic_constitutive_relation": false, + "reconstruction_gradient_limiter": 0.5, + "relative_tolerance": 0.0, + "rotation_correction": false, + "type_name": "SpalartAllmaras", + "update_jacobian_frequency": 4 + }, + "type": "Fluid" + } + ], + "operating_condition": { + "alpha": { + "units": "degree", + "value": 0.0 + }, + "beta": { + "units": "degree", + "value": 0.0 + }, + "private_attribute_constructor": "default", + "private_attribute_input_cache": { + "alpha": { + "units": "degree", + "value": 0.0 + }, + "beta": { + "units": "degree", + "value": 0.0 + }, + "thermal_state": { + "density": { + "units": "kg/m**3", + "value": 1.225 + }, + "material": { + "dynamic_viscosity": { + "effective_temperature": { + "units": "K", + "value": 110.4 + }, + "reference_temperature": { + "units": "K", + "value": 273.15 + }, + "reference_viscosity": { + "units": "Pa*s", + "value": 1.716e-05 + } + }, + "name": "air", + "type": "air" + }, + "private_attribute_constructor": "default", + "private_attribute_input_cache": {}, + "temperature": { + "units": "K", + "value": 288.15 + }, + "type_name": "ThermalState" + } + }, + "thermal_state": { + "density": { + "units": "kg/m**3", + "value": 1.225 + }, + "material": { + "dynamic_viscosity": { + "effective_temperature": { + "units": "K", + "value": 110.4 + }, + "reference_temperature": { + "units": "K", + "value": 273.15 + }, + "reference_viscosity": { + "units": "Pa*s", + "value": 1.716e-05 + } + }, + "name": "air", + "type": "air" + }, + "private_attribute_constructor": "default", + "private_attribute_input_cache": {}, + "temperature": { + "units": "K", + "value": 288.15 + }, + "type_name": "ThermalState" + }, + "type_name": "AerospaceCondition" + }, + "outputs": [ + { + "entities": { + "stored_entities": [ + { + "name": "*", + "private_attribute_entity_type_name": "Surface", + "private_attribute_sub_components": [] + } + ] + }, + "frequency": -1, + "frequency_offset": 0, + "name": "Surface output", + "output_fields": { + "items": [ + "Cp", + "yPlus", + "Cf", + "CfVec" + ] + }, + "output_format": "paraview", + "output_type": "SurfaceOutput", + "private_attribute_id": "39c6e6c8-9756-401f-bb12-8d037d829908", + "write_single_file": false + } + ], + "private_attribute_asset_cache": { + "project_entity_info": { + "bodies_face_edge_ids": { + "body00001": { + "edge_ids": [ + "body00001_edge00001", + "body00001_edge00002", + "body00001_edge00003", + "body00001_edge00004", + "body00001_edge00005", + "body00001_edge00006", + "body00001_edge00007", + "body00001_edge00008", + "body00001_edge00009", + "body00001_edge00010", + "body00001_edge00011", + "body00001_edge00012" + ], + "face_ids": [ + "body00001_face00001", + "body00001_face00002", + "body00001_face00003", + "body00001_face00004", + "body00001_face00005", + "body00001_face00006" + ] + } + }, + "body_attribute_names": [ + "bodyId", + "groupByFile" + ], + "body_group_tag": "groupByFile", + "body_ids": [ + "body00001" + ], + "default_geometry_accuracy": { + "units": "m", + "value": 0.0001 + }, + "draft_entities": [], + "edge_attribute_names": [ + "name", + "edgeId" + ], + "edge_group_tag": "name", + "edge_ids": [ + "body00001_edge00001", + "body00001_edge00002", + "body00001_edge00003", + "body00001_edge00004", + "body00001_edge00005", + "body00001_edge00006", + "body00001_edge00007", + "body00001_edge00008", + "body00001_edge00009", + "body00001_edge00010", + "body00001_edge00011", + "body00001_edge00012" + ], + "face_attribute_names": [ + "builtinName", + "groupByBodyId", + "name", + "faceId" + ], + "face_group_tag": "groupByBodyId", + "face_ids": [ + "body00001_face00001", + "body00001_face00002", + "body00001_face00003", + "body00001_face00004", + "body00001_face00005", + "body00001_face00006" + ], + "ghost_entities": [ + { + "center": [ + 0, + 0, + 0 + ], + "max_radius": 100.0, + "name": "farfield", + "private_attribute_entity_type_name": "GhostSphere", + "private_attribute_id": "farfield" + }, + { + "center": [ + 12.0, + -1.0, + 0.0 + ], + "max_radius": 100.0, + "name": "symmetric-1", + "normal_axis": [ + 0, + -1, + 0 + ], + "private_attribute_entity_type_name": "GhostCircularPlane", + "private_attribute_id": "symmetric-1" + }, + { + "center": [ + 12.0, + 1.0, + 0.0 + ], + "max_radius": 100.0, + "name": "symmetric-2", + "normal_axis": [ + 0, + 1, + 0 + ], + "private_attribute_entity_type_name": "GhostCircularPlane", + "private_attribute_id": "symmetric-2" + }, + { + "center": [ + 12.0, + 0, + 0.0 + ], + "max_radius": 100.0, + "name": "symmetric", + "normal_axis": [ + 0, + 1, + 0 + ], + "private_attribute_entity_type_name": "GhostCircularPlane", + "private_attribute_id": "symmetric" + }, + { + "name": "windTunnelInlet", + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelInlet", + "used_by": [ + "all" + ] + }, + { + "name": "windTunnelOutlet", + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelOutlet", + "used_by": [ + "all" + ] + }, + { + "name": "windTunnelCeiling", + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelCeiling", + "used_by": [ + "all" + ] + }, + { + "name": "windTunnelFloor", + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelFloor", + "used_by": [ + "all" + ] + }, + { + "name": "windTunnelLeft", + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelLeft", + "used_by": [ + "all" + ] + }, + { + "name": "windTunnelRight", + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelRight", + "used_by": [ + "all" + ] + }, + { + "name": "windTunnelFrictionPatch", + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelFrictionPatch", + "used_by": [ + "StaticFloor" + ] + }, + { + "name": "windTunnelCentralBelt", + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelCentralBelt", + "used_by": [ + "CentralBelt", + "WheelBelts" + ] + }, + { + "name": "windTunnelFrontWheelBelt", + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelFrontWheelBelt", + "used_by": [ + "WheelBelts" + ] + }, + { + "name": "windTunnelRearWheelBelt", + "private_attribute_entity_type_name": "WindTunnelGhostSurface", + "private_attribute_id": "windTunnelRearWheelBelt", + "used_by": [ + "WheelBelts" + ] + } + ], + "global_bounding_box": [ + [ + 11.0, + -1.0, + -1.0 + ], + [ + 13.0, + 1.0, + 1.0 + ] + ], + "grouped_bodies": [ + [ + { + "mesh_exterior": true, + "name": "body00001", + "private_attribute_entity_type_name": "GeometryBodyGroup", + "private_attribute_id": "b496086c-136d-4f01-998d-c9e1eea120ae", + "private_attribute_sub_components": [ + "body00001" + ], + "private_attribute_tag_key": "bodyId" + } + ], + [ + { + "mesh_exterior": false, + "name": "cube_2mm_center", + "private_attribute_entity_type_name": "GeometryBodyGroup", + "private_attribute_id": "7d2b9129-f5f4-4f92-b9b6-a554b420f162", + "private_attribute_sub_components": [ + "body00001" + ], + "private_attribute_tag_key": "groupByFile" + } + ] + ], + "grouped_edges": [ + [ + { + "name": "body00001_edge00001", + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "99d61ea6-9f7e-4236-87c0-c6101dfb0ee0", + "private_attribute_sub_components": [ + "body00001_edge00001" + ], + "private_attribute_tag_key": "name" + }, + { + "name": "body00001_edge00002", + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "67bf3b4f-b319-4cd2-a7b6-9c81281a7d2d", + "private_attribute_sub_components": [ + "body00001_edge00002" + ], + "private_attribute_tag_key": "name" + }, + { + "name": "body00001_edge00003", + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "a27906f8-10ff-4820-a32b-41fa6c6cfa3c", + "private_attribute_sub_components": [ + "body00001_edge00003" + ], + "private_attribute_tag_key": "name" + }, + { + "name": "body00001_edge00004", + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "be4f9f1f-e938-492a-aa73-d90b089619f8", + "private_attribute_sub_components": [ + "body00001_edge00004" + ], + "private_attribute_tag_key": "name" + }, + { + "name": "body00001_edge00005", + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "0dcf993b-6ac4-4089-b4d6-2fd2d3a94d29", + "private_attribute_sub_components": [ + "body00001_edge00005" + ], + "private_attribute_tag_key": "name" + }, + { + "name": "body00001_edge00006", + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "2e7e546a-753e-4b80-9d46-281f13070365", + "private_attribute_sub_components": [ + "body00001_edge00006" + ], + "private_attribute_tag_key": "name" + }, + { + "name": "body00001_edge00007", + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "596a214f-3b47-448e-b18e-7e7b2999a845", + "private_attribute_sub_components": [ + "body00001_edge00007" + ], + "private_attribute_tag_key": "name" + }, + { + "name": "body00001_edge00008", + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "75c1c7be-cbee-430e-9b0f-cf3fbf01ac61", + "private_attribute_sub_components": [ + "body00001_edge00008" + ], + "private_attribute_tag_key": "name" + }, + { + "name": "body00001_edge00009", + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "1442e021-6026-4e98-a0a2-d45f14e017a5", + "private_attribute_sub_components": [ + "body00001_edge00009" + ], + "private_attribute_tag_key": "name" + }, + { + "name": "body00001_edge00010", + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "b60b9fa0-e08c-44f4-9869-0b80a92c3c7e", + "private_attribute_sub_components": [ + "body00001_edge00010" + ], + "private_attribute_tag_key": "name" + }, + { + "name": "body00001_edge00011", + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "6e06acde-5ff1-475d-a327-ea330f44b267", + "private_attribute_sub_components": [ + "body00001_edge00011" + ], + "private_attribute_tag_key": "name" + }, + { + "name": "body00001_edge00012", + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "791f24ba-efdd-4912-aa3c-66e5d6b667f9", + "private_attribute_sub_components": [ + "body00001_edge00012" + ], + "private_attribute_tag_key": "name" + } + ], + [ + { + "name": "body00001_edge00001", + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "05742309-f8f3-439b-a3d1-6a60570a35ed", + "private_attribute_sub_components": [ + "body00001_edge00001" + ], + "private_attribute_tag_key": "edgeId" + }, + { + "name": "body00001_edge00002", + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "5297031f-260c-4ff3-ba06-3e08018a51a4", + "private_attribute_sub_components": [ + "body00001_edge00002" + ], + "private_attribute_tag_key": "edgeId" + }, + { + "name": "body00001_edge00003", + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "a6462fd9-871c-4ee7-b33c-f36e5788a965", + "private_attribute_sub_components": [ + "body00001_edge00003" + ], + "private_attribute_tag_key": "edgeId" + }, + { + "name": "body00001_edge00004", + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "8e8d80ee-35ed-431a-bee6-460118ba75e9", + "private_attribute_sub_components": [ + "body00001_edge00004" + ], + "private_attribute_tag_key": "edgeId" + }, + { + "name": "body00001_edge00005", + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "e5e5b6e3-2835-4c1b-a0ce-056617827745", + "private_attribute_sub_components": [ + "body00001_edge00005" + ], + "private_attribute_tag_key": "edgeId" + }, + { + "name": "body00001_edge00006", + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "2fe046d7-7d58-4b37-b4f5-b391335a520a", + "private_attribute_sub_components": [ + "body00001_edge00006" + ], + "private_attribute_tag_key": "edgeId" + }, + { + "name": "body00001_edge00007", + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "93549028-dbcd-4dd3-87f5-745d269f8243", + "private_attribute_sub_components": [ + "body00001_edge00007" + ], + "private_attribute_tag_key": "edgeId" + }, + { + "name": "body00001_edge00008", + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "4978befb-352c-4879-a1d5-c4dc0dc83a07", + "private_attribute_sub_components": [ + "body00001_edge00008" + ], + "private_attribute_tag_key": "edgeId" + }, + { + "name": "body00001_edge00009", + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "f0839250-331b-4d76-8409-824679adb5f5", + "private_attribute_sub_components": [ + "body00001_edge00009" + ], + "private_attribute_tag_key": "edgeId" + }, + { + "name": "body00001_edge00010", + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "177aac2c-d04b-4bd5-8e5f-758b472bf985", + "private_attribute_sub_components": [ + "body00001_edge00010" + ], + "private_attribute_tag_key": "edgeId" + }, + { + "name": "body00001_edge00011", + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "599a9e71-3d9c-4ae6-962b-6bb34e8ee961", + "private_attribute_sub_components": [ + "body00001_edge00011" + ], + "private_attribute_tag_key": "edgeId" + }, + { + "name": "body00001_edge00012", + "private_attribute_entity_type_name": "Edge", + "private_attribute_id": "e3c65004-eec0-469b-9dfe-bbd62705e412", + "private_attribute_sub_components": [ + "body00001_edge00012" + ], + "private_attribute_tag_key": "edgeId" + } + ] + ], + "grouped_faces": [ + [ + { + "name": "No Name", + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "359eeb96-86b5-4952-ac23-192dfd3a40ac", + "private_attribute_sub_components": [ + "body00001_face00001", + "body00001_face00002", + "body00001_face00003", + "body00001_face00004", + "body00001_face00005", + "body00001_face00006" + ], + "private_attribute_tag_key": "builtinName", + "private_attributes": { + "bounding_box": [ + [ + 11.0, + -1.0, + -1.0 + ], + [ + 13.0, + 1.0, + 1.0 + ] + ], + "type_name": "SurfacePrivateAttributes" + } + } + ], + [ + { + "name": "cube_body", + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "dc724906-9b8f-4e62-ac58-ec97e2a12c76", + "private_attribute_sub_components": [ + "body00001_face00001", + "body00001_face00002", + "body00001_face00003", + "body00001_face00004", + "body00001_face00005", + "body00001_face00006" + ], + "private_attribute_tag_key": "groupByBodyId", + "private_attributes": { + "bounding_box": [ + [ + 11.0, + -1.0, + -1.0 + ], + [ + 13.0, + 1.0, + 1.0 + ] + ], + "type_name": "SurfacePrivateAttributes" + } + } + ], + [ + { + "name": "body00001_face00001", + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "900a49b1-6d7b-44df-89f9-2cf48ac243f8", + "private_attribute_sub_components": [ + "body00001_face00001" + ], + "private_attribute_tag_key": "name", + "private_attributes": { + "bounding_box": [ + [ + 11.0, + -1.0, + -1.0 + ], + [ + 11.0, + 1.0, + 1.0 + ] + ], + "type_name": "SurfacePrivateAttributes" + } + }, + { + "name": "body00001_face00002", + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "084ec7b0-6ff1-4a3f-993d-06128efa49f8", + "private_attribute_sub_components": [ + "body00001_face00002" + ], + "private_attribute_tag_key": "name", + "private_attributes": { + "bounding_box": [ + [ + 11.0, + -1.0, + -1.0 + ], + [ + 13.0, + -1.0, + 1.0 + ] + ], + "type_name": "SurfacePrivateAttributes" + } + }, + { + "name": "body00001_face00003", + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "59eda58d-0dac-4f77-b4c4-2fcdf4361378", + "private_attribute_sub_components": [ + "body00001_face00003" + ], + "private_attribute_tag_key": "name", + "private_attributes": { + "bounding_box": [ + [ + 13.0, + -1.0, + -1.0 + ], + [ + 13.0, + 1.0, + 1.0 + ] + ], + "type_name": "SurfacePrivateAttributes" + } + }, + { + "name": "body00001_face00004", + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "b4ca0f33-0d8f-4fc1-be3a-30aa7b2d22da", + "private_attribute_sub_components": [ + "body00001_face00004" + ], + "private_attribute_tag_key": "name", + "private_attributes": { + "bounding_box": [ + [ + 11.0, + 1.0, + -1.0 + ], + [ + 13.0, + 1.0, + 1.0 + ] + ], + "type_name": "SurfacePrivateAttributes" + } + }, + { + "name": "body00001_face00005", + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "8277de84-8306-4547-9cb0-687d178b14b1", + "private_attribute_sub_components": [ + "body00001_face00005" + ], + "private_attribute_tag_key": "name", + "private_attributes": { + "bounding_box": [ + [ + 11.0, + -1.0, + -1.0 + ], + [ + 13.0, + 1.0, + -1.0 + ] + ], + "type_name": "SurfacePrivateAttributes" + } + }, + { + "name": "body00001_face00006", + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "d4efa00f-dfdd-4461-b317-25fc6a775d4e", + "private_attribute_sub_components": [ + "body00001_face00006" + ], + "private_attribute_tag_key": "name", + "private_attributes": { + "bounding_box": [ + [ + 11.0, + -1.0, + 1.0 + ], + [ + 13.0, + 1.0, + 1.0 + ] + ], + "type_name": "SurfacePrivateAttributes" + } + } + ], + [ + { + "name": "body00001_face00001", + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "a3c3b221-97e2-4368-ac13-addb3102b46c", + "private_attribute_sub_components": [ + "body00001_face00001" + ], + "private_attribute_tag_key": "faceId", + "private_attributes": { + "bounding_box": [ + [ + 11.0, + -1.0, + -1.0 + ], + [ + 11.0, + 1.0, + 1.0 + ] + ], + "type_name": "SurfacePrivateAttributes" + } + }, + { + "name": "body00001_face00002", + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "c89e0cf2-777a-413f-8e59-48f387f564ee", + "private_attribute_sub_components": [ + "body00001_face00002" + ], + "private_attribute_tag_key": "faceId", + "private_attributes": { + "bounding_box": [ + [ + 11.0, + -1.0, + -1.0 + ], + [ + 13.0, + -1.0, + 1.0 + ] + ], + "type_name": "SurfacePrivateAttributes" + } + }, + { + "name": "body00001_face00003", + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "3db752a1-2925-4fc6-89fe-100a490d141b", + "private_attribute_sub_components": [ + "body00001_face00003" + ], + "private_attribute_tag_key": "faceId", + "private_attributes": { + "bounding_box": [ + [ + 13.0, + -1.0, + -1.0 + ], + [ + 13.0, + 1.0, + 1.0 + ] + ], + "type_name": "SurfacePrivateAttributes" + } + }, + { + "name": "body00001_face00004", + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "cb676b21-1ecd-4062-94f6-3eb5fb279c7e", + "private_attribute_sub_components": [ + "body00001_face00004" + ], + "private_attribute_tag_key": "faceId", + "private_attributes": { + "bounding_box": [ + [ + 11.0, + 1.0, + -1.0 + ], + [ + 13.0, + 1.0, + 1.0 + ] + ], + "type_name": "SurfacePrivateAttributes" + } + }, + { + "name": "body00001_face00005", + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "c236edf3-4d79-4fbe-908d-0420dfdf53cc", + "private_attribute_sub_components": [ + "body00001_face00005" + ], + "private_attribute_tag_key": "faceId", + "private_attributes": { + "bounding_box": [ + [ + 11.0, + -1.0, + -1.0 + ], + [ + 13.0, + 1.0, + -1.0 + ] + ], + "type_name": "SurfacePrivateAttributes" + } + }, + { + "name": "body00001_face00006", + "private_attribute_entity_type_name": "Surface", + "private_attribute_id": "346806f8-3946-4a48-8616-56b538605814", + "private_attribute_sub_components": [ + "body00001_face00006" + ], + "private_attribute_tag_key": "faceId", + "private_attributes": { + "bounding_box": [ + [ + 11.0, + -1.0, + 1.0 + ], + [ + 13.0, + 1.0, + 1.0 + ] + ], + "type_name": "SurfacePrivateAttributes" + } + } + ] + ], + "type_name": "GeometryEntityInfo" + }, + "project_length_unit": { + "units": "m", + "value": 1.0 + }, + "use_geometry_AI": false, + "use_inhouse_mesher": false + }, + "reference_geometry": { + "area": { + "type_name": "number", + "units": "m**2", + "value": 1.0 + }, + "moment_center": { + "units": "m", + "value": [ + 0.0, + 0.0, + 0.0 + ] + }, + "moment_length": { + "units": "m", + "value": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + "time_stepping": { + "CFL": { + "convergence_limiting_factor": 0.25, + "max": 10000.0, + "max_relative_change": 1.0, + "min": 0.1, + "type": "adaptive" + }, + "max_steps": 2000, + "type_name": "Steady" + }, + "unit_system": { + "name": "SI" + }, + "user_defined_fields": [], + "version": "25.8.2b1" +} \ No newline at end of file diff --git a/tests/simulation/service/test_services_v2.py b/tests/simulation/service/test_services_v2.py index 7a16753f8..c7addc910 100644 --- a/tests/simulation/service/test_services_v2.py +++ b/tests/simulation/service/test_services_v2.py @@ -6,6 +6,7 @@ import flow360.component.simulation.units as u from flow360.component.simulation import services +from flow360.component.simulation.entity_info import GeometryEntityInfo from flow360.component.simulation.exposed_units import supported_units_by_front_end from flow360.component.simulation.framework.updater_utils import compare_values from flow360.component.simulation.unit_system import _PredefinedUnitSystem @@ -1349,3 +1350,184 @@ def test_get_default_report_config_json(): with open("ref/default_report_config.json", "r") as fp: ref_dict = json.load(fp) assert compare_values(report_config_dict, ref_dict, ignore_keys=["formatter"]) + + +def test_merge_geometry_entity_info(): + """ + Test the merge_geometry_entity_info function to ensure proper merging of geometry entity information. + + Test scenarios: + 1. Merge root geometry with one dependency geometry, should preserve mesh_exterior and name settings + of geometryBodyGroup in root geometry. + 2. Start from result of (1), replace the dependency with another dependency geometry, should check: + a. the preservation of mesh_exterior and name settings of geometryBodyGroup in new_draft_param_as_dict + b. the new dependency geometry should have replaced the old dependency geometry in the new_draft_param_as_dict + """ + import copy + + def check_setting_preserved( + result_entity_info: GeometryEntityInfo, + reference_entity_infos: list[GeometryEntityInfo], + entity_type: str, + setting_name: str, + ): + """ + Check that a specific setting is preserved from reference entity infos. + + Args: + result_entity_info: The merged entity info to verify + reference_entity_infos: List of reference entity infos to check against + entity_type: Either "body" or "face" + setting_name: The setting to check (e.g., "mesh_exterior", "name") + """ + if entity_type == "body": + group_tag = result_entity_info.body_group_tag + attribute_names = result_entity_info.body_attribute_names + grouped_entities = result_entity_info.grouped_bodies + elif entity_type == "face": + group_tag = result_entity_info.face_group_tag + attribute_names = result_entity_info.face_attribute_names + grouped_entities = result_entity_info.grouped_faces + else: + raise ValueError(f"Invalid entity_type: {entity_type}") + + group_index_result = attribute_names.index(group_tag) + + for entity in grouped_entities[group_index_result]: + found = False + for reference_info in reference_entity_infos: + if entity_type == "body": + ref_attribute_names = reference_info.body_attribute_names + ref_grouped_entities = reference_info.grouped_bodies + else: # face + ref_attribute_names = reference_info.face_attribute_names + ref_grouped_entities = reference_info.grouped_faces + + group_index_ref = ref_attribute_names.index(group_tag) + + for ref_entity in ref_grouped_entities[group_index_ref]: + if entity.private_attribute_id == ref_entity.private_attribute_id: + result_value = getattr(entity, setting_name) + ref_value = getattr(ref_entity, setting_name) + assert result_value == ref_value, ( + f"{setting_name} mismatch for {entity_type} " + f"'{entity.name}' (id: {entity.private_attribute_id}): " + f"expected {ref_value}, got {result_value}" + ) + found = True + break + if found: + break + assert found, ( + f"{entity_type.capitalize()} '{entity.name}' (id: {entity.private_attribute_id}) " + f"not found in any reference entity info" + ) + + # Load test data + with open("data/root_geometry_cube_simulation.json", "r") as f: + root_cube_simulation_dict = json.load(f) + root_cube_entity_info = GeometryEntityInfo.model_validate( + root_cube_simulation_dict["private_attribute_asset_cache"]["project_entity_info"] + ) + with open("data/dependency_geometry_sphere1_simulation.json", "r") as f: + dependency_sphere1_simulation_dict = json.load(f) + dependency_sphere1_entity_info = GeometryEntityInfo.model_validate( + dependency_sphere1_simulation_dict["private_attribute_asset_cache"][ + "project_entity_info" + ] + ) + with open("data/dependency_geometry_sphere2_simulation.json", "r") as f: + dependency_sphere2_simulation_dict = json.load(f) + dependency_sphere2_entity_info = GeometryEntityInfo.model_validate( + dependency_sphere2_simulation_dict["private_attribute_asset_cache"][ + "project_entity_info" + ] + ) + + # Test 1: Merge root geometry and one dependency geometry + # Should preserve mesh_exterior and name settings of geometryBodyGroup in root geometry + result_entity_info_dict1 = services.merge_geometry_entity_info( + draft_param_as_dict=root_cube_simulation_dict, + geometry_dependencies_param_as_dict=[ + root_cube_simulation_dict, + dependency_sphere1_simulation_dict, + ], + ) + result_entity_info1 = GeometryEntityInfo.model_validate(result_entity_info_dict1) + + # Load expected result for test 1 + with open("data/result_merged_geometry_entity_info1.json", "r") as f: + expected_result1 = json.load(f) + + # Compare results + assert compare_values( + result_entity_info_dict1, expected_result1 + ), "Test 1 failed: Merged entity info does not match expected result" + + # Verify key properties are preserved using helper function + check_setting_preserved( + result_entity_info1, + [root_cube_entity_info, dependency_sphere1_entity_info], + entity_type="body", + setting_name="mesh_exterior", + ) + check_setting_preserved( + result_entity_info1, + [root_cube_entity_info, dependency_sphere1_entity_info], + entity_type="body", + setting_name="name", + ) + check_setting_preserved( + result_entity_info1, + [root_cube_entity_info, dependency_sphere1_entity_info], + entity_type="face", + setting_name="name", + ) + + # Test 2: Start from result of (1), replace the dependency with another dependency geometry + # Should check: + # a. the preservation of mesh_exterior and name settings of geometryBodyGroup in new_draft_param_as_dict + # b. the new dependency geometry should have replaced the old dependency geometry + new_draft_param_as_dict = copy.deepcopy(root_cube_simulation_dict) + new_draft_param_as_dict["private_attribute_asset_cache"]["project_entity_info"] = copy.deepcopy( + result_entity_info_dict1 + ) + + result_entity_info_dict2 = services.merge_geometry_entity_info( + draft_param_as_dict=new_draft_param_as_dict, + geometry_dependencies_param_as_dict=[ + root_cube_simulation_dict, + dependency_sphere2_simulation_dict, + ], + ) + + # Load expected result for test 2 + with open("data/result_merged_geometry_entity_info2.json", "r") as f: + expected_result2 = json.load(f) + + # Compare results + assert compare_values( + result_entity_info_dict2, expected_result2 + ), "Test 2 failed: Merged entity info with replaced dependency does not match expected result" + + result_entity_info2 = GeometryEntityInfo.model_validate(result_entity_info_dict2) + + # Verify key properties are preserved using helper function + check_setting_preserved( + result_entity_info2, + [root_cube_entity_info, dependency_sphere2_entity_info], + entity_type="body", + setting_name="mesh_exterior", + ) + check_setting_preserved( + result_entity_info2, + [root_cube_entity_info, dependency_sphere2_entity_info], + entity_type="body", + setting_name="name", + ) + check_setting_preserved( + result_entity_info2, + [root_cube_entity_info, dependency_sphere2_entity_info], + entity_type="face", + setting_name="name", + ) From 3ea349f1d263cb01373e7638ebb1ad68019ee24d Mon Sep 17 00:00:00 2001 From: Angran Date: Sat, 27 Dec 2025 03:36:44 +0000 Subject: [PATCH 31/38] Update imported surface example --- .../import_surface_field_and_integral.py | 162 +++++++++--------- 1 file changed, 84 insertions(+), 78 deletions(-) diff --git a/examples/post_processing/imported_surfaces/import_surface_field_and_integral.py b/examples/post_processing/imported_surfaces/import_surface_field_and_integral.py index 9146827d3..9b42300c5 100644 --- a/examples/post_processing/imported_surfaces/import_surface_field_and_integral.py +++ b/examples/post_processing/imported_surfaces/import_surface_field_and_integral.py @@ -4,87 +4,93 @@ ObliqueChannel.get_files() project = fl.Project.from_volume_mesh( - ObliqueChannel.mesh_filename, - name="Cartesian channel mesh", + ObliqueChannel.mesh_filename, name="Cartesian channel mesh", solver_version=solver_version ) volume_mesh = project.volume_mesh -with fl.SI_unit_system: - op = fl.GenericReferenceCondition.from_mach( - mach=0.3, - ) - massFlowRate = fl.UserVariable( - name="MassFluxProjected", - value=-1 - * fl.solution.density - * fl.math.dot(fl.solution.velocity, fl.solution.node_unit_normal), - ) - massFlowRateIntegral = fl.SurfaceIntegralOutput( - name="MassFluxIntegral", - output_fields=[massFlowRate], - surfaces=volume_mesh["VOLUME/LEFT"], - ) - params = fl.SimulationParams( - operating_condition=op, - models=[ - fl.Fluid( - navier_stokes_solver=fl.NavierStokesSolver(absolute_tolerance=1e-10), - turbulence_model_solver=fl.NoneSolver(), - ), - fl.Inflow( - entities=[volume_mesh["VOLUME/LEFT"]], - total_temperature=op.thermal_state.temperature * 1.018, - velocity_direction=(1.0, 0.0, 0.0), - spec=fl.MassFlowRate( - value=op.velocity_magnitude * op.thermal_state.density * (0.2 * fl.u.m**2) +normal_imported_surface = project.import_surface_mesh_from_file( + ObliqueChannel.extra["rectangle_normal"], name="normal" +) +oblique_imported_surface = project.import_surface_mesh_from_file( + ObliqueChannel.extra["rectangle_oblique"], name="oblique" +) +imported_surface_components = [normal_imported_surface, oblique_imported_surface] + +with fl.create_draft( + new_run_from=volume_mesh, + imported_surface_components=imported_surface_components, +) as draft: + with fl.SI_unit_system: + op = fl.GenericReferenceCondition.from_mach( + mach=0.3, + ) + massFlowRate = fl.UserVariable( + name="MassFluxProjected", + value=-1 + * fl.solution.density + * fl.math.dot(fl.solution.velocity, fl.solution.node_unit_normal), + ) + massFlowRateIntegral = fl.SurfaceIntegralOutput( + name="MassFluxIntegral", + output_fields=[massFlowRate], + surfaces=volume_mesh["VOLUME/LEFT"], + ) + params = fl.SimulationParams( + operating_condition=op, + models=[ + fl.Fluid( + navier_stokes_solver=fl.NavierStokesSolver(absolute_tolerance=1e-10), + turbulence_model_solver=fl.NoneSolver(), ), - ), - fl.Outflow( - entities=[volume_mesh["VOLUME/RIGHT"]], - spec=fl.Pressure(op.thermal_state.pressure), - ), - fl.SlipWall( - entities=[ - volume_mesh["VOLUME/FRONT"], - volume_mesh["VOLUME/BACK"], - volume_mesh["VOLUME/TOP"], - volume_mesh["VOLUME/BOTTOM"], - ] - ), - ], - time_stepping=fl.Steady(), - outputs=[ - fl.VolumeOutput( - output_format="paraview", - output_fields=["primitiveVars"], - ), - fl.SurfaceOutput( - output_fields=[ - fl.solution.velocity, - fl.solution.Cp, - ], - surfaces=[ - fl.ImportedSurface( - name="normal", file_name=ObliqueChannel.extra["rectangle_normal"] - ), - fl.ImportedSurface( - name="oblique", file_name=ObliqueChannel.extra["rectangle_oblique"] - ), - ], - ), - fl.SurfaceIntegralOutput( - name="MassFlowRateImportedSurface", - output_fields=[massFlowRate], - surfaces=[ - fl.ImportedSurface( - name="normal", file_name=ObliqueChannel.extra["rectangle_normal"] - ), - fl.ImportedSurface( - name="oblique", file_name=ObliqueChannel.extra["rectangle_oblique"] + fl.Inflow( + entities=[volume_mesh["VOLUME/LEFT"]], + total_temperature=op.thermal_state.temperature * 1.018, + velocity_direction=(1.0, 0.0, 0.0), + spec=fl.MassFlowRate( + value=op.velocity_magnitude * op.thermal_state.density * (0.2 * fl.u.m**2) ), - ], - ), - ], - ) -project.run_case(params, "test_imported_surfaces_field_and_integral") + ), + fl.Outflow( + entities=[volume_mesh["VOLUME/RIGHT"]], + spec=fl.Pressure(op.thermal_state.pressure), + ), + fl.SlipWall( + entities=[ + volume_mesh["VOLUME/FRONT"], + volume_mesh["VOLUME/BACK"], + volume_mesh["VOLUME/TOP"], + volume_mesh["VOLUME/BOTTOM"], + ] + ), + ], + time_stepping=fl.Steady(), + outputs=[ + fl.VolumeOutput( + output_format="paraview", + output_fields=["primitiveVars"], + ), + fl.SurfaceOutput( + output_fields=[ + fl.solution.velocity, + fl.solution.Cp, + ], + surfaces=[ + volume_mesh["VOLUME/FRONT"], + draft.imported_surface_components["normal"], + normal_imported_surface, + oblique_imported_surface, + ], + ), + fl.SurfaceIntegralOutput( + name="MassFlowRateImportedSurface", + output_fields=[massFlowRate], + surfaces=[ + draft.imported_surface_components["normal"], + normal_imported_surface, + oblique_imported_surface, + ], + ), + ], + ) + project.run_case(params, "test_imported_surfaces_field_and_integral") From e69c6f1ce1ae64648f55c80e6220c8cc33877736 Mon Sep 17 00:00:00 2001 From: Angran Date: Sun, 28 Dec 2025 02:13:22 +0000 Subject: [PATCH 32/38] log info update --- flow360/component/geometry.py | 4 ++-- flow360/component/surface_mesh_v2.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/flow360/component/geometry.py b/flow360/component/geometry.py index 5097fd9cc..18f179e8a 100644 --- a/flow360/component/geometry.py +++ b/flow360/component/geometry.py @@ -145,7 +145,6 @@ def __init__( self._submission_mode: SubmissionMode = SubmissionMode.PROJECT_ROOT self._validate_geometry() - self._set_default_project_name() ResourceDraft.__init__(self) def _validate_geometry(self): @@ -229,6 +228,7 @@ def _create_project_root_resource( self, mapbc_files: List[str], description: str = "" ) -> GeometryMeta: """Create a new geometry resource that will be the root of a new project.""" + self._set_default_project_name() req = NewGeometryRequest( name=self.project_name, solver_version=self.solver_version, @@ -362,7 +362,7 @@ def submit( log_message = "Geometry successfully submitted" else: info = self._create_dependency_resource(mapbc_files, description, draft_id, icon) - log_message = "Supplementary geometry resources successfully submitted" + log_message = "New geometry successfully submitted to the project" # Upload files geometry = self._upload_files(info, mapbc_files, progress_callback) diff --git a/flow360/component/surface_mesh_v2.py b/flow360/component/surface_mesh_v2.py index db41c1d63..43a8abe3f 100644 --- a/flow360/component/surface_mesh_v2.py +++ b/flow360/component/surface_mesh_v2.py @@ -147,7 +147,6 @@ def __init__( self._submission_mode: SubmissionMode = SubmissionMode.PROJECT_ROOT self._validate_surface_mesh() - self._set_default_project_name() ResourceDraft.__init__(self) def _validate_surface_mesh(self): @@ -205,6 +204,7 @@ def set_dependency_context( def _create_project_root_resource(self, description: str = "") -> SurfaceMeshMetaV2: """Create a new surface mesh resource that will be the root of a new project.""" + self._set_default_project_name() req = NewSurfaceMeshRequestV2( name=self.project_name, solver_version=self.solver_version, @@ -340,7 +340,7 @@ def submit( log_message = "Surface mesh successfully submitted" else: info = self._create_dependency_resource(description, draft_id, icon) - log_message = "Supplementary surface mesh resources successfully submitted" + log_message = "New surface mesh successfully submitted to the project" # Upload files surface_mesh = self._upload_files(info, progress_callback) From 94d6fff3de8b5d122714dcd7d3b5861bea211b18 Mon Sep 17 00:00:00 2001 From: Feilin Jia Date: Mon, 29 Dec 2025 16:57:24 +0000 Subject: [PATCH 33/38] fix mirroring functionality --- flow360/component/simulation/draft_context/mirror.py | 2 +- flow360/component/simulation/entity_info.py | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/flow360/component/simulation/draft_context/mirror.py b/flow360/component/simulation/draft_context/mirror.py index 4f962ff09..7eb4291b2 100644 --- a/flow360/component/simulation/draft_context/mirror.py +++ b/flow360/component/simulation/draft_context/mirror.py @@ -159,7 +159,7 @@ def _build_mirrored_surfaces( if not body_group_id_to_mirror_id or face_group_to_body_group is None: return [] - surfaces_by_name: Dict[str, Surface] = {surface.name: surface for surface in surfaces} + surfaces_by_name: Dict[str, Surface] = {surface.private_attribute_id: surface for surface in surfaces} requested_body_group_ids = set(body_group_id_to_mirror_id.keys()) mirrored_surfaces: List[MirroredSurface] = [] diff --git a/flow360/component/simulation/entity_info.py b/flow360/component/simulation/entity_info.py index dec3a75c0..a977a001d 100644 --- a/flow360/component/simulation/entity_info.py +++ b/flow360/component/simulation/entity_info.py @@ -509,10 +509,13 @@ def get_body_group_to_face_group_name_map(self) -> dict[str, list[str]]: """ # pylint: disable=too-many-locals - def create_group_to_sub_component_mapping(group): + def create_group_to_sub_component_mapping(group, use_name_as_key=False): mapping = defaultdict(list) for item in group: - mapping[item.private_attribute_id].extend(item.private_attribute_sub_components) + if use_name_as_key: + mapping[item.name].extend(item.private_attribute_sub_components) + else: + mapping[item.private_attribute_id].extend(item.private_attribute_sub_components) return mapping body_group_to_body = create_group_to_sub_component_mapping( @@ -530,7 +533,8 @@ def create_group_to_sub_component_mapping(group): ) face_group_by_body_id_to_face = create_group_to_sub_component_mapping( - self._get_list_of_entities(entity_type_name="face", attribute_name="groupByBodyId") + self._get_list_of_entities(entity_type_name="face", attribute_name="groupByBodyId"), + use_name_as_key=True ) body_group_to_face = defaultdict(list) From 41f204eb2f811664e6458463276a330a3375e64e Mon Sep 17 00:00:00 2001 From: Feilin Jia Date: Mon, 29 Dec 2025 17:02:17 +0000 Subject: [PATCH 34/38] lint --- flow360/component/simulation/draft_context/mirror.py | 4 +++- flow360/component/simulation/entity_info.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/flow360/component/simulation/draft_context/mirror.py b/flow360/component/simulation/draft_context/mirror.py index 7eb4291b2..6c0958f89 100644 --- a/flow360/component/simulation/draft_context/mirror.py +++ b/flow360/component/simulation/draft_context/mirror.py @@ -159,7 +159,9 @@ def _build_mirrored_surfaces( if not body_group_id_to_mirror_id or face_group_to_body_group is None: return [] - surfaces_by_name: Dict[str, Surface] = {surface.private_attribute_id: surface for surface in surfaces} + surfaces_by_name: Dict[str, Surface] = { + surface.private_attribute_id: surface for surface in surfaces + } requested_body_group_ids = set(body_group_id_to_mirror_id.keys()) mirrored_surfaces: List[MirroredSurface] = [] diff --git a/flow360/component/simulation/entity_info.py b/flow360/component/simulation/entity_info.py index a977a001d..a48371b43 100644 --- a/flow360/component/simulation/entity_info.py +++ b/flow360/component/simulation/entity_info.py @@ -533,8 +533,8 @@ def create_group_to_sub_component_mapping(group, use_name_as_key=False): ) face_group_by_body_id_to_face = create_group_to_sub_component_mapping( - self._get_list_of_entities(entity_type_name="face", attribute_name="groupByBodyId"), - use_name_as_key=True + self._get_list_of_entities(entity_type_name="face", attribute_name="groupByBodyId"), + use_name_as_key=True, ) body_group_to_face = defaultdict(list) From d1b8de60da2141338790a42708f62d64373b1b53 Mon Sep 17 00:00:00 2001 From: Angran Date: Wed, 31 Dec 2025 15:34:42 +0000 Subject: [PATCH 35/38] Implement more robust fix for mirroring functionality --- .../simulation/draft_context/mirror.py | 4 +- flow360/component/simulation/entity_info.py | 47 +++++++++++++++---- 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/flow360/component/simulation/draft_context/mirror.py b/flow360/component/simulation/draft_context/mirror.py index 6c0958f89..4f962ff09 100644 --- a/flow360/component/simulation/draft_context/mirror.py +++ b/flow360/component/simulation/draft_context/mirror.py @@ -159,9 +159,7 @@ def _build_mirrored_surfaces( if not body_group_id_to_mirror_id or face_group_to_body_group is None: return [] - surfaces_by_name: Dict[str, Surface] = { - surface.private_attribute_id: surface for surface in surfaces - } + surfaces_by_name: Dict[str, Surface] = {surface.name: surface for surface in surfaces} requested_body_group_ids = set(body_group_id_to_mirror_id.keys()) mirrored_surfaces: List[MirroredSurface] = [] diff --git a/flow360/component/simulation/entity_info.py b/flow360/component/simulation/entity_info.py index a48371b43..080185f27 100644 --- a/flow360/component/simulation/entity_info.py +++ b/flow360/component/simulation/entity_info.py @@ -509,20 +509,35 @@ def get_body_group_to_face_group_name_map(self) -> dict[str, list[str]]: """ # pylint: disable=too-many-locals - def create_group_to_sub_component_mapping(group, use_name_as_key=False): + def create_group_to_sub_component_mapping(group, id_to_private_id): mapping = defaultdict(list) for item in group: - if use_name_as_key: - mapping[item.name].extend(item.private_attribute_sub_components) - else: - mapping[item.private_attribute_id].extend(item.private_attribute_sub_components) + mapping[item.name].extend( + [ + id_to_private_id[component] + for component in item.private_attribute_sub_components + ] + ) return mapping + body_id_to_private_id = { + body.name: body.private_attribute_id + for body in self._get_list_of_entities(entity_type_name="body", attribute_name="bodyId") + } + face_id_to_private_id = { + face.name: face.private_attribute_id + for face in self._get_list_of_entities(entity_type_name="face", attribute_name="faceId") + } + + # body group name to body private id mapping body_group_to_body = create_group_to_sub_component_mapping( - self._get_list_of_entities(entity_type_name="body", attribute_name=self.body_group_tag) + self._get_list_of_entities(entity_type_name="body", attribute_name=self.body_group_tag), + body_id_to_private_id, ) + # face group name to face private id mapping boundary_to_face = create_group_to_sub_component_mapping( - self._get_list_of_entities(entity_type_name="face", attribute_name=self.face_group_tag) + self._get_list_of_entities(entity_type_name="face", attribute_name=self.face_group_tag), + face_id_to_private_id, ) if "groupByBodyId" not in self.face_attribute_names: @@ -534,13 +549,18 @@ def create_group_to_sub_component_mapping(group, use_name_as_key=False): face_group_by_body_id_to_face = create_group_to_sub_component_mapping( self._get_list_of_entities(entity_type_name="face", attribute_name="groupByBodyId"), - use_name_as_key=True, + face_id_to_private_id, ) + # body private id to face private id mapping + body_to_face = { + body_id_to_private_id[key]: value + for key, value in face_group_by_body_id_to_face.items() + } body_group_to_face = defaultdict(list) for body_group, body_ids in body_group_to_body.items(): for body_id in body_ids: - body_group_to_face[body_group].extend(face_group_by_body_id_to_face[body_id]) + body_group_to_face[body_group].extend(body_to_face[body_id]) face_to_body_group = {} for body_group_name, face_ids in body_group_to_face.items(): @@ -579,9 +599,16 @@ def get_face_group_to_body_group_id_map(self) -> dict[str, str]: """ body_group_to_boundary = self.get_body_group_to_face_group_name_map() + body_group_name_to_id = { + body.name: body.private_attribute_id + for body in self._get_list_of_entities( + entity_type_name="body", attribute_name=self.body_group_tag + ) + } face_group_to_body_group: dict[str, str] = {} - for body_group_id, boundary_names in body_group_to_boundary.items(): + for body_group_name, boundary_names in body_group_to_boundary.items(): + body_group_id = body_group_name_to_id[body_group_name] for boundary_name in boundary_names: existing_owner = face_group_to_body_group.get(boundary_name) if existing_owner is not None and existing_owner != body_group_id: From 4251b14d996a70f2a7b8065f91f453790d569bf9 Mon Sep 17 00:00:00 2001 From: Angran Date: Wed, 31 Dec 2025 20:52:46 +0000 Subject: [PATCH 36/38] use draft in the example --- .../import_surface_field_and_integral.py | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/examples/post_processing/imported_surfaces/import_surface_field_and_integral.py b/examples/post_processing/imported_surfaces/import_surface_field_and_integral.py index 9b42300c5..4d603b50e 100644 --- a/examples/post_processing/imported_surfaces/import_surface_field_and_integral.py +++ b/examples/post_processing/imported_surfaces/import_surface_field_and_integral.py @@ -3,23 +3,21 @@ ObliqueChannel.get_files() -project = fl.Project.from_volume_mesh( - ObliqueChannel.mesh_filename, name="Cartesian channel mesh", solver_version=solver_version -) +project = fl.Project.from_volume_mesh(ObliqueChannel.mesh_filename, name="Cartesian channel mesh") volume_mesh = project.volume_mesh -normal_imported_surface = project.import_surface_mesh_from_file( +normal_imported_surface = project.import_surface( ObliqueChannel.extra["rectangle_normal"], name="normal" ) -oblique_imported_surface = project.import_surface_mesh_from_file( +oblique_imported_surface = project.import_surface( ObliqueChannel.extra["rectangle_oblique"], name="oblique" ) -imported_surface_components = [normal_imported_surface, oblique_imported_surface] +imported_surfaces = [normal_imported_surface, oblique_imported_surface] with fl.create_draft( new_run_from=volume_mesh, - imported_surface_components=imported_surface_components, + imported_surfaces=imported_surfaces, ) as draft: with fl.SI_unit_system: op = fl.GenericReferenceCondition.from_mach( @@ -34,7 +32,7 @@ massFlowRateIntegral = fl.SurfaceIntegralOutput( name="MassFluxIntegral", output_fields=[massFlowRate], - surfaces=volume_mesh["VOLUME/LEFT"], + surfaces=draft.surfaces["VOLUME/LEFT"], ) params = fl.SimulationParams( operating_condition=op, @@ -44,7 +42,7 @@ turbulence_model_solver=fl.NoneSolver(), ), fl.Inflow( - entities=[volume_mesh["VOLUME/LEFT"]], + entities=[draft.surfaces["VOLUME/LEFT"]], total_temperature=op.thermal_state.temperature * 1.018, velocity_direction=(1.0, 0.0, 0.0), spec=fl.MassFlowRate( @@ -52,15 +50,15 @@ ), ), fl.Outflow( - entities=[volume_mesh["VOLUME/RIGHT"]], + entities=[draft.surfaces["VOLUME/RIGHT"]], spec=fl.Pressure(op.thermal_state.pressure), ), fl.SlipWall( entities=[ - volume_mesh["VOLUME/FRONT"], - volume_mesh["VOLUME/BACK"], - volume_mesh["VOLUME/TOP"], - volume_mesh["VOLUME/BOTTOM"], + draft.surfaces["VOLUME/FRONT"], + draft.surfaces["VOLUME/BACK"], + draft.surfaces["VOLUME/TOP"], + draft.surfaces["VOLUME/BOTTOM"], ] ), ], @@ -76,19 +74,17 @@ fl.solution.Cp, ], surfaces=[ - volume_mesh["VOLUME/FRONT"], - draft.imported_surface_components["normal"], - normal_imported_surface, - oblique_imported_surface, + draft.surfaces["VOLUME/FRONT"], + draft.imported_surfaces["normal"], + draft.imported_surfaces["oblique"], ], ), fl.SurfaceIntegralOutput( name="MassFlowRateImportedSurface", output_fields=[massFlowRate], surfaces=[ - draft.imported_surface_components["normal"], - normal_imported_surface, - oblique_imported_surface, + draft.imported_surfaces["normal"], + draft.imported_surfaces["oblique"], ], ), ], From fe55883a398b818398cb9cf584bd16012b25bb0e Mon Sep 17 00:00:00 2001 From: Angran Date: Wed, 31 Dec 2025 20:55:24 +0000 Subject: [PATCH 37/38] Address comments 1 --- flow360/component/geometry.py | 21 ++- flow360/component/project.py | 121 +++++++++--------- flow360/component/project_utils.py | 9 +- .../simulation/draft_context/context.py | 28 ++-- flow360/component/simulation/entity_info.py | 2 +- flow360/component/simulation/services.py | 6 +- .../component/simulation/web/asset_base.py | 6 +- flow360/component/simulation/web/draft.py | 8 +- flow360/component/surface_mesh_v2.py | 21 ++- 9 files changed, 109 insertions(+), 113 deletions(-) diff --git a/flow360/component/geometry.py b/flow360/component/geometry.py index 18f179e8a..717f0fe2b 100644 --- a/flow360/component/geometry.py +++ b/flow360/component/geometry.py @@ -79,7 +79,7 @@ class GeometryMeta(AssetMetaBaseModelV2): """ status: GeometryStatus = pd.Field() # Overshadowing to ensure correct is_final() method - dependency: Optional[bool] = pd.Field(False) + dependency: bool = pd.Field(False) class GeometryDraft(ResourceDraft): @@ -116,7 +116,7 @@ def __init__( Use Geometry.from_file() which sets project_name, solver_version, folder For adding to existing project (dependency geometry): - Use Geometry.from_file_for_project() which sets the dependency context + Use Geometry.import_to_project() which sets the dependency context Parameters ---------- @@ -140,6 +140,8 @@ def __init__( self.solver_version = solver_version self.folder = folder + # pylint: disable=fixme + # TODO: create a DependableResourceDraft for GeometryDraft and SurfaceMeshDraft self.dependency_name = None self.dependency_project_id = None self._submission_mode: SubmissionMode = SubmissionMode.PROJECT_ROOT @@ -179,16 +181,13 @@ def _set_default_project_name(self): def _validate_submission_context(self): """Validate context for submission based on mode.""" if self._submission_mode is None: - raise Flow360ValueError( - "Submission context not set. Use Geometry.from_file() or " - "Geometry.from_file_for_project() to create a properly configured draft." - ) + raise ValueError("[Internal] Geometry submission context not set.") if self._submission_mode == SubmissionMode.PROJECT_ROOT and self.solver_version is None: raise Flow360ValueError("solver_version field is required.") if self._submission_mode == SubmissionMode.PROJECT_DEPENDENCY: if self.dependency_name is None or self.dependency_project_id is None: - raise Flow360ValueError( - "Dependency name and project ID must be set for geometry dependency submission." + raise ValueError( + "[Internal] Dependency name and project ID must be set for geometry dependency submission." ) @property @@ -206,7 +205,7 @@ def set_dependency_context( """ Configure this draft to add geometry to an existing project. - Called internally by Geometry.from_file_for_project(). + Called internally by Geometry.import_to_project(). """ self._submission_mode = SubmissionMode.PROJECT_DEPENDENCY self.dependency_name = name @@ -321,7 +320,7 @@ def submit( The behavior depends on how this draft was created: - If created via Geometry.from_file(): Creates a new project with this geometry as root - - If created via Geometry.from_file_for_project(): Adds geometry to an existing project + - If created via Geometry.import_to_project(): Adds geometry to an existing project Parameters ---------- @@ -475,7 +474,7 @@ def from_file( @classmethod # pylint: disable=too-many-arguments - def from_file_for_project( + def import_to_project( cls, name: str, file_names: Union[List[str], str], diff --git a/flow360/component/project.py b/flow360/component/project.py index 7d64bb24b..069860b16 100644 --- a/flow360/component/project.py +++ b/flow360/component/project.py @@ -48,7 +48,7 @@ from flow360.component.simulation.draft_context.mirror import MirrorStatus from flow360.component.simulation.entity_info import ( GeometryEntityInfo, - update_geometry_entity_info, + merge_geometry_entity_info, ) from flow360.component.simulation.folder import Folder from flow360.component.simulation.primitives import ImportedSurface @@ -101,7 +101,7 @@ class RootType(Enum): VOLUME_MESH = "VolumeMesh" -class ProjectResourceType(Enum): +class ProjectDependencyType(Enum): """ Enum for dependency resource types in the project. @@ -125,7 +125,7 @@ def create_draft( edge_grouping: Optional[str] = None, include_geometries: Optional[List[Geometry]] = None, exclude_geometries: Optional[List[Geometry]] = None, - imported_surface_components: Optional[List[ImportedSurface]] = None, + imported_surfaces: Optional[List[ImportedSurface]] = None, ) -> DraftContext: """ Factory helper used by end users (`with fl.create_draft() as draft`). @@ -137,21 +137,12 @@ def create_draft( # region -----------------------------Private implementations Below----------------------------- - def _resolve_geometry_components( - entity_info, - new_run_from, - current_geometry_dependencies: Optional[List] = None, - include_geometries: Optional[List[Geometry]] = None, - exclude_geometries: Optional[List[Geometry]] = None, + def _resolve_active_geometry_dependencies( + current_geometry_dependencies: List, + include_geometries: List[Geometry], + exclude_geometries: List[Geometry], ) -> Dict[str, Geometry]: - if not isinstance(entity_info, GeometryEntityInfo): - if include_geometries or exclude_geometries: - log.warning( - "Editing geometry components ignored: " - "only project with a non-geometry root asset supports this feature." - ) - return {} - current_geometry_components = ( + active_geometry_dependencies = ( { geometry_dependency["id"]: Geometry.from_cloud(geometry_dependency["id"]) for geometry_dependency in current_geometry_dependencies @@ -159,58 +150,66 @@ def _resolve_geometry_components( if current_geometry_dependencies else {} ) - project = Project.from_cloud(new_run_from.info.project_id) - root_geometry = project.geometry - current_geometry_components.update({root_geometry.id: root_geometry}) - if include_geometries: for geometry in include_geometries: - if geometry.id not in current_geometry_components: - current_geometry_components[geometry.id] = geometry + if geometry.id not in active_geometry_dependencies: + active_geometry_dependencies[geometry.id] = geometry if exclude_geometries: for geometry in exclude_geometries: - excluded_geometry = current_geometry_components.pop(geometry.id, None) + excluded_geometry = active_geometry_dependencies.pop(geometry.id, None) if excluded_geometry is None: log.warning( f"Geometry {geometry.name} not found among current dependencies. Ignoring its exclusion." ) + return active_geometry_dependencies - return current_geometry_components - - def _update_geometry_entity_info(entity_info, geometry_components: Dict[str, Geometry]): - """Update the geometry entity info based on the root and imported geometries.""" - if not isinstance(entity_info, GeometryEntityInfo): + def _merge_geometry_entity_info( + new_run_from, entity_info, active_geometry_dependencies: Dict[str, Geometry] + ): + """Merge the geometry entity info based on the root and imported geometries.""" + if not active_geometry_dependencies: return entity_info - updated_entity_info = update_geometry_entity_info( + # Add root geometry to components to be merged + project = Project.from_cloud(new_run_from.info.project_id) + root_geometry = project.geometry + active_geometry_dependencies.update({root_geometry.id: root_geometry}) + merged_entity_info = merge_geometry_entity_info( current_entity_info=entity_info, entity_info_components=[ - geometry.entity_info for geometry in geometry_components.values() + geometry.entity_info for geometry in active_geometry_dependencies.values() ], ) - return updated_entity_info + return merged_entity_info # endregion ------------------------------------------------------------------------------------ if not isinstance(new_run_from, AssetBase): raise Flow360RuntimeError("create_draft expects a cloud asset instance as `new_run_from`.") + if not isinstance(new_run_from.entity_info, GeometryEntityInfo) and ( + include_geometries or exclude_geometries + ): + raise Flow360ValueError( + "Only project with a geometry root asset supports editing geometry components." + "Please use create_draft without `include_geometries` and `exclude_geometries`." + ) + # Deep copy entity_info for draft isolation entity_info_copy = deep_copy_entity_info(new_run_from.entity_info) - # Edit geometry dependencies if applicable - geometry_components = {} - if new_run_from.info.geometry_dependencies or include_geometries or exclude_geometries: - geometry_components = _resolve_geometry_components( - entity_info=entity_info_copy, - new_run_from=new_run_from, + # Resolve geometry components and merge entity info if applicable + active_geometry_dependencies = {} + if isinstance(new_run_from.entity_info, GeometryEntityInfo): + active_geometry_dependencies = _resolve_active_geometry_dependencies( current_geometry_dependencies=new_run_from.info.geometry_dependencies, include_geometries=include_geometries, exclude_geometries=exclude_geometries, ) - entity_info_copy = _update_geometry_entity_info( + entity_info_copy = _merge_geometry_entity_info( + new_run_from=new_run_from, entity_info=entity_info_copy, - geometry_components=geometry_components, + active_geometry_dependencies=active_geometry_dependencies, ) apply_and_inform_grouping_selections( @@ -234,8 +233,8 @@ def _update_geometry_entity_info(entity_info, geometry_components: Dict[str, Geo entity_info=entity_info_copy, mirror_status=mirror_status, coordinate_system_status=coordinate_system_status, - imported_surface_components=imported_surface_components, - imported_geometry_components=list(geometry_components.values()), + imported_surfaces=imported_surfaces, + imported_geometries=list(active_geometry_dependencies.values()), ) @@ -1222,16 +1221,16 @@ def _detect_input_file_type(file: Union[str, list[str]]): ) def _check_conflicts_with_existing_dependency_resources( - self, name: str, resource_type: ProjectResourceType + self, name: str, resource_type: ProjectDependencyType ): resp = self._project_webapi.post(method="dependency/namecheck", json={"name": name}) if resp.get("status") == "success": return if ( - resource_type == ProjectResourceType.GEOMETRY + resource_type == ProjectDependencyType.GEOMETRY and resp["conflictResourceId"].startswith("geo") ) or ( - resource_type == ProjectResourceType.SURFACE_MESH + resource_type == ProjectDependencyType.SURFACE_MESH and resp["conflictResourceId"].startswith("sm") ): raise Flow360ValueError( @@ -1253,9 +1252,9 @@ def _import_dependency_resource_from_file( if isinstance(files, GeometryFiles): self._check_conflicts_with_existing_dependency_resources( - name=name, resource_type=ProjectResourceType.GEOMETRY + name=name, resource_type=ProjectDependencyType.GEOMETRY ) - draft = Geometry.from_file_for_project( + draft = Geometry.import_to_project( name=name, file_names=files.file_names, project_id=self.id, @@ -1264,9 +1263,9 @@ def _import_dependency_resource_from_file( ) elif isinstance(files, SurfaceMeshFile): self._check_conflicts_with_existing_dependency_resources( - name=name, resource_type=ProjectResourceType.SURFACE_MESH + name=name, resource_type=ProjectDependencyType.SURFACE_MESH ) - draft = SurfaceMeshV2.from_file_for_project( + draft = SurfaceMeshV2.import_to_project( name=name, file_name=files.file_names, project_id=self.id, @@ -1279,7 +1278,7 @@ def _import_dependency_resource_from_file( dependency_resource = draft.submit(run_async=run_async) return dependency_resource - def import_geometry_from_file( + def import_geometry( self, file: Union[str, list[str]], /, @@ -1329,7 +1328,7 @@ def import_geometry_from_file( run_async=run_async, ) - def import_surface_mesh_from_file( + def import_surface( self, file: str, /, @@ -1384,23 +1383,23 @@ def import_surface_mesh_from_file( surface_mesh_id=surface_mesh.id, ) - def _get_dependency_resources_from_cloud(self, resource_type: ProjectResourceType): + def _get_dependency_resources_from_cloud(self, resource_type: ProjectDependencyType): """ Get all imported dependency resources of a given type in the project. Parameters ---------- - resource_type : ProjectResourceType + resource_type : ProjectDependencyType The type of dependency resource to retrieve. """ resp = self._project_webapi.get(method="dependency") - if resource_type == ProjectResourceType.GEOMETRY: + if resource_type == ProjectDependencyType.GEOMETRY: imported_resources = [ Geometry.from_cloud(item["id"]) for item in resp["geometryDependencyResources"] ] - elif resource_type == ProjectResourceType.SURFACE_MESH: + elif resource_type == ProjectDependencyType.SURFACE_MESH: imported_resources = [ ImportedSurface( name=item["name"], @@ -1414,7 +1413,7 @@ def _get_dependency_resources_from_cloud(self, resource_type: ProjectResourceTyp return imported_resources @property - def imported_geometry_components(self) -> List[Geometry]: + def imported_geometries(self) -> List[Geometry]: """ Get all imported geometry components in the project. @@ -1424,10 +1423,12 @@ def imported_geometry_components(self) -> List[Geometry]: A list of Geometry objects representing the imported geometry components. """ - return self._get_dependency_resources_from_cloud(resource_type=ProjectResourceType.GEOMETRY) + return self._get_dependency_resources_from_cloud( + resource_type=ProjectDependencyType.GEOMETRY + ) @property - def imported_surface_components(self) -> List[ImportedSurface]: + def imported_surfaces(self) -> List[ImportedSurface]: """ Get all imported surface components in the project. @@ -1438,7 +1439,7 @@ def imported_surface_components(self) -> List[ImportedSurface]: """ return self._get_dependency_resources_from_cloud( - resource_type=ProjectResourceType.SURFACE_MESH + resource_type=ProjectDependencyType.SURFACE_MESH ) @classmethod @@ -1870,7 +1871,7 @@ def _run( params.pre_submit_summary() active_draft = get_active_draft() - draft.enable_dependency_resources(active_draft) + draft.activate_dependencies(active_draft) draft.update_simulation_params(params) if draft_only: diff --git a/flow360/component/project_utils.py b/flow360/component/project_utils.py index de8ef00d9..48a504a2a 100644 --- a/flow360/component/project_utils.py +++ b/flow360/component/project_utils.py @@ -63,7 +63,7 @@ def _select_tag(new_tag, default_tag, kind): if new_tag is not None: tag = new_tag else: - log.info( + log.debug( f"No {kind} grouping specified when creating draft; " f"using {kind} grouping: {default_tag} from `new_run_from`." ) @@ -82,19 +82,20 @@ def _select_tag(new_tag, default_tag, kind): def _validate_tag(tag, available: list[str], kind: str) -> str: if not available: raise Flow360ValueError( - f"The geometry does not have any {kind} groupings. Please check geometry components." + f"The geometry does not have any {kind} groupings. " + f"Please check the activated geometries in the draft." ) if tag not in available: raise Flow360ValueError( f"The current {kind} grouping '{tag}' is not valid in the geometry. " - f"Please specify a {kind}_grouping when creating draft. " + f"Please specify a valid {kind} grouping via `fl.create_draft({kind}_grouping=...)`. " f"Available tags: {available}." ) return tag face_tag = _validate_tag(face_tag, entity_info.face_attribute_names, "face") # face_tag must be specified either from override or entity_info default - assert face_tag is not None, print( + assert face_tag is not None, log.debug( "[Internal] Default face grouping should be set, face tag to be applied: ", face_tag ) entity_info._group_entity_by_tag("face", face_tag) # pylint:disable=protected-access diff --git a/flow360/component/simulation/draft_context/context.py b/flow360/component/simulation/draft_context/context.py index a0984f165..11a6c83a0 100644 --- a/flow360/component/simulation/draft_context/context.py +++ b/flow360/component/simulation/draft_context/context.py @@ -68,8 +68,8 @@ class DraftContext( # pylint: disable=too-many-instance-attributes "_entity_info", # Interface accessing ALL types of entities. "_entity_registry", - "_imported_surface_components", - "_imported_geometry_components", + "_imported_surfaces", + "_imported_geometries", # Lightweight mirror relationships storage (compared to entity storages) "_mirror_manager", # Internal mirror related entities data storage. @@ -84,8 +84,8 @@ def __init__( self, *, entity_info: EntityInfoModel, - imported_geometry_components: Optional[List] = None, - imported_surface_components: Optional[List[ImportedSurface]] = None, + imported_geometries: Optional[List] = None, + imported_surfaces: Optional[List[ImportedSurface]] = None, mirror_status: Optional[MirrorStatus] = None, coordinate_system_status: Optional[CoordinateSystemStatus] = None, ) -> None: @@ -114,17 +114,13 @@ def __init__( # This builds the registry by referencing entities from our copied entity_info. self._entity_registry: EntityRegistry = EntityRegistry.from_entity_info(entity_info) - self._imported_surface_components: List = ( - imported_surface_components if imported_surface_components else [] - ) + self._imported_surfaces: List = imported_surfaces or [] known_frozen_hashes = set() - for imported_surface in self._imported_surface_components: + for imported_surface in self._imported_surfaces: known_frozen_hashes = self._entity_registry.fast_register( imported_surface, known_frozen_hashes ) - self._imported_geometry_components: List = ( - imported_geometry_components if imported_geometry_components else [] - ) + self._imported_geometries: List = imported_geometries if imported_geometries else [] # Pre-compute face_group_to_body_group map for mirror operations. # This is only available for GeometryEntityInfo. face_group_to_body_group = None @@ -252,16 +248,16 @@ def cylinders(self) -> EntityRegistryView: return self._entity_registry.view(Cylinder) @property - def imported_geometry_components(self) -> List: + def imported_geometries(self) -> List: """ - Return the list of imported surface components in the draft. + Return the list of imported geometries in the draft. """ - return self._imported_geometry_components + return self._imported_geometries @property - def imported_surface_components(self) -> EntityRegistryView: + def imported_surfaces(self) -> EntityRegistryView: """ - Return the list of imported surface components in the draft. + Return the list of imported surfaces in the draft. """ return self._entity_registry.view(ImportedSurface) diff --git a/flow360/component/simulation/entity_info.py b/flow360/component/simulation/entity_info.py index 080185f27..34c65acf2 100644 --- a/flow360/component/simulation/entity_info.py +++ b/flow360/component/simulation/entity_info.py @@ -729,7 +729,7 @@ def parse_entity_info_model(data) -> EntityInfoUnion: return pd.TypeAdapter(EntityInfoUnion).validate_python(data) -def update_geometry_entity_info( +def merge_geometry_entity_info( current_entity_info: GeometryEntityInfo, entity_info_components: List[GeometryEntityInfo], ) -> GeometryEntityInfo: diff --git a/flow360/component/simulation/services.py b/flow360/component/simulation/services.py index 53450fe52..bcf9275c3 100644 --- a/flow360/component/simulation/services.py +++ b/flow360/component/simulation/services.py @@ -11,9 +11,9 @@ # Required for correct global scope initialization from flow360.component.simulation.blueprint.core.dependency_graph import DependencyGraph +from flow360.component.simulation.entity_info import GeometryEntityInfo from flow360.component.simulation.entity_info import ( - GeometryEntityInfo, - update_geometry_entity_info, + merge_geometry_entity_info as merge_geometry_entity_info_obj, ) from flow360.component.simulation.exposed_units import supported_units_by_front_end from flow360.component.simulation.framework.entity_materializer import ( @@ -1235,7 +1235,7 @@ def merge_geometry_entity_info( GeometryEntityInfo.model_validate(dependency_entity_info_dict) ) - merged_entity_info = update_geometry_entity_info( + merged_entity_info = merge_geometry_entity_info_obj( current_entity_info=current_entity_info, entity_info_components=entity_info_components, ) diff --git a/flow360/component/simulation/web/asset_base.py b/flow360/component/simulation/web/asset_base.py index e9b8ca2cc..01868f59c 100644 --- a/flow360/component/simulation/web/asset_base.py +++ b/flow360/component/simulation/web/asset_base.py @@ -169,7 +169,7 @@ def _from_supplied_simulation_dict( @classmethod def _get_simulation_json(cls, asset: AssetBase, clean_front_end_keys: bool = False) -> dict: """Get the simulation json AKA birth setting of the asset. Do we want to cache it in the asset object?""" - ##>> Check if the current asset is project's root item. + ##>> Check if the current asset is project's root item or the dependency assets is still processing ##>> If so then we need to wait for its pipeline to finish generating the simulation json. _resp = RestApi(ProjectInterface.endpoint, id=asset.project_id).get() dependency_ids = [] @@ -185,7 +185,9 @@ def _get_simulation_json(cls, asset: AssetBase, clean_front_end_keys: bool = Fal ) dependency_ids = [_item["id"] for _item in _dependency_resources] if asset.id == _resp["rootItemId"] or asset.id in dependency_ids: - log.debug("Current asset is project's root item. Waiting for pipeline to finish.") + log.debug( + "Current asset is project's root/dependency item. Waiting for pipeline to finish." + ) # pylint: disable=protected-access asset.wait() diff --git a/flow360/component/simulation/web/draft.py b/flow360/component/simulation/web/draft.py index fdf7d8c9f..5ef8355b2 100644 --- a/flow360/component/simulation/web/draft.py +++ b/flow360/component/simulation/web/draft.py @@ -143,18 +143,16 @@ def update_simulation_params(self, params): method="simulation/file", ) - def enable_dependency_resources(self, active_draft): + def activate_dependencies(self, active_draft): """Enable dependency resources for the draft""" if active_draft is None: return - geometry_dependencies = [ - geometry.id for geometry in active_draft.imported_geometry_components - ] + geometry_dependencies = [geometry.id for geometry in active_draft.imported_geometries] surface_mesh_dependencies = [ - surface.surface_mesh_id for surface in active_draft.imported_surface_components + surface.surface_mesh_id for surface in active_draft.imported_surfaces ] self.put( diff --git a/flow360/component/surface_mesh_v2.py b/flow360/component/surface_mesh_v2.py index 43a8abe3f..ba7cf08c3 100644 --- a/flow360/component/surface_mesh_v2.py +++ b/flow360/component/surface_mesh_v2.py @@ -81,7 +81,7 @@ class SurfaceMeshMetaV2(AssetMetaBaseModelV2): file_name: Optional[str] = pd.Field(None, alias="fileName") status: SurfaceMeshStatusV2 = pd.Field() # Overshadowing to ensure correct is_final() method - dependency: Optional[bool] = pd.Field(False) + dependency: bool = pd.Field(False) class SurfaceMeshDraftV2(ResourceDraft): @@ -118,7 +118,7 @@ def __init__( Use SurfaceMeshV2.from_file() which sets project_name, solver_version, folder For adding to existing project (dependency surface mesh): - Use SurfaceMeshV2.from_file_for_project() which sets the dependency context + Use SurfaceMeshV2.import_to_project() which sets the dependency context Parameters ---------- @@ -142,6 +142,8 @@ def __init__( self.solver_version = solver_version self.folder = folder + # pylint: disable=fixme + # TODO: create a DependableResourceDraft for GeometryDraft and SurfaceMeshDraft self.dependency_name = None self.dependency_project_id = None self._submission_mode: SubmissionMode = SubmissionMode.PROJECT_ROOT @@ -175,16 +177,13 @@ def _set_default_project_name(self): def _validate_submission_context(self): """Validate context for submission based on mode.""" if self._submission_mode is None: - raise Flow360ValueError( - "Submission context not set. Use SurfaceMeshV2.from_file() or " - "SurfaceMeshV2.from_file_for_project() to create a properly configured draft." - ) + raise ValueError("[Internal] Surface Mesh Submission context not set.") if self._submission_mode == SubmissionMode.PROJECT_ROOT and self.solver_version is None: raise Flow360ValueError("solver_version field is required.") if self._submission_mode == SubmissionMode.PROJECT_DEPENDENCY: if self.dependency_name is None or self.dependency_project_id is None: - raise Flow360ValueError( - "Dependency name and project ID must be set for surface mesh dependency submission." + raise ValueError( + "[Internal] Dependency name and project ID must be set for surface mesh dependency submission." ) def set_dependency_context( @@ -195,7 +194,7 @@ def set_dependency_context( """ Configure this draft to add surface mesh to an existing project. - Called internally by SurfaceMeshV2.from_file_for_project(). + Called internally by SurfaceMeshV2.import_to_project(). """ self._submission_mode = SubmissionMode.PROJECT_DEPENDENCY self.dependency_name = name @@ -301,7 +300,7 @@ def submit( The behavior depends on how this draft was created: - If created via SurfaceMeshV2.from_file(): Creates a new project with this surface mesh as root - - If created via SurfaceMeshV2.from_file_for_project(): Adds surface mesh to an existing project + - If created via SurfaceMeshV2.import_to_project(): Adds surface mesh to an existing project Parameters ---------- @@ -455,7 +454,7 @@ def from_file( @classmethod # pylint: disable=too-many-arguments - def from_file_for_project( + def import_to_project( cls, name: str, file_name: str, From 1dca4129bec989ba656eefa1361ca1eda4ce3a19 Mon Sep 17 00:00:00 2001 From: Angran Date: Wed, 31 Dec 2025 21:10:32 +0000 Subject: [PATCH 38/38] Add helper function to get project dependency resources --- flow360/component/project.py | 12 +++--- .../component/simulation/web/asset_base.py | 10 ++--- flow360/component/simulation/web/utils.py | 41 +++++++++++++++++++ 3 files changed, 51 insertions(+), 12 deletions(-) create mode 100644 flow360/component/simulation/web/utils.py diff --git a/flow360/component/project.py b/flow360/component/project.py index 069860b16..dba1b00d3 100644 --- a/flow360/component/project.py +++ b/flow360/component/project.py @@ -56,6 +56,7 @@ from flow360.component.simulation.unit_system import LengthType from flow360.component.simulation.web.asset_base import AssetBase from flow360.component.simulation.web.draft import Draft +from flow360.component.simulation.web.utils import get_project_dependency_resources_raw from flow360.component.surface_mesh_v2 import SurfaceMeshV2 from flow360.component.utils import ( AssetShortID, @@ -1394,18 +1395,19 @@ def _get_dependency_resources_from_cloud(self, resource_type: ProjectDependencyT """ - resp = self._project_webapi.get(method="dependency") + raw_resources = get_project_dependency_resources_raw( + project_id=self.id, resource_type=resource_type.value + ) + if resource_type == ProjectDependencyType.GEOMETRY: - imported_resources = [ - Geometry.from_cloud(item["id"]) for item in resp["geometryDependencyResources"] - ] + imported_resources = [Geometry.from_cloud(item["id"]) for item in raw_resources] elif resource_type == ProjectDependencyType.SURFACE_MESH: imported_resources = [ ImportedSurface( name=item["name"], surface_mesh_id=item["id"], ) - for item in resp["surfaceMeshDependencyResources"] + for item in raw_resources ] else: raise Flow360ValueError(f"Unsupported imported resource type: {resource_type}") diff --git a/flow360/component/simulation/web/asset_base.py b/flow360/component/simulation/web/asset_base.py index 01868f59c..1c06784f7 100644 --- a/flow360/component/simulation/web/asset_base.py +++ b/flow360/component/simulation/web/asset_base.py @@ -27,6 +27,7 @@ ) from flow360.component.simulation.folder import Folder from flow360.component.simulation.simulation_params import SimulationParams +from flow360.component.simulation.web.utils import get_project_dependency_resources_raw from flow360.component.utils import ( _local_download_overwrite, formatting_validation_errors, @@ -175,13 +176,8 @@ def _get_simulation_json(cls, asset: AssetBase, clean_front_end_keys: bool = Fal dependency_ids = [] # pylint: disable=protected-access if asset._cloud_resource_type_name in ["Geometry", "SurfaceMesh"]: - _resp_dependency = RestApi(ProjectInterface.endpoint, id=asset.project_id).get( - method="dependency" - ) - _dependency_resources = ( - _resp_dependency["geometryDependencyResources"] - if asset._cloud_resource_type_name == "Geometry" - else _resp_dependency["surfaceMeshDependencyResources"] + _dependency_resources = get_project_dependency_resources_raw( + project_id=asset.project_id, resource_type=asset._cloud_resource_type_name ) dependency_ids = [_item["id"] for _item in _dependency_resources] if asset.id == _resp["rootItemId"] or asset.id in dependency_ids: diff --git a/flow360/component/simulation/web/utils.py b/flow360/component/simulation/web/utils.py new file mode 100644 index 000000000..39bfe28ae --- /dev/null +++ b/flow360/component/simulation/web/utils.py @@ -0,0 +1,41 @@ +"""Utility functions for web/cloud resource operations.""" + +from typing import List, Literal + +from flow360.cloud.rest_api import RestApi +from flow360.component.interfaces import ProjectInterface +from flow360.exceptions import Flow360ValueError + + +def get_project_dependency_resources_raw( + project_id: str, resource_type: Literal["Geometry", "SurfaceMesh"] +) -> List[dict]: + """ + Fetch raw dependency resource data from cloud API. + + Parameters + ---------- + project_id : str + The project ID + resource_type : Literal["Geometry", "SurfaceMesh"] + The type of dependency resource to retrieve + + Returns + ------- + List[dict] + List of raw dependency resource dictionaries from the API response. + Each dict contains at minimum 'id' and 'name' fields. + + Raises + ------ + Flow360ValueError + If resource_type is not supported + """ + resp = RestApi(ProjectInterface.endpoint, id=project_id).get(method="dependency") + + if resource_type == "Geometry": + return resp["geometryDependencyResources"] + if resource_type == "SurfaceMesh": + return resp["surfaceMeshDependencyResources"] + + raise Flow360ValueError(f"Unsupported resource type: {resource_type}")