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..4d603b50e --- /dev/null +++ b/examples/post_processing/imported_surfaces/import_surface_field_and_integral.py @@ -0,0 +1,92 @@ +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 + +normal_imported_surface = project.import_surface( + ObliqueChannel.extra["rectangle_normal"], name="normal" +) +oblique_imported_surface = project.import_surface( + ObliqueChannel.extra["rectangle_oblique"], name="oblique" +) +imported_surfaces = [normal_imported_surface, oblique_imported_surface] + +with fl.create_draft( + new_run_from=volume_mesh, + imported_surfaces=imported_surfaces, +) 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=draft.surfaces["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=[draft.surfaces["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=[draft.surfaces["VOLUME/RIGHT"]], + spec=fl.Pressure(op.thermal_state.pressure), + ), + fl.SlipWall( + entities=[ + draft.surfaces["VOLUME/FRONT"], + draft.surfaces["VOLUME/BACK"], + draft.surfaces["VOLUME/TOP"], + draft.surfaces["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=[ + draft.surfaces["VOLUME/FRONT"], + draft.imported_surfaces["normal"], + draft.imported_surfaces["oblique"], + ], + ), + fl.SurfaceIntegralOutput( + name="MassFlowRateImportedSurface", + output_fields=[massFlowRate], + surfaces=[ + draft.imported_surfaces["normal"], + draft.imported_surfaces["oblique"], + ], + ), + ], + ) + project.run_case(params, "test_imported_surfaces_field_and_integral") diff --git a/flow360/cloud/flow360_requests.py b/flow360/cloud/flow360_requests.py index 76ca7cac7..79410ed38 100644 --- a/flow360/cloud/flow360_requests.py +++ b/flow360/cloud/flow360_requests.py @@ -153,6 +153,36 @@ 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 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/geometry.py b/flow360/component/geometry.py index 9151bb20b..717f0fe2b 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,14 +79,27 @@ class GeometryMeta(AssetMetaBaseModelV2): """ status: GeometryStatus = pd.Field() # Overshadowing to ensure correct is_final() method + dependency: 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 + # pylint: disable=too-many-arguments, too-many-instance-attributes def __init__( self, file_names: Union[List[str], str], @@ -94,19 +109,48 @@ def __init__( tags: List[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.import_to_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): + # 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 + 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,6 +163,14 @@ def _validate_geometry(self): if not os.path.exists(geometry_file): raise Flow360FileError(f"{geometry_file} not found.") + 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__)}" + ) + + 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( @@ -126,47 +178,41 @@ def _validate_geometry(self): 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: + def _validate_submission_context(self): + """Validate context for submission based on mode.""" + if self._submission_mode is None: + 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 ValueError( + "[Internal] Dependency name and project ID must be set for geometry dependency submission." + ) @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_dependency_context( + self, + name: str, + project_id: str, + ) -> None: """ - Submit geometry to cloud and create a new project + Configure this draft to add geometry to an existing project. - 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.import_to_project(). """ + self._submission_mode = SubmissionMode.PROJECT_DEPENDENCY + self.dependency_name = name + self.dependency_project_id = project_id - self._validate() - - 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,9 +221,13 @@ 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 - # Files with 'main' type are treated as MASTER_FILES and are processed after uploading - # 'dependency' type files are uploaded only but not processed. + 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, @@ -194,33 +244,134 @@ def submit(self, description="", progress_callback=None, run_async=False) -> Geo 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.""" - ##:: upload geometry files + req = NewGeometryDependencyRequest( + name=self.dependency_name, + project_id=self.dependency_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) + + 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.import_to_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 + """ + + self._validate_geometry() + self._validate_submission_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 = "New geometry successfully submitted to the project" + + # 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 +472,48 @@ def from_file( file_names, project_name, solver_version, length_unit, tags, folder=folder ) + @classmethod + # pylint: disable=too-many-arguments + def import_to_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 30b138bf2..dba1b00d3 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 @@ -35,20 +35,28 @@ load_status_from_asset, 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 -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, ) from flow360.component.simulation.draft_context.mirror import MirrorStatus +from flow360.component.simulation.entity_info import ( + GeometryEntityInfo, + merge_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 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, @@ -94,11 +102,31 @@ class RootType(Enum): VOLUME_MESH = "VolumeMesh" +class ProjectDependencyType(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" + + +# 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_surfaces: Optional[List[ImportedSurface]] = None, ) -> DraftContext: """ Factory helper used by end users (`with fl.create_draft() as draft`). @@ -108,12 +136,83 @@ def create_draft( the original asset. """ + # region -----------------------------Private implementations Below----------------------------- + + def _resolve_active_geometry_dependencies( + current_geometry_dependencies: List, + include_geometries: List[Geometry], + exclude_geometries: List[Geometry], + ) -> Dict[str, Geometry]: + active_geometry_dependencies = ( + { + 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 active_geometry_dependencies: + active_geometry_dependencies[geometry.id] = geometry + + if exclude_geometries: + for geometry in exclude_geometries: + 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 + + 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 + # 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 active_geometry_dependencies.values() + ], + ) + 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) + # 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 = _merge_geometry_entity_info( + new_run_from=new_run_from, + entity_info=entity_info_copy, + active_geometry_dependencies=active_geometry_dependencies, + ) + apply_and_inform_grouping_selections( entity_info=entity_info_copy, face_grouping=face_grouping, @@ -135,6 +234,8 @@ def create_draft( entity_info=entity_info_copy, mirror_status=mirror_status, coordinate_system_status=coordinate_system_status, + imported_surfaces=imported_surfaces, + imported_geometries=list(active_geometry_dependencies.values()), ) @@ -1120,6 +1221,229 @@ 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: ProjectDependencyType + ): + resp = self._project_webapi.post(method="dependency/namecheck", json={"name": name}) + if resp.get("status") == "success": + return + if ( + resource_type == ProjectDependencyType.GEOMETRY + and resp["conflictResourceId"].startswith("geo") + ) or ( + resource_type == ProjectDependencyType.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, + *, + files: Union[GeometryFiles, SurfaceMeshFile], + name: str, + length_unit: LengthUnitType = "m", + tags: List[str] = None, + run_async: bool = False, + ): + # pylint:disable = protected-access + files._check_files_existence() + + if isinstance(files, GeometryFiles): + self._check_conflicts_with_existing_dependency_resources( + name=name, resource_type=ProjectDependencyType.GEOMETRY + ) + draft = Geometry.import_to_project( + name=name, + file_names=files.file_names, + project_id=self.id, + length_unit=length_unit, + tags=tags, + ) + elif isinstance(files, SurfaceMeshFile): + self._check_conflicts_with_existing_dependency_resources( + name=name, resource_type=ProjectDependencyType.SURFACE_MESH + ) + draft = SurfaceMeshV2.import_to_project( + name=name, + file_name=files.file_names, + project_id=self.id, + length_unit=length_unit, + tags=tags, + ) + else: + raise Flow360ValueError(f"Unsupported file type: {type(files)}") + + dependency_resource = draft.submit(run_async=run_async) + return dependency_resource + + def import_geometry( + self, + file: Union[str, list[str]], + /, + name: str, + 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 + Name of the geometry dependency resource. + 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, + ) + + def import_surface( + 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 ImportedSurface( + name=name, + surface_mesh_id=surface_mesh.id, + ) + + 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 : ProjectDependencyType + The type of dependency resource to retrieve. + + """ + + 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 raw_resources] + elif resource_type == ProjectDependencyType.SURFACE_MESH: + imported_resources = [ + ImportedSurface( + name=item["name"], + surface_mesh_id=item["id"], + ) + for item in raw_resources + ] + else: + raise Flow360ValueError(f"Unsupported imported resource type: {resource_type}") + + return imported_resources + + @property + def imported_geometries(self) -> List[Geometry]: + """ + Get all imported geometry components in the project. + + Returns + ------- + List[Geometry] + A list of Geometry objects representing the imported geometry components. + """ + + return self._get_dependency_resources_from_cloud( + resource_type=ProjectDependencyType.GEOMETRY + ) + + @property + def imported_surfaces(self) -> List[ImportedSurface]: + """ + Get all imported surface components in the project. + + Returns + ------- + List[ImportedSurface] + A list of ImportedSurface objects representing the imported surface components. + """ + + return self._get_dependency_resources_from_cloud( + resource_type=ProjectDependencyType.SURFACE_MESH + ) + @classmethod def _get_user_requested_entity_info( cls, @@ -1548,8 +1872,9 @@ def _run( params.pre_submit_summary() + active_draft = get_active_draft() + draft.activate_dependencies(active_draft) draft.update_simulation_params(params) - upload_imported_surfaces_to_draft(params, draft, fork_from) if draft_only: # pylint: disable=import-outside-toplevel diff --git a/flow360/component/project_utils.py b/flow360/component/project_utils.py index 07806803c..48a504a2a 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 @@ -58,20 +57,55 @@ 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 + # >>> 1. Select groupings to use, either from overrides or entity_info defaults. + + def _select_tag(new_tag, default_tag, kind): + if new_tag is not None: + tag = new_tag + else: + log.debug( + f"No {kind} grouping specified when creating draft; " + f"using {kind} grouping: {default_tag} from `new_run_from`." + ) + tag = default_tag + return 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 + ) + + # >>> 2. Validate groupings + def _validate_tag(tag, available: list[str], kind: str) -> str: + if not available: + raise Flow360ValueError( + 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"Invalid {kind} grouping tag '{tag}'. Available tags: {available}." + f"The current {kind} grouping '{tag}' is not valid in the geometry. " + f"Please specify a valid {kind} grouping via `fl.create_draft({kind}_grouping=...)`. " + f"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 + 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, 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 + # edge_tag can be None if the geometry asset created with surface mesh if edge_grouping is not None and entity_info.edge_attribute_names: - edge_tag = _validate_tag(edge_grouping, entity_info.edge_attribute_names, "edge") + 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 + entity_info._group_entity_by_tag("body", body_group_tag) # pylint:disable=protected-access + return { "face": entity_info.face_group_tag, "edge": entity_info.edge_group_tag, @@ -157,25 +191,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 = [] @@ -399,6 +422,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, @@ -652,6 +699,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_and_broken_entities_inplace(params) @@ -675,41 +725,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_base_names = [] - if parent_case is not None: - parent_existing_imported_file_base_names = _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_base_names: - deduplicated_surface_file_paths_to_import.append(file_path_to_import) - draft.upload_imported_surfaces(deduplicated_surface_file_paths_to_import) 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/draft_context/context.py b/flow360/component/simulation/draft_context/context.py index 9c1e1b6c1..11a6c83a0 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.draft_context.coordinate_system_manager import ( CoordinateSystemManager, @@ -29,6 +29,7 @@ Edge, GenericVolume, GeometryBodyGroup, + ImportedSurface, MirroredGeometryBodyGroup, MirroredSurface, Surface, @@ -67,6 +68,8 @@ class DraftContext( # pylint: disable=too-many-instance-attributes "_entity_info", # Interface accessing ALL types of entities. "_entity_registry", + "_imported_surfaces", + "_imported_geometries", # Lightweight mirror relationships storage (compared to entity storages) "_mirror_manager", # Internal mirror related entities data storage. @@ -76,10 +79,13 @@ class DraftContext( # pylint: disable=too-many-instance-attributes "_token", ) + # pylint: disable=too-many-arguments def __init__( self, *, entity_info: EntityInfoModel, + imported_geometries: Optional[List] = None, + imported_surfaces: Optional[List[ImportedSurface]] = None, mirror_status: Optional[MirrorStatus] = None, coordinate_system_status: Optional[CoordinateSystemStatus] = None, ) -> None: @@ -108,6 +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_surfaces: List = imported_surfaces or [] + known_frozen_hashes = set() + for imported_surface in self._imported_surfaces: + known_frozen_hashes = self._entity_registry.fast_register( + imported_surface, known_frozen_hashes + ) + 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 @@ -234,6 +247,20 @@ def cylinders(self) -> EntityRegistryView: return self._entity_registry.view(Cylinder) + @property + def imported_geometries(self) -> List: + """ + Return the list of imported geometries in the draft. + """ + return self._imported_geometries + + @property + def imported_surfaces(self) -> EntityRegistryView: + """ + Return the list of imported surfaces in the draft. + """ + return self._entity_registry.view(ImportedSurface) + @property def coordinate_systems(self) -> CoordinateSystemManager: """Coordinate system manager.""" diff --git a/flow360/component/simulation/entity_info.py b/flow360/component/simulation/entity_info.py index 60a06f06a..34c65acf2 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 @@ -509,17 +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): + def create_group_to_sub_component_mapping(group, id_to_private_id): mapping = defaultdict(list) for item in group: - 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: @@ -530,13 +548,19 @@ 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"), + 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(): @@ -575,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: @@ -696,3 +727,262 @@ 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 merge_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() + 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] + 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 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 + + 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) + + # 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], + ): + """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_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)) + + 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( + 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, + 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/framework/param_utils.py b/flow360/component/simulation/framework/param_utils.py index c17e9f887..3944e50dd 100644 --- a/flow360/component/simulation/framework/param_utils.py +++ b/flow360/component/simulation/framework/param_utils.py @@ -19,6 +19,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, ) @@ -60,6 +61,9 @@ class AssetCache(Flow360BaseModel): None, description="Collected entity selectors for token reference.", ) + imported_surfaces: Optional[List[ImportedSurface]] = pd.Field( + None, description="List of imported surface meshes for post-processing." + ) mirror_status: Optional[MirrorStatus] = pd.Field( None, description="Status of mirroring operations that are used in the simulation." ) diff --git a/flow360/component/simulation/outputs/outputs.py b/flow360/component/simulation/outputs/outputs.py index 8b60e3017..b6caee6e4 100644 --- a/flow360/component/simulation/outputs/outputs.py +++ b/flow360/component/simulation/outputs/outputs.py @@ -77,6 +77,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 @@ -369,6 +370,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): """ @@ -713,6 +723,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 ForceOutput(_OutputBase): """ diff --git a/flow360/component/simulation/primitives.py b/flow360/component/simulation/primitives.py index bba092f0b..316a8177f 100644 --- a/flow360/component/simulation/primitives.py +++ b/flow360/component/simulation/primitives.py @@ -772,11 +772,15 @@ 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 ) - file_name: str + + 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 class GhostSurface(_SurfaceEntityBase): diff --git a/flow360/component/simulation/services.py b/flow360/component/simulation/services.py index d36e5931a..bcf9275c3 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 +from flow360.component.simulation.entity_info import ( + 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 ( materialize_entities_and_selectors_in_place, @@ -207,6 +211,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}, @@ -238,6 +243,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}, @@ -1190,3 +1196,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 = merge_geometry_entity_info_obj( + 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 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/flow360/component/simulation/web/asset_base.py b/flow360/component/simulation/web/asset_base.py index 82863c4ca..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, @@ -169,11 +170,20 @@ 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() - if asset.id == _resp["rootItemId"]: - log.debug("Current asset is project's root item. Waiting for pipeline to finish.") + dependency_ids = [] + # pylint: disable=protected-access + if asset._cloud_resource_type_name in ["Geometry", "SurfaceMesh"]: + _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: + 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 4878c8293..5ef8355b2 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,25 @@ def update_simulation_params(self, params): method="simulation/file", ) - def upload_imported_surfaces(self, file_paths): - """upload imported surfaces to draft""" + def activate_dependencies(self, active_draft): + """Enable dependency resources for the draft""" - if len(file_paths) == 0: + if active_draft is None: 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", + + geometry_dependencies = [geometry.id for geometry in active_draft.imported_geometries] + + surface_mesh_dependencies = [ + surface.surface_mesh_id for surface in active_draft.imported_surfaces + ] + + 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""" 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}") diff --git a/flow360/component/surface_mesh_v2.py b/flow360/component/surface_mesh_v2.py index 5f319e06a..ba7cf08c3 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,14 +81,27 @@ class SurfaceMeshMetaV2(AssetMetaBaseModelV2): file_name: Optional[str] = pd.Field(None, alias="fileName") status: SurfaceMeshStatusV2 = pd.Field() # Overshadowing to ensure correct is_final() method + dependency: 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 + # pylint: disable=too-many-arguments, too-many-instance-attributes def __init__( self, file_names: str, @@ -93,25 +111,62 @@ def __init__( tags: List[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.import_to_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): + # 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 + 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.length_unit not in LengthUnitType.__args__: + raise Flow360ValueError( + f"specified length_unit : {self.length_unit} is invalid. " + 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_name))[0] log.warning( @@ -119,42 +174,36 @@ def _validate_surface_mesh(self): 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 _validate_submission_context(self): + """Validate context for submission based on mode.""" + if self._submission_mode is None: + 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 ValueError( + "[Internal] Dependency name and project ID must be set for surface mesh dependency submission." + ) - # pylint: disable=protected-access - # pylint: disable=duplicate-code - def submit(self, description="", progress_callback=None, run_async=False) -> SurfaceMeshV2: + def set_dependency_context( + self, + name: str, + project_id: str, + ) -> None: """ - Submit surface mesh file to cloud and create a new project + Configure this draft to add surface mesh to an existing project. - 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 surface mesh asynchronously (default is False). - - Returns - ------- - SurfaceMeshV2 - SurfaceMeshV2 object with id + Called internally by SurfaceMeshV2.import_to_project(). """ + self._submission_mode = SubmissionMode.PROJECT_DEPENDENCY + self.dependency_name = name + self.dependency_project_id = project_id - self._validate() - - if not shared_account_confirm_proceed(): - raise Flow360ValueError("User aborted resource submit.") + def _create_project_root_resource(self, description: str = "") -> SurfaceMeshMetaV2: + """Create a new surface mesh resource that will be the root of a new project.""" - # The first geometry is assumed to be the main one. + self._set_default_project_name() req = NewSurfaceMeshRequestV2( name=self.project_name, solver_version=self.solver_version, @@ -165,49 +214,141 @@ def submit(self, description="", progress_callback=None, run_async=False) -> Sur description=description, ) - ##:: Create new Geometry resource and project resp = RestApi(SurfaceMeshInterfaceV2.endpoint).post(req.dict()) - info = SurfaceMeshMetaV2(**resp) + 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.""" + + req = NewSurfaceMeshDependencyRequest( + name=self.dependency_name, + project_id=self.dependency_project_id, + draft_id=draft_id, + file_name=self._file_name, + length_unit=self.length_unit, + tags=self.tags, + description=description, + icon=icon, + ) - ##:: upload surface mesh file + 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() - 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." - ) + try: + surface_mesh._webapi._upload_file( + remote_file_name=info.file_name, + file_name=self._file_name, + progress_callback=progress_callback, + ) - heartbeat_info["stop"] = True - heartbeat_thread.join() - ##:: kick off pipeline + 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() - log.info(f"Surface mesh successfully submitted: {surface_mesh.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 surface_mesh + + # pylint: disable=protected-access + # pylint: disable=duplicate-code + def submit( + self, + description: str = "", + progress_callback=None, + run_async: bool = False, + draft_id: str = "", + icon: str = "", + ) -> SurfaceMeshV2: + """ + 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.import_to_project(): Adds surface mesh to an existing project + + Parameters + ---------- + description : str, optional + Description of the surface mesh/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 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 + """ + + self._validate_surface_mesh() + self._validate_submission_context() + + if not shared_account_confirm_proceed(): + raise Flow360ValueError("User aborted resource submit.") + + # 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 = "New surface mesh successfully submitted to the project" + + # Upload files + surface_mesh = self._upload_files(info, progress_callback) + + log.info(f"{log_message}: {surface_mesh.short_description()}") + 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 +452,48 @@ def from_file( folder=folder, ) + @classmethod + # pylint: disable=too-many-arguments + def import_to_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""" 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", + } 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, diff --git a/tests/simulation/params/test_validators_output.py b/tests/simulation/params/test_validators_output.py index 4fb651b97..5a13c3635 100644 --- a/tests/simulation/params/test_validators_output.py +++ b/tests/simulation/params/test_validators_output.py @@ -928,6 +928,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, diff --git a/tests/simulation/params/test_validators_params.py b/tests/simulation/params/test_validators_params.py index da4d3d858..5e4b30f55 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( 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", + )