From f62b2e038739a3a78ae2172f4cd3e03308f3a2a7 Mon Sep 17 00:00:00 2001 From: Matteo Bini <91963243+b-matteo@users.noreply.github.com> Date: Mon, 1 Dec 2025 16:19:24 +0100 Subject: [PATCH 01/22] First push of designs v1 implementation --- .../core/_grpc/_services/v1/conversions.py | 45 +++++++++++++++- .../core/_grpc/_services/v1/designs.py | 52 +++++++++++++++++-- .../geometry/core/_grpc/_services/v1/parts.py | 25 +++++++-- src/ansys/geometry/core/misc/options.py | 17 ++++++ src/ansys/geometry/core/modeler.py | 13 +++-- 5 files changed, 141 insertions(+), 11 deletions(-) diff --git a/src/ansys/geometry/core/_grpc/_services/v1/conversions.py b/src/ansys/geometry/core/_grpc/_services/v1/conversions.py index 23791ce99e..a71700a7f6 100644 --- a/src/ansys/geometry/core/_grpc/_services/v1/conversions.py +++ b/src/ansys/geometry/core/_grpc/_services/v1/conversions.py @@ -23,7 +23,10 @@ from typing import TYPE_CHECKING -from ansys.api.discovery.v1.commonenums_pb2 import BackendType as GRPCBackendType +from ansys.api.discovery.v1.commonenums_pb2 import ( + BackendType as GRPCBackendType, + FileFormat as GRPCFileFormat, +) from ansys.api.discovery.v1.commonmessages_pb2 import ( Arc as GRPCArc, Circle as GRPCCircle, @@ -76,6 +79,7 @@ import semver from ansys.geometry.core.connection.backend import BackendType + from ansys.geometry.core.designer.design import DesignFileFormat from ansys.geometry.core.designer.face import SurfaceType from ansys.geometry.core.materials.material import Material from ansys.geometry.core.materials.property import MaterialProperty @@ -1236,6 +1240,45 @@ def from_grpc_update_status_to_parameter_update_status( return status_mapping.get(update_status, ParameterUpdateStatus.UNKNOWN) +def from_design_file_format_to_grpc_file_export_format( + design_file_format: "DesignFileFormat", +) -> GRPCFileFormat: + """Convert from a DesignFileFormat object to a gRPC FileExportFormat one. + + Parameters + ---------- + design_file_format : DesignFileFormat + The file format desired + + Returns + ------- + GRPCFileExportFormat + Converted gRPC File format + """ + from ansys.geometry.core.designer.design import DesignFileFormat + + if design_file_format == DesignFileFormat.SCDOCX: + return GRPCFileFormat.FILEFORMAT_SCDOCX + elif design_file_format == DesignFileFormat.PARASOLID_TEXT: + return GRPCFileFormat.FILEFORMAT_PARASOLID_TEXT + elif design_file_format == DesignFileFormat.PARASOLID_BIN: + return GRPCFileFormat.FILEFORMAT_PARASOLID_BINARY + elif design_file_format == DesignFileFormat.FMD: + return GRPCFileFormat.FILEFORMAT_FMD + elif design_file_format == DesignFileFormat.STEP: + return GRPCFileFormat.FILEFORMAT_STEP + elif design_file_format == DesignFileFormat.IGES: + return GRPCFileFormat.FILEFORMAT_IGES + elif design_file_format == DesignFileFormat.PMDB: + return GRPCFileFormat.FILEFORMAT_PMDB + elif design_file_format == DesignFileFormat.STRIDE: + return GRPCFileFormat.FILEFORMAT_STRIDE + elif design_file_format == DesignFileFormat.DISCO: + return GRPCFileFormat.FILEFORMAT_DISCO + else: + return None + + def from_material_to_grpc_material( material: "Material", ) -> GRPCMaterial: diff --git a/src/ansys/geometry/core/_grpc/_services/v1/designs.py b/src/ansys/geometry/core/_grpc/_services/v1/designs.py index 3c83edc289..13840e640c 100644 --- a/src/ansys/geometry/core/_grpc/_services/v1/designs.py +++ b/src/ansys/geometry/core/_grpc/_services/v1/designs.py @@ -43,13 +43,59 @@ class GRPCDesignsServiceV1(GRPCDesignsService): # pragma: no cover @protect_grpc def __init__(self, channel: grpc.Channel): # noqa: D102 - from ansys.api.dbu.v1.designs_pb2_grpc import DesignsStub + from ansys.api.discovery.v1.commands.file_pb2_grpc import FileStub - self.stub = DesignsStub(channel) + self.stub = FileStub(channel) @protect_grpc def open(self, **kwargs) -> dict: # noqa: D102 - raise NotImplementedError + from pathlib import Path + from typing import TYPE_CHECKING, Generator + + from ansys.api.discovery.v1.commands.file_pb2 import OpenMode, OpenRequest + from ansys.api.discovery.v1.commonenums_pb2 import FileFormat + + import ansys.geometry.core.connection.defaults as pygeom_defaults + + if TYPE_CHECKING: # pragma: no cover + from ansys.geometry.core.misc.options import ImportOptions, ImportOptionsDefinitions + + def request_generator( + file_path: Path, + file_format: FileFormat, + import_options: "ImportOptions", + import_options_definitions: "ImportOptionsDefinitions", + ) -> Generator[OpenRequest, None, None]: + """Generate requests for streaming file upload.""" + msg_buffer = 5 * 1024 # 5KB - for additional message data + if pygeom_defaults.MAX_MESSAGE_LENGTH - msg_buffer < 0: # pragma: no cover + raise ValueError("MAX_MESSAGE_LENGTH is too small for file upload.") + + chunk_size = pygeom_defaults.MAX_MESSAGE_LENGTH - msg_buffer + with Path.open(file_path, "rb") as file: + while chunk := file.read(chunk_size): + test_req = OpenRequest( + data=chunk, + file_format=file_format, + open_mode=OpenMode.OPENMODE_NEW, + import_named_selections=True, + import_options=import_options.to_dict(), + import_options_definitions=import_options_definitions.to_dict(), + ) + yield test_req + + # Call the gRPC service + response = self.stub.Open( + request_generator( + file_path=kwargs["filepath"], + file_format=FileFormat.FILEFORMAT_DISCO, + import_options=kwargs["import_options"], + import_options_definitions=kwargs["import_options_definitions"], + ) + ) + + # Return the response - formatted as a dictionary + return {"file_path": response.design.path} @protect_grpc def new(self, **kwargs) -> dict: # noqa: D102 diff --git a/src/ansys/geometry/core/_grpc/_services/v1/parts.py b/src/ansys/geometry/core/_grpc/_services/v1/parts.py index 2105272b11..92d82d3385 100644 --- a/src/ansys/geometry/core/_grpc/_services/v1/parts.py +++ b/src/ansys/geometry/core/_grpc/_services/v1/parts.py @@ -26,6 +26,7 @@ from ansys.geometry.core.errors import protect_grpc from ..base.parts import GRPCPartsService +from .conversions import from_design_file_format_to_grpc_file_export_format class GRPCPartsServiceV1(GRPCPartsService): # pragma: no cover @@ -43,10 +44,28 @@ class GRPCPartsServiceV1(GRPCPartsService): # pragma: no cover @protect_grpc def __init__(self, channel: grpc.Channel): # noqa: D102 - from ansys.api.geometry.v1.parts_pb2_grpc import PartsStub + from ansys.api.discovery.v1.commands.file_pb2_grpc import FileStub - self.stub = PartsStub(channel) + self.stub = FileStub(channel) @protect_grpc def export(self, **kwargs) -> dict: # noqa: D102 - raise NotImplementedError + from ansys.api.discovery.v1.commands.file_pb2 import SaveMode, SaveRequest + + # Create the request - assumes all inputs are valid and of the proper type + request = SaveRequest( + format=from_design_file_format_to_grpc_file_export_format(kwargs["format"]), + save_mode=SaveMode.SAVEMODE_STANDARD, + write_body_facets=kwargs["write_body_facets"], + parent_entity_id=kwargs["parent_entity_id"], + ) + + # Call the gRPC service + response = self.stub.Save(request) + + # Return the response - formatted as a dictionary + data = bytes() + for elem in response: + data += elem.data + + return {"data": data} diff --git a/src/ansys/geometry/core/misc/options.py b/src/ansys/geometry/core/misc/options.py index a0e43427dc..8a87ecb438 100644 --- a/src/ansys/geometry/core/misc/options.py +++ b/src/ansys/geometry/core/misc/options.py @@ -66,6 +66,23 @@ def to_dict(self): return {k: bool(v) for k, v in asdict(self).items()} +@dataclass +class ImportOptionsDefinitions: + """Import options definitionswhen opening a file. + + Parameters + ---------- + import_named_selections_keys : string = None + Import the named selections keys associated with the root component being inserted. + """ + + import_named_selections_keys: str = None + + def to_dict(self): + """Provide the dictionary representation of the ImportOptionsDefinitions class.""" + return {k: str(v) for k, v in asdict(self).items()} + + class TessellationOptions: """Provides options for getting tessellation. diff --git a/src/ansys/geometry/core/modeler.py b/src/ansys/geometry/core/modeler.py index f0fc490719..9cc96002cb 100644 --- a/src/ansys/geometry/core/modeler.py +++ b/src/ansys/geometry/core/modeler.py @@ -27,12 +27,13 @@ from grpc import Channel +from ansys.geometry.core._grpc._version import GeometryApiProtos from ansys.geometry.core.connection.backend import ApiVersions, BackendType from ansys.geometry.core.connection.client import GrpcClient import ansys.geometry.core.connection.defaults as pygeom_defaults from ansys.geometry.core.errors import GeometryRuntimeError from ansys.geometry.core.misc.checks import check_type, min_backend_version -from ansys.geometry.core.misc.options import ImportOptions +from ansys.geometry.core.misc.options import ImportOptions, ImportOptionsDefinitions from ansys.geometry.core.tools.measurement_tools import MeasurementTools from ansys.geometry.core.tools.prepare_tools import PrepareTools from ansys.geometry.core.tools.repair_tools import RepairTools @@ -363,6 +364,7 @@ def open_file( file_path: str | Path, upload_to_server: bool = True, import_options: ImportOptions = ImportOptions(), + import_options_definitions: ImportOptionsDefinitions = ImportOptionsDefinitions(), ) -> "Design": """Open a file. @@ -411,8 +413,10 @@ def open_file( if self._design is not None and self._design.is_active: self._design.close() - # Format-specific logic - upload the whole containing folder for assemblies - if upload_to_server: + # Format-specific logic - upload the whole containing folder for assemblies. If backend's + # version is > 26.1.0 we're going to upload the file no matter what, as streaming is + # supported. + if upload_to_server and self.client.services.version == GeometryApiProtos.V0: fp_path = Path(file_path) file_size_kb = fp_path.stat().st_size if any( @@ -424,7 +428,7 @@ def open_file( if full_path != fp_path: if full_path.stat().st_size < pygeom_defaults.MAX_MESSAGE_LENGTH: self._upload_file(full_path) - elif self.client.backend_version >= (25, 2, 0): + elif self.client.backend_version == (25, 2, 0): self._upload_file_stream(full_path) else: # pragma: no cover raise RuntimeError( @@ -444,6 +448,7 @@ def open_file( self.client.services.designs.open( filepath=file_path, import_options=import_options, + import_options_definitions=import_options_definitions, ) return self.read_existing_design() From dcb6aad1a687d1474b9f9a6f24762a93c6b336bc Mon Sep 17 00:00:00 2001 From: Jacob Kerstetter Date: Mon, 1 Dec 2025 12:25:04 -0500 Subject: [PATCH 02/22] wip --- .../core/_grpc/_services/v1/conversions.py | 29 ++- .../core/_grpc/_services/v1/designs.py | 229 +++++++++++++++++- 2 files changed, 251 insertions(+), 7 deletions(-) diff --git a/src/ansys/geometry/core/_grpc/_services/v1/conversions.py b/src/ansys/geometry/core/_grpc/_services/v1/conversions.py index a71700a7f6..bc4263aca3 100644 --- a/src/ansys/geometry/core/_grpc/_services/v1/conversions.py +++ b/src/ansys/geometry/core/_grpc/_services/v1/conversions.py @@ -23,6 +23,9 @@ from typing import TYPE_CHECKING +from ansys.api.discovery.v1.commands.file_pb2 import ( + ImportOptionDefinition as GRPCImportOptionDefinition, +) from ansys.api.discovery.v1.commonenums_pb2 import ( BackendType as GRPCBackendType, FileFormat as GRPCFileFormat, @@ -89,7 +92,7 @@ from ansys.geometry.core.math.point import Point2D, Point3D from ansys.geometry.core.math.vector import UnitVector3D from ansys.geometry.core.misc.measurements import Measurement - from ansys.geometry.core.misc.options import TessellationOptions + from ansys.geometry.core.misc.options import TessellationOptions, ImportOptionsDefinitions from ansys.geometry.core.parameters.parameter import ( Parameter, ParameterUpdateStatus, @@ -1388,6 +1391,30 @@ def from_angle_to_grpc_quantity(input: "Measurement") -> GRPCQuantity: return GRPCQuantity(value_in_geometry_units=input.value.m_as(DEFAULT_UNITS.SERVER_ANGLE)) +def from_import_options_definitions_to_grpc_import_options_definition( + import_options_definitions: "ImportOptionsDefinitions", +) -> GRPCImportOptionDefinition: + """Convert an ``ImportOptionsDefinitions`` to import options definition gRPC message. + + Parameters + ---------- + import_options_definitions : ImportOptionsDefinitions + Definition of the import options. + + Returns + ------- + GRPCImportOptionDefinition + Geometry service gRPC import options definition message. + """ + definitions = {} + for key, definition in import_options_definitions.to_dict().items(): + definitions[key] = GRPCImportOptionDefinition( + string_option=str(definition) + ) + + return definitions + + def _nurbs_curves_compatibility(backend_version: "semver.Version", grpc_geometries: GRPCGeometries): """Check if the backend version is compatible with NURBS curves in sketches. diff --git a/src/ansys/geometry/core/_grpc/_services/v1/designs.py b/src/ansys/geometry/core/_grpc/_services/v1/designs.py index 13840e640c..635e41e780 100644 --- a/src/ansys/geometry/core/_grpc/_services/v1/designs.py +++ b/src/ansys/geometry/core/_grpc/_services/v1/designs.py @@ -26,6 +26,14 @@ from ansys.geometry.core.errors import protect_grpc from ..base.designs import GRPCDesignsService +from .conversions import ( + build_grpc_id, + from_grpc_curve_to_curve, + from_grpc_frame_to_frame, + from_grpc_material_to_material, + from_grpc_matrix_to_matrix, + from_grpc_point_to_point3d, +) class GRPCDesignsServiceV1(GRPCDesignsService): # pragma: no cover @@ -44,8 +52,10 @@ class GRPCDesignsServiceV1(GRPCDesignsService): # pragma: no cover @protect_grpc def __init__(self, channel: grpc.Channel): # noqa: D102 from ansys.api.discovery.v1.commands.file_pb2_grpc import FileStub + from ansys.api.discovery.v1.design.designdoc_pb2_grpc import DesignDocStub - self.stub = FileStub(channel) + self.file_stub = FileStub(channel) + self.designdoc_stub = DesignDocStub(channel) @protect_grpc def open(self, **kwargs) -> dict: # noqa: D102 @@ -57,6 +67,8 @@ def open(self, **kwargs) -> dict: # noqa: D102 import ansys.geometry.core.connection.defaults as pygeom_defaults + from .conversions import from_import_options_definitions_to_grpc_import_options_definition + if TYPE_CHECKING: # pragma: no cover from ansys.geometry.core.misc.options import ImportOptions, ImportOptionsDefinitions @@ -77,15 +89,15 @@ def request_generator( test_req = OpenRequest( data=chunk, file_format=file_format, - open_mode=OpenMode.OPENMODE_NEW, + open_mode=OpenMode.OPENMODE_NEW, # TODO: add different modes import_named_selections=True, import_options=import_options.to_dict(), - import_options_definitions=import_options_definitions.to_dict(), + import_options_definitions=from_import_options_definitions_to_grpc_import_options_definition(import_options_definitions), ) yield test_req # Call the gRPC service - response = self.stub.Open( + response = self.file_stub.Open( request_generator( file_path=kwargs["filepath"], file_format=FileFormat.FILEFORMAT_DISCO, @@ -103,7 +115,17 @@ def new(self, **kwargs) -> dict: # noqa: D102 @protect_grpc def get_assembly(self, **kwargs) -> dict: # noqa: D102 - raise NotImplementedError + from ansys.api.discovery.v1.design.designdoc_pb2 import GetAssemblyRequest + + # Create the request - assumes all inputs are valid and of the proper type + request = GetAssemblyRequest(id=build_grpc_id(kwargs["active_design"].get("design_id").id)) + + # Call the gRPC service + response = self.designdoc_stub.GetAssembly(request) + + # Return the response - formatted as a dictionary + serialized_response = self._serialize_assembly_response(response) + return serialized_response @protect_grpc def close(self, **kwargs) -> dict: # noqa: D102 @@ -131,7 +153,18 @@ def insert(self, **kwargs) -> dict: # noqa: D102 @protect_grpc def get_active(self, **kwargs) -> dict: # noqa: D102 - raise NotImplementedError + from ansys.api.discovery.v1.commonmessages_pb2 import EntityRequest + + # Call the gRPC service + response = self.designdoc_stub.Get(request=EntityRequest(id=build_grpc_id(""))) + + # Return the response - formatted as a dictionary + if response.design: + return { + "design_id": response.design.id, + "main_part_id": response.design.main_part_id.id, + "name": response.design.name, + } @protect_grpc def upload_file(self, **kwargs) -> dict: # noqa: D102 @@ -148,3 +181,187 @@ def stream_design_tessellation(self, **kwargs) -> dict: # noqa: D102 @protect_grpc def download_file(self, **kwargs) -> dict: # noqa: D102 raise NotImplementedError + + def _serialize_assembly_response(self, response): + def serialize_body(body): + return { + "id": body.id.id, + "name": body.name, + "master_id": body.master_id.id, + "parent_id": body.parent_id.id, + "is_surface": body.is_surface, + } + + def serialize_component(component): + return { + "id": component.id.id, + "parent_id": component.parent_id.id, + "master_id": component.master_id.id, + "name": component.name, + "placement": component.placement, + "part_master": serialize_part(component.part_master), + } + + def serialize_transformed_part(transformed_part): + return { + "id": transformed_part.id.id, + "name": transformed_part.name, + "placement": from_grpc_matrix_to_matrix(transformed_part.placement), + "part_master": serialize_part(transformed_part.part_master), + } + + def serialize_part(part): + return { + "id": part.id.id, + "name": part.name, + } + + def serialize_material_properties(material_property): + return { + "id": material_property.id.id, + "display_name": material_property.display_name, + "value": material_property.value, + "units": material_property.units, + } + + def serialize_material(material): + material_properties = getattr(material, "material_properties", []) + return { + "name": material.name, + "material_properties": [ + serialize_material_properties(property) for property in material_properties + ], + } + + def serialize_named_selection(named_selection): + return {"id": named_selection.id.id, "name": named_selection.name} + + def serialize_coordinate_systems(coordinate_systems): + + + serialized_cs = [] + for cs in coordinate_systems.coordinate_systems: + serialized_cs.append( + { + "id": cs.id.id, + "name": cs.name, + "frame": from_grpc_frame_to_frame(cs.frame), + } + ) + + return serialized_cs + + def serialize_component_coordinate_systems(component_coordinate_system): + serialized_component_coordinate_systems = [] + for ( + component_coordinate_system_id, + coordinate_systems, + ) in component_coordinate_system.items(): + serialized_component_coordinate_systems.append( + { + "component_id": component_coordinate_system_id, + "coordinate_systems": serialize_coordinate_systems(coordinate_systems), + } + ) + + return serialized_component_coordinate_systems + + def serialize_component_shared_topologies(component_share_topology): + serialized_share_topology = [] + for component_shared_topology_id, shared_topology in component_share_topology.items(): + serialized_share_topology.append( + { + "component_id": component_shared_topology_id, + "shared_topology_type": shared_topology, + } + ) + return serialized_share_topology + + def serialize_beam_curve(curve): + return { + "curve": from_grpc_curve_to_curve(curve.curve), + "start": from_grpc_point_to_point3d(curve.start), + "end": from_grpc_point_to_point3d(curve.end), + "interval_start": curve.interval_start, + "interval_end": curve.interval_end, + "length": curve.length, + } + + def serialize_beam_curve_list(curve_list): + return {"curves": [serialize_beam_curve(curve) for curve in curve_list.curves]} + + def serialize_beam_cross_section(cross_section): + return { + "section_anchor": cross_section.section_anchor, + "section_angle": cross_section.section_angle, + "section_frame": from_grpc_frame_to_frame(cross_section.section_frame), + "section_profile": [ + serialize_beam_curve_list(curve_list) + for curve_list in cross_section.section_profile + ], + } + + def serialize_beam_properties(properties): + return { + "area": properties.area, + "centroid_x": properties.centroid_x, + "centroid_y": properties.centroid_y, + "warping_constant": properties.warping_constant, + "ixx": properties.ixx, + "ixy": properties.ixy, + "iyy": properties.iyy, + "shear_center_x": properties.shear_center_x, + "shear_center_y": properties.shear_center_y, + "torsional_constant": properties.torsional_constant, + } + + def serialize_beam(beam): + return { + "id": beam.id.id, + "parent_id": beam.parent.id, + "start": from_grpc_point_to_point3d(beam.shape.start), + "end": from_grpc_point_to_point3d(beam.shape.end), + "name": beam.name, + "is_deleted": beam.is_deleted, + "is_reversed": beam.is_reversed, + "is_rigid": beam.is_rigid, + "material": from_grpc_material_to_material(beam.material), + "type": beam.type, + "properties": serialize_beam_properties(beam.properties), + "cross_section": serialize_beam_cross_section(beam.cross_section), + } + + def serialize_design_point(design_point): + return { + "id": design_point.id.id, + "name": design_point.owner_name, + "point": from_grpc_point_to_point3d(design_point.points[0]), + "parent_id": design_point.parent_id.id, + } + + parts = getattr(response, "parts", []) + transformed_parts = getattr(response, "transformed_parts", []) + bodies = getattr(response, "bodies", []) + components = getattr(response, "components", []) + materials = getattr(response, "materials", []) + named_selections = getattr(response, "named_selections", []) + component_coordinate_systems = getattr(response, "component_coord_systems", []) + component_shared_topologies = getattr(response, "component_shared_topologies", []) + beams = getattr(response, "beams", []) + design_points = getattr(response, "design_points", []) + return { + "parts": [serialize_part(part) for part in parts] if len(parts) > 0 else [], + "transformed_parts": [serialize_transformed_part(tp) for tp in transformed_parts], + "bodies": [serialize_body(body) for body in bodies] if len(bodies) > 0 else [], + "components": [serialize_component(component) for component in components], + "materials": [serialize_material(material) for material in materials], + "named_selections": [serialize_named_selection(ns) for ns in named_selections], + "component_coordinate_systems": serialize_component_coordinate_systems( + component_coordinate_systems + ), + "component_shared_topologies": serialize_component_shared_topologies( + component_shared_topologies + ), + "beams": [serialize_beam(beam) for beam in beams], + "design_points": [serialize_design_point(dp) for dp in design_points], + } From 3f2be02c9297c09d2b3259e3f5f8857584e7c947 Mon Sep 17 00:00:00 2001 From: Jacob Kerstetter Date: Mon, 1 Dec 2025 13:17:51 -0500 Subject: [PATCH 03/22] improvements to design import/export --- .../core/_grpc/_services/v1/components.py | 8 ++-- .../core/_grpc/_services/v1/conversions.py | 21 ++++++++- .../core/_grpc/_services/v1/designs.py | 43 +++++++++++++++++-- 3 files changed, 64 insertions(+), 8 deletions(-) diff --git a/src/ansys/geometry/core/_grpc/_services/v1/components.py b/src/ansys/geometry/core/_grpc/_services/v1/components.py index 7592ce0a8b..b01f4a8476 100644 --- a/src/ansys/geometry/core/_grpc/_services/v1/components.py +++ b/src/ansys/geometry/core/_grpc/_services/v1/components.py @@ -26,9 +26,9 @@ from ansys.geometry.core.errors import protect_grpc from ..base.components import GRPCComponentsService -from ..base.conversions import from_measurement_to_server_angle from .conversions import ( build_grpc_id, + from_angle_to_grpc_quantity, from_grpc_matrix_to_matrix, from_point3d_to_grpc_point, from_unit_vector_to_grpc_direction, @@ -80,7 +80,7 @@ def create(self, **kwargs) -> dict: # noqa: D102 # Note: response.components is a repeated field, we return the first one component = response.components[0] return { - "id": component.id, + "id": component.id.id, "name": component.name, "instance_name": component.instance_name, "template": kwargs["template_id"], # template_id from input @@ -132,7 +132,7 @@ def set_placement(self, **kwargs) -> dict: # noqa: D102 translation=translation, rotation_axis_origin=origin, rotation_axis_direction=direction, - rotation_angle=from_measurement_to_server_angle(kwargs["rotation_angle"]), + rotation_angle=from_angle_to_grpc_quantity(kwargs["rotation_angle"]), ) ], ) @@ -143,7 +143,7 @@ def set_placement(self, **kwargs) -> dict: # noqa: D102 # Return the response - formatted as a dictionary # Note: response.matrices is a map # Get the matrix for our component ID - matrix_value = response.matrices.get(kwargs["id"].id) + matrix_value = response.matrices.get(kwargs["id"]) return {"matrix": from_grpc_matrix_to_matrix(matrix_value) if matrix_value else None} @protect_grpc diff --git a/src/ansys/geometry/core/_grpc/_services/v1/conversions.py b/src/ansys/geometry/core/_grpc/_services/v1/conversions.py index bc4263aca3..eb216ecbe8 100644 --- a/src/ansys/geometry/core/_grpc/_services/v1/conversions.py +++ b/src/ansys/geometry/core/_grpc/_services/v1/conversions.py @@ -92,7 +92,7 @@ from ansys.geometry.core.math.point import Point2D, Point3D from ansys.geometry.core.math.vector import UnitVector3D from ansys.geometry.core.misc.measurements import Measurement - from ansys.geometry.core.misc.options import TessellationOptions, ImportOptionsDefinitions + from ansys.geometry.core.misc.options import ImportOptionsDefinitions, TessellationOptions from ansys.geometry.core.parameters.parameter import ( Parameter, ParameterUpdateStatus, @@ -1556,3 +1556,22 @@ def serialize_entity_identifier(entity): for entity in getattr(response.tracked_changes, "deleted_edge_ids", []) ], } + +def _check_write_body_facets_input(backend_version: "semver.Version", write_body_facets: bool): + """Check if the backend version is compatible with NURBS curves in sketches. + + Parameters + ---------- + backend_version : semver.Version + The version of the backend. + write_body_facets : bool + Option to write out body facets. + """ + if write_body_facets and backend_version < (26, 1, 0): + from ansys.geometry.core.logger import LOG + + LOG.warning( + "The usage of write_body_facets requires a minimum Ansys release version of " + + "26.1.0, but the current version used is " + + f"{backend_version}." + ) \ No newline at end of file diff --git a/src/ansys/geometry/core/_grpc/_services/v1/designs.py b/src/ansys/geometry/core/_grpc/_services/v1/designs.py index 635e41e780..e34c646c9a 100644 --- a/src/ansys/geometry/core/_grpc/_services/v1/designs.py +++ b/src/ansys/geometry/core/_grpc/_services/v1/designs.py @@ -111,7 +111,19 @@ def request_generator( @protect_grpc def new(self, **kwargs) -> dict: # noqa: D102 - raise NotImplementedError + from ansys.api.discovery.v1.commands.file_pb2 import NewRequest + + # Create the request - assumes all inputs are valid and of the proper type + request = NewRequest(name=kwargs["name"]) + + # Call the gRPC service + response = self.file_stub.New(request) + + # Return the response - formatted as a dictionary + return { + "design_id": response.design.id.id, + "main_part_id": response.design.main_part_id.id, + } @protect_grpc def get_assembly(self, **kwargs) -> dict: # noqa: D102 @@ -141,7 +153,32 @@ def save_as(self, **kwargs) -> dict: # noqa: D102 @protect_grpc def download_export(self, **kwargs) -> dict: # noqa: D102 - raise NotImplementedError + from ansys.api.discovery.v1.commands.file_pb2 import SaveMode, SaveRequest + + from .conversions import ( + _check_write_body_facets_input, + from_design_file_format_to_grpc_file_export_format, + ) + + _check_write_body_facets_input(kwargs["backend_version"], kwargs["write_body_facets"]) + + # Create the request - assumes all inputs are valid and of the proper type + request = SaveRequest( + format=from_design_file_format_to_grpc_file_export_format(kwargs["format"]), + save_mode=SaveMode.SAVEMODE_STANDARD, + write_body_facets=kwargs["write_body_facets"], + ) + + # Call the gRPC service + response_stream = self.file_stub.Save(request) + + # Return the response - formatted as a dictionary + data = bytes() + for response in response_stream: + data += response.data + return { + "data": data, + } @protect_grpc def stream_download_export(self, **kwargs) -> dict: # noqa: D102 @@ -364,4 +401,4 @@ def serialize_design_point(design_point): ), "beams": [serialize_beam(beam) for beam in beams], "design_points": [serialize_design_point(dp) for dp in design_points], - } + } \ No newline at end of file From 561e91378d90f0d72df2d0079a2cff0742bdcb79 Mon Sep 17 00:00:00 2001 From: Jacob Kerstetter Date: Tue, 2 Dec 2025 18:18:11 -0500 Subject: [PATCH 04/22] fixing small issues in materials and points to get v1 tests running --- .../core/_grpc/_services/v1/conversions.py | 21 ++++++++++++++++++- .../core/_grpc/_services/v1/materials.py | 10 ++++----- .../core/_grpc/_services/v1/points.py | 11 +++++----- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/ansys/geometry/core/_grpc/_services/v1/conversions.py b/src/ansys/geometry/core/_grpc/_services/v1/conversions.py index 5363911bdf..5c241f33dd 100644 --- a/src/ansys/geometry/core/_grpc/_services/v1/conversions.py +++ b/src/ansys/geometry/core/_grpc/_services/v1/conversions.py @@ -45,6 +45,7 @@ ) from ansys.api.discovery.v1.design.designmessages_pb2 import ( CurveGeometry as GRPCCurveGeometry, + DatumPointEntity as GRPCDesignPoint, DrivingDimensionEntity as GRPCDrivingDimension, EdgeTessellation as GRPCEdgeTessellation, Geometries as GRPCGeometries, @@ -243,6 +244,24 @@ def from_point2d_to_grpc_point(plane: "Plane", point2d: "Point2D") -> GRPCPoint: ) +def from_point3d_to_grpc_design_point(point: "Point3D") -> GRPCDesignPoint: + """Convert a ``Point3D`` class to a design point gRPC message. + + Parameters + ---------- + point : Point3D + Source point data. + + Returns + ------- + GRPCDesignPoint + Geometry service gRPC design point message. The unit is meters. + """ + return GRPCDesignPoint( + position=from_point3d_to_grpc_point(point), + ) + + def from_unit_vector_to_grpc_direction(unit_vector: "UnitVector3D") -> GRPCDirection: """Convert a ``UnitVector3D`` class to a unit vector gRPC message. @@ -1301,7 +1320,7 @@ def from_material_to_grpc_material( name=material.name, material_properties=[ GRPCMaterialProperty( - id=property.type.value, + id=build_grpc_id(property.type.value), display_name=property.name, value=property.quantity.m, units=format(property.quantity.units), diff --git a/src/ansys/geometry/core/_grpc/_services/v1/materials.py b/src/ansys/geometry/core/_grpc/_services/v1/materials.py index 9078d9edb6..3e10d459c2 100644 --- a/src/ansys/geometry/core/_grpc/_services/v1/materials.py +++ b/src/ansys/geometry/core/_grpc/_services/v1/materials.py @@ -44,17 +44,17 @@ class GRPCMaterialsServiceV1(GRPCMaterialsService): @protect_grpc def __init__(self, channel: grpc.Channel): # noqa: D102 - from ansys.api.discovery.v1.design.data.cadmaterial_pb2_grpc import MaterialsStub + from ansys.api.discovery.v1.design.data.cadmaterial_pb2_grpc import CADMaterialStub - self.stub = MaterialsStub(channel) + self.stub = CADMaterialStub(channel) @protect_grpc def add_material(self, **kwargs) -> dict: # noqa: D102 - from ansys.api.discovery.v1.design.data.cadmaterial_pb2_grpc import CreateRequest + from ansys.api.discovery.v1.design.data.cadmaterial_pb2 import CreateRequest # Create the request - assumes all inputs are valid and of the proper type request = CreateRequest( - request_data=from_material_to_grpc_material(kwargs["material"]), + request_data=[from_material_to_grpc_material(kwargs["material"])], ) # Call the gRPC service @@ -65,7 +65,7 @@ def add_material(self, **kwargs) -> dict: # noqa: D102 @protect_grpc def remove_material(self, **kwargs) -> dict: # noqa: D102 - from ansys.api.discovery.v1.design.data.cadmaterial_pb2_grpc import DeleteRequest + from ansys.api.discovery.v1.design.data.cadmaterial_pb2 import DeleteRequest # Create the request - assumes all inputs are valid and of the proper type request = DeleteRequest( diff --git a/src/ansys/geometry/core/_grpc/_services/v1/points.py b/src/ansys/geometry/core/_grpc/_services/v1/points.py index 0ef2a863b7..0ce1e0a927 100644 --- a/src/ansys/geometry/core/_grpc/_services/v1/points.py +++ b/src/ansys/geometry/core/_grpc/_services/v1/points.py @@ -26,6 +26,7 @@ from ansys.geometry.core.errors import protect_grpc from ..base.points import GRPCPointsService +from .conversions import build_grpc_id, from_point3d_to_grpc_design_point class GRPCPointsServiceV1(GRPCPointsService): # pragma: no cover @@ -49,19 +50,17 @@ def __init__(self, channel: grpc.Channel): # noqa: D102 @protect_grpc def create_design_points(self, **kwargs) -> dict: # noqa: D102 - from ansys.api.discovery.v1.design.constructs.datumpoint_pb2_grpc import ( + from ansys.api.discovery.v1.design.constructs.datumpoint_pb2 import ( DatumPointCreationRequest, DatumPointCreationRequestData, ) - from .conversions import from_point3d_to_grpc_point - # Create the request - assumes all inputs are valid and of the proper type request = DatumPointCreationRequest( - requestData=[ + request_data=[ DatumPointCreationRequestData( - points=[from_point3d_to_grpc_point(point) for point in kwargs["points"]], - parent=kwargs["parent_id"], + points=[from_point3d_to_grpc_design_point(point) for point in kwargs["points"]], + parent_id=build_grpc_id(kwargs["parent_id"]), ) ] ) From 43e89d91e472cc32b7ebae21a194226f3ce4e370 Mon Sep 17 00:00:00 2001 From: Matteo Bini <91963243+b-matteo@users.noreply.github.com> Date: Wed, 3 Dec 2025 14:48:39 +0100 Subject: [PATCH 05/22] Update pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 92c59098f2..388b7f066f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ classifiers = [ ] dependencies = [ - "ansys-api-discovery==1.0.15", + "ansys-api-discovery==1.0.16", "ansys-tools-common>=0.3,<1", "beartype>=0.11.0,<0.23", "geomdl>=5,<6", From b3f744d86e84bca8a8ba5eb814faa2a986808bea Mon Sep 17 00:00:00 2001 From: Jacob Kerstetter Date: Wed, 3 Dec 2025 16:04:13 -0500 Subject: [PATCH 06/22] open_file fully working, insert not working --- .../core/_grpc/_services/v1/designs.py | 18 ++--- src/ansys/geometry/core/designer/design.py | 71 ++++++++++++++++--- src/ansys/geometry/core/misc/options.py | 2 +- src/ansys/geometry/core/modeler.py | 6 +- 4 files changed, 77 insertions(+), 20 deletions(-) diff --git a/src/ansys/geometry/core/_grpc/_services/v1/designs.py b/src/ansys/geometry/core/_grpc/_services/v1/designs.py index 5bf685ef98..516131c5f6 100644 --- a/src/ansys/geometry/core/_grpc/_services/v1/designs.py +++ b/src/ansys/geometry/core/_grpc/_services/v1/designs.py @@ -73,6 +73,7 @@ def open(self, **kwargs) -> dict: # noqa: D102 def request_generator( file_path: Path, + file_name: str, import_options: "ImportOptions", import_options_definitions: "ImportOptionsDefinitions", open_mode: OpenMode, @@ -87,7 +88,7 @@ def request_generator( while chunk := file.read(chunk_size): test_req = OpenRequest( data=chunk, - file_name=file_path.name, + file_name=file_name, open_mode=open_mode, import_named_selections=True, import_options=import_options.to_dict(), @@ -106,6 +107,7 @@ def request_generator( response = self.file_stub.Open( request_generator( file_path=kwargs["filepath"], + file_name=kwargs["original_file_name"], import_options=kwargs["import_options"], import_options_definitions=kwargs["import_options_definitions"], open_mode=open_mode @@ -155,11 +157,7 @@ def put_active(self, **kwargs) -> dict: # noqa: D102 @protect_grpc def save_as(self, **kwargs) -> dict: # noqa: D102 - raise NotImplementedError - - @protect_grpc - def download_export(self, **kwargs) -> dict: # noqa: D102 - from ansys.api.discovery.v1.commands.file_pb2 import SaveMode, SaveRequest + from ansys.api.discovery.v1.commands.file_pb2 import SaveRequest from .conversions import ( _check_write_body_facets_input, @@ -171,7 +169,6 @@ def download_export(self, **kwargs) -> dict: # noqa: D102 # Create the request - assumes all inputs are valid and of the proper type request = SaveRequest( format=from_design_file_format_to_grpc_file_export_format(kwargs["format"]), - save_mode=SaveMode.SAVEMODE_STANDARD, write_body_facets=kwargs["write_body_facets"], ) @@ -182,13 +179,18 @@ def download_export(self, **kwargs) -> dict: # noqa: D102 data = bytes() for response in response_stream: data += response.data + return { "data": data, } + @protect_grpc + def download_export(self, **kwargs) -> dict: # noqa: D102 + return self.save_as(**kwargs) + @protect_grpc def stream_download_export(self, **kwargs) -> dict: # noqa: D102 - raise NotImplementedError + return self.save_as(**kwargs) @protect_grpc def insert(self, **kwargs) -> dict: # noqa: D102 diff --git a/src/ansys/geometry/core/designer/design.py b/src/ansys/geometry/core/designer/design.py index cb7baa278d..aa8f898826 100644 --- a/src/ansys/geometry/core/designer/design.py +++ b/src/ansys/geometry/core/designer/design.py @@ -29,6 +29,7 @@ import numpy as np from pint import Quantity, UndefinedUnitError +from ansys.geometry.core._grpc._version import GeometryApiProtos from ansys.geometry.core.connection.backend import BackendType from ansys.geometry.core.designer.beam import ( Beam, @@ -58,7 +59,11 @@ min_backend_version, ) from ansys.geometry.core.misc.measurements import Distance -from ansys.geometry.core.misc.options import ImportOptions, TessellationOptions +from ansys.geometry.core.misc.options import ( + ImportOptions, + ImportOptionsDefinitions, + TessellationOptions, +) from ansys.geometry.core.modeler import Modeler from ansys.geometry.core.parameters.parameter import Parameter, ParameterUpdateStatus from ansys.geometry.core.shapes.curves.trimmed_curve import TrimmedCurve @@ -247,6 +252,7 @@ def save(self, file_location: Path | str, write_body_facets: bool = False) -> No filepath=file_location, write_body_facets=write_body_facets, backend_version=self._grpc_client.backend_version, + format=DesignFileFormat.SCDOCX, ) self._grpc_client.log.debug(f"Design successfully saved at location {file_location}.") @@ -988,6 +994,7 @@ def insert_file( self, file_location: Path | str, import_options: ImportOptions = ImportOptions(), + import_options_definitions: ImportOptionsDefinitions = ImportOptionsDefinitions(), ) -> Component: """Insert a file into the design. @@ -995,8 +1002,11 @@ def insert_file( ---------- file_location : ~pathlib.Path | str Location on disk where the file is located. - import_options : ImportOptions - The options to pass into upload file + import_options : ImportOptions, optional + The options to pass into upload file. If none are provided, default options are used. + import_options_definitions : ImportOptionsDefinitions, optional + Additional options to pass into insert file. If none are provided, default options + are used. Returns ------- @@ -1007,13 +1017,56 @@ def insert_file( -------- This method is only available starting on Ansys release 24R2. """ - # Upload the file to the server - filepath_server = self._modeler._upload_file(file_location, import_options=import_options) + # Upload the file to the server if using v0 protos + #if self._grpc_client.services.version == GeometryApiProtos.V0: + if False: + filepath_server = self._modeler._upload_file( + file_location, import_options=import_options + ) + + # Insert the file into the design + self._grpc_client.services.designs.insert( + filepath=filepath_server, + import_named_selections=import_options.import_named_selections, + ) + else: + # Zip file and pass filepath to service to open + import tempfile + from zipfile import ZipFile + + fp_path = Path(file_location).resolve() + + # Create a temporary zip file with the same name as the original file + temp_dir = Path(tempfile.gettempdir()) + temp_zip_path = temp_dir / f"{fp_path.stem}.zip" + + try: + # Create zip archive + with ZipFile(temp_zip_path, 'w') as zipf: + # Add the main file + zipf.write(fp_path, fp_path.name) + + # If it's an assembly format, add all files from the same directory + assembly_extensions = [".CATProduct", ".asm", ".solution", ".sldasm"] + if any(ext in str(file_location) for ext in assembly_extensions): + dir_path = fp_path.parent + for file in dir_path.iterdir(): + if file.is_file() and file != fp_path: + zipf.write(file, file.name) + + # Pass the zip file path to the service + self._grpc_client.services.designs.insert( + filepath=temp_zip_path, + original_file_name=fp_path.name, + import_options=import_options, + import_options_definitions=import_options_definitions + ) + + finally: + # Clean up the temporary zip file + if temp_zip_path.exists(): + temp_zip_path.unlink() - # Insert the file into the design - self._grpc_client.services.designs.insert( - filepath=filepath_server, import_named_selections=import_options.import_named_selections - ) self._grpc_client.log.debug(f"File {file_location} successfully inserted into design.") self._update_design_inplace() diff --git a/src/ansys/geometry/core/misc/options.py b/src/ansys/geometry/core/misc/options.py index 8a87ecb438..4f88dc780c 100644 --- a/src/ansys/geometry/core/misc/options.py +++ b/src/ansys/geometry/core/misc/options.py @@ -68,7 +68,7 @@ def to_dict(self): @dataclass class ImportOptionsDefinitions: - """Import options definitionswhen opening a file. + """Import options definitions when opening a file. Parameters ---------- diff --git a/src/ansys/geometry/core/modeler.py b/src/ansys/geometry/core/modeler.py index 561afd9dc6..441a1afc91 100644 --- a/src/ansys/geometry/core/modeler.py +++ b/src/ansys/geometry/core/modeler.py @@ -416,7 +416,8 @@ def open_file( # Format-specific logic - upload the whole containing folder for assemblies. If backend's # version is > 26.1.0 we're going to upload the file no matter what, as streaming is # supported. - if upload_to_server and self.client.services.version == GeometryApiProtos.V0: + # if upload_to_server and self.client.services.version == GeometryApiProtos.V0: + if False: fp_path = Path(file_path) file_size_kb = fp_path.stat().st_size if any( @@ -453,7 +454,7 @@ def open_file( # Create a temporary zip file with the same name as the original file temp_dir = Path(tempfile.gettempdir()) - temp_zip_path = temp_dir / fp_path.name + temp_zip_path = temp_dir / f"{fp_path.stem}.zip" try: # Create zip archive @@ -472,6 +473,7 @@ def open_file( # Pass the zip file path to the service self.client.services.designs.open( filepath=temp_zip_path, + original_file_name=fp_path.name, import_options=import_options, import_options_definitions=import_options_definitions, open_mode="new", From 6e6be142f30253a3a6e2facb2268a49cc9f8e7d3 Mon Sep 17 00:00:00 2001 From: Jacob Kerstetter Date: Thu, 4 Dec 2025 09:01:36 -0500 Subject: [PATCH 07/22] fixed frame conversion impl full design tessellation streaming --- .../core/_grpc/_services/v1/conversions.py | 6 +- .../core/_grpc/_services/v1/designs.py | 79 +++++++++++++++---- 2 files changed, 66 insertions(+), 19 deletions(-) diff --git a/src/ansys/geometry/core/_grpc/_services/v1/conversions.py b/src/ansys/geometry/core/_grpc/_services/v1/conversions.py index 5c241f33dd..52cd1fc571 100644 --- a/src/ansys/geometry/core/_grpc/_services/v1/conversions.py +++ b/src/ansys/geometry/core/_grpc/_services/v1/conversions.py @@ -398,9 +398,9 @@ def from_grpc_frame_to_frame(frame: GRPCFrame) -> "Frame": return Frame( Point3D( input=[ - frame.origin.x, - frame.origin.y, - frame.origin.z, + frame.origin.x.value_in_geometry_units, + frame.origin.y.value_in_geometry_units, + frame.origin.z.value_in_geometry_units, ], unit=DEFAULT_UNITS.SERVER_LENGTH, ), diff --git a/src/ansys/geometry/core/_grpc/_services/v1/designs.py b/src/ansys/geometry/core/_grpc/_services/v1/designs.py index 516131c5f6..86c113b629 100644 --- a/src/ansys/geometry/core/_grpc/_services/v1/designs.py +++ b/src/ansys/geometry/core/_grpc/_services/v1/designs.py @@ -138,7 +138,7 @@ def get_assembly(self, **kwargs) -> dict: # noqa: D102 from ansys.api.discovery.v1.design.designdoc_pb2 import GetAssemblyRequest # Create the request - assumes all inputs are valid and of the proper type - request = GetAssemblyRequest(id=build_grpc_id(kwargs["active_design"].get("design_id").id)) + request = GetAssemblyRequest(id=build_grpc_id(kwargs["active_design"].get("design_id"))) # Call the gRPC service response = self.designdoc_stub.GetAssembly(request) @@ -149,7 +149,16 @@ def get_assembly(self, **kwargs) -> dict: # noqa: D102 @protect_grpc def close(self, **kwargs) -> dict: # noqa: D102 - raise NotImplementedError + from ansys.api.discovery.v1.commands.file_pb2 import CloseRequest + + # Create the request - assumes all inputs are valid and of the proper type + request = CloseRequest(design_id=build_grpc_id(kwargs["design_id"])) + + # Call the gRPC service + _ = self.file_stub.Close(request) + + # Return the response - formatted as a dictionary + return {} @protect_grpc def put_active(self, **kwargs) -> dict: # noqa: D102 @@ -207,7 +216,7 @@ def get_active(self, **kwargs) -> dict: # noqa: D102 # Return the response - formatted as a dictionary if response.design: return { - "design_id": response.design.id, + "design_id": response.design.id.id, "main_part_id": response.design.main_part_id.id, "name": response.design.name, } @@ -222,11 +231,49 @@ def upload_file_stream(self, **kwargs) -> dict: # noqa: D102 @protect_grpc def stream_design_tessellation(self, **kwargs) -> dict: # noqa: D102 - raise NotImplementedError + from ansys.api.discovery.v1.design.designdoc_pb2 import DesignTessellationRequest + + from .conversions import ( + from_grpc_edge_tess_to_raw_data, + from_grpc_tess_to_raw_data, + from_tess_options_to_grpc_tess_options, + ) + + # If there are options, convert to gRPC options + options = ( + from_tess_options_to_grpc_tess_options(kwargs["options"]) + if kwargs["options"] is not None + else None + ) + + # Create the request - assumes all inputs are valid and of the proper type + request = DesignTessellationRequest( + options=options, + include_faces=kwargs["include_faces"], + include_edges=kwargs["include_edges"], + ) + + # Call the gRPC service + response_stream = self.designdoc_stub.StreamDesignTessellation(request) + + # Return the response - formatted as a dictionary + tess_map = {} + for elem in response_stream: + for body_id, body_tess in elem.body_tessellation.items(): + tess = {} + for face_id, face_tess in body_tess.face_tessellation.items(): + tess[face_id] = from_grpc_tess_to_raw_data(face_tess) + for edge_id, edge_tess in body_tess.edge_tessellation.items(): + tess[edge_id] = from_grpc_edge_tess_to_raw_data(edge_tess) + tess_map[body_id] = tess + + return { + "tessellation": tess_map, + } @protect_grpc def download_file(self, **kwargs) -> dict: # noqa: D102 - raise NotImplementedError + return self.save_as(**kwargs) def _serialize_assembly_response(self, response): def serialize_body(body): @@ -339,7 +386,7 @@ def serialize_beam_curve_list(curve_list): def serialize_beam_cross_section(cross_section): return { "section_anchor": cross_section.section_anchor, - "section_angle": cross_section.section_angle, + "section_angle": cross_section.section_angle.value_in_geometry_units, "section_frame": from_grpc_frame_to_frame(cross_section.section_frame), "section_profile": [ serialize_beam_curve_list(curve_list) @@ -349,16 +396,16 @@ def serialize_beam_cross_section(cross_section): def serialize_beam_properties(properties): return { - "area": properties.area, - "centroid_x": properties.centroid_x, - "centroid_y": properties.centroid_y, - "warping_constant": properties.warping_constant, - "ixx": properties.ixx, - "ixy": properties.ixy, - "iyy": properties.iyy, - "shear_center_x": properties.shear_center_x, - "shear_center_y": properties.shear_center_y, - "torsional_constant": properties.torsional_constant, + "area": properties.area.value_in_geometry_units, + "centroid_x": properties.centroid_x.value_in_geometry_units, + "centroid_y": properties.centroid_y.value_in_geometry_units, + "warping_constant": properties.warping_constant.value_in_geometry_units, + "ixx": properties.ixx.value_in_geometry_units, + "ixy": properties.ixy.value_in_geometry_units, + "iyy": properties.iyy.value_in_geometry_units, + "shear_center_x": properties.shear_center_x.value_in_geometry_units, + "shear_center_y": properties.shear_center_y.value_in_geometry_units, + "torsional_constant": properties.torsional_constant.value_in_geometry_units, } def serialize_beam(beam): From f36baf54ec6cf0d197dbef63d640e7ca0f98f3b2 Mon Sep 17 00:00:00 2001 From: Jacob Kerstetter Date: Thu, 4 Dec 2025 13:15:27 -0500 Subject: [PATCH 08/22] minor fixes in command_script implementing open_file, save, and insert and routing v1 calls to them tests that require the use of uploading garbage files are checking for errors thrown --- .../_grpc/_services/v1/commands_script.py | 8 +- .../core/_grpc/_services/v1/designs.py | 1 - src/ansys/geometry/core/designer/design.py | 3 +- src/ansys/geometry/core/modeler.py | 80 ++++++++++++------- tests/integration/test_design.py | 50 +++++++++--- tests/integration/test_runscript.py | 11 +++ 6 files changed, 104 insertions(+), 49 deletions(-) diff --git a/src/ansys/geometry/core/_grpc/_services/v1/commands_script.py b/src/ansys/geometry/core/_grpc/_services/v1/commands_script.py index 1e482cbeb6..c060fd79c6 100644 --- a/src/ansys/geometry/core/_grpc/_services/v1/commands_script.py +++ b/src/ansys/geometry/core/_grpc/_services/v1/commands_script.py @@ -43,13 +43,13 @@ class GRPCCommandsScriptServiceV1(GRPCCommandsScriptService): # pragma: no cove @protect_grpc def __init__(self, channel: grpc.Channel): # noqa: D102 - from ansys.api.discovery.v1.commands.script_pb2 import ScriptStub + from ansys.api.discovery.v1.commands.script_pb2_grpc import ScriptStub self.stub = ScriptStub(channel) @protect_grpc def run_script_file(self, **kwargs) -> dict: # noqa: D102 - from aansys.api.discovery.v1.commands.script_pb2 import RunScriptFileRequest + from ansys.api.discovery.v1.commands.script_pb2 import RunScriptFileRequest # Create the request - assumes all inputs are valid and of the proper type request = RunScriptFileRequest( @@ -63,7 +63,7 @@ def run_script_file(self, **kwargs) -> dict: # noqa: D102 # Return the response - formatted as a dictionary return { - "success": response.success, - "message": response.message, + "success": response.command_response.success, + "message": response.command_response.message, "values": None if not response.values else dict(response.values), } diff --git a/src/ansys/geometry/core/_grpc/_services/v1/designs.py b/src/ansys/geometry/core/_grpc/_services/v1/designs.py index 86c113b629..dbb98f6713 100644 --- a/src/ansys/geometry/core/_grpc/_services/v1/designs.py +++ b/src/ansys/geometry/core/_grpc/_services/v1/designs.py @@ -90,7 +90,6 @@ def request_generator( data=chunk, file_name=file_name, open_mode=open_mode, - import_named_selections=True, import_options=import_options.to_dict(), import_options_definitions=from_import_options_definitions_to_grpc_import_options_definition(import_options_definitions), ) diff --git a/src/ansys/geometry/core/designer/design.py b/src/ansys/geometry/core/designer/design.py index aa8f898826..34b62ec761 100644 --- a/src/ansys/geometry/core/designer/design.py +++ b/src/ansys/geometry/core/designer/design.py @@ -1018,8 +1018,7 @@ def insert_file( This method is only available starting on Ansys release 24R2. """ # Upload the file to the server if using v0 protos - #if self._grpc_client.services.version == GeometryApiProtos.V0: - if False: + if self._grpc_client.services.version == GeometryApiProtos.V0: filepath_server = self._modeler._upload_file( file_location, import_options=import_options ) diff --git a/src/ansys/geometry/core/modeler.py b/src/ansys/geometry/core/modeler.py index 441a1afc91..c8ce75295f 100644 --- a/src/ansys/geometry/core/modeler.py +++ b/src/ansys/geometry/core/modeler.py @@ -294,28 +294,35 @@ def _upload_file( This method creates a file on the server that has the same name and extension as the file on the client. """ - from pathlib import Path + if self.client.services.version == GeometryApiProtos.V0: + from pathlib import Path - fp_path = Path(file_path).resolve() + fp_path = Path(file_path).resolve() - if not fp_path.exists(): - raise ValueError(f"Could not find file: {file_path}") - if fp_path.is_dir(): - raise ValueError("File path must lead to a file, not a directory.") + if not fp_path.exists(): + raise ValueError(f"Could not find file: {file_path}") + if fp_path.is_dir(): + raise ValueError("File path must lead to a file, not a directory.") - file_name = fp_path.name + file_name = fp_path.name - with fp_path.open(mode="rb") as file: - data = file.read() + with fp_path.open(mode="rb") as file: + data = file.read() - response = self.client.services.designs.upload_file( - data=data, - file_name=file_name, - open_file=open_file, - import_options=import_options, - ) + response = self.client.services.designs.upload_file( + data=data, + file_name=file_name, + open_file=open_file, + import_options=import_options, + ) - return response.get("file_path") + return response.get("file_path") + else: + raise GeometryRuntimeError( + "The '_upload_file' method is not supported in protos v1 and beyond. " + "Use 'modeler.open_file()' to open files or 'design.insert_file()' " + "to insert files into an existing design." + ) def _upload_file_stream( self, @@ -344,20 +351,27 @@ def _upload_file_stream( This method creates a file on the server that has the same name and extension as the file on the client. """ - from pathlib import Path + if self.client.services.version == GeometryApiProtos.V0: + from pathlib import Path - fp_path = Path(file_path).resolve() + fp_path = Path(file_path).resolve() - if not fp_path.exists(): - raise ValueError(f"Could not find file: {file_path}") - if fp_path.is_dir(): - raise ValueError("File path must lead to a file, not a directory.") + if not fp_path.exists(): + raise ValueError(f"Could not find file: {file_path}") + if fp_path.is_dir(): + raise ValueError("File path must lead to a file, not a directory.") - response = self.client.services.designs.upload_file_stream( - file_path=fp_path, open_file=open_file, import_options=import_options - ) + response = self.client.services.designs.upload_file_stream( + file_path=fp_path, open_file=open_file, import_options=import_options + ) - return response.get("file_path") + return response.get("file_path") + else: + raise GeometryRuntimeError( + "The '_upload_file_stream' method is not supported with protos v1 and beyond. " + "Use 'modeler.open_file()' to open files or 'design.insert_file()' " + "to insert files into an existing design." + ) def open_file( self, @@ -416,8 +430,7 @@ def open_file( # Format-specific logic - upload the whole containing folder for assemblies. If backend's # version is > 26.1.0 we're going to upload the file no matter what, as streaming is # supported. - # if upload_to_server and self.client.services.version == GeometryApiProtos.V0: - if False: + if upload_to_server and self.client.services.version == GeometryApiProtos.V0: fp_path = Path(file_path) file_size_kb = fp_path.stat().st_size if any( @@ -584,7 +597,16 @@ def run_discovery_script_file( # but this method has been tested independently api_version = ApiVersions.parse_input(api_version) - serv_path = self._upload_file(file_path) + # Prepare the script path + #f self.client.services.version == GeometryApiProtos.V0: + serv_path = self._upload_file(file_path) + else: + # Check if a design exists + if self._design is None or not self._design.is_active: + raise GeometryRuntimeError( + "No active design available. Create or open a design before running a script." + ) + serv_path = file_path self.client.log.debug(f"Running Discovery script file at {file_path}...") response = self.client.services.commands_script.run_script_file( diff --git a/tests/integration/test_design.py b/tests/integration/test_design.py index 53d17c3971..e8ba6d3d48 100644 --- a/tests/integration/test_design.py +++ b/tests/integration/test_design.py @@ -31,6 +31,7 @@ import pytest from ansys.geometry.core import Modeler +from ansys.geometry.core._grpc._version import GeometryApiProtos from ansys.geometry.core.connection import BackendType import ansys.geometry.core.connection.defaults as pygeom_defaults from ansys.geometry.core.designer import ( @@ -86,18 +87,31 @@ def test_error_opening_file(modeler: Modeler, tmp_path_factory: pytest.TempPathFactory): """Validating error messages when opening up files""" - fake_file_path = Path("C:\\Users\\FakeUser\\Documents\\FakeProject\\FakeFile.scdocx") - with pytest.raises(ValueError, match="Could not find file:"): - modeler._upload_file(fake_file_path) - file = tmp_path_factory.mktemp("test_design") - with pytest.raises(ValueError, match="File path must lead to a file, not a directory"): - modeler._upload_file(file) - fake_file_path = Path("C:\\Users\\FakeUser\\Documents\\FakeProject\\FakeFile.scdocx") - with pytest.raises(ValueError, match="Could not find file:"): - modeler._upload_file_stream(fake_file_path) - file = tmp_path_factory.mktemp("test_design") - with pytest.raises(ValueError, match="File path must lead to a file, not a directory"): - modeler._upload_file_stream(file) + # If the protos version is v1 or higher, uploading files is not supported + if modeler.client.services.version != GeometryApiProtos.V0: + fake_path = Path("C:\\Users\\FakeUser\\Documents\\FakeProject\\FakeFile.scdocx") + with pytest.raises( + GeometryRuntimeError, + match="The '_upload_file' method is not supported in backend v1 and beyond.", + ): + modeler._upload_file(fake_path) + with pytest.raises( + GeometryRuntimeError, + match="The '_upload_file_stream' method is not supported with backend v1 and beyond.", + ): + modeler._upload_file_stream(fake_path) + else: + fake_path = Path("C:\\Users\\FakeUser\\Documents\\FakeProject\\FakeFile.scdocx") + temp_dir = tmp_path_factory.mktemp("test_design") + + with pytest.raises(ValueError, match="Could not find file:"): + modeler._upload_file(fake_path) + with pytest.raises(ValueError, match="File path must lead to a file, not a directory"): + modeler._upload_file(temp_dir) + with pytest.raises(ValueError, match="Could not find file:"): + modeler._upload_file_stream(fake_path) + with pytest.raises(ValueError, match="File path must lead to a file, not a directory"): + modeler._upload_file_stream(temp_dir) def test_modeler_open_files(modeler: Modeler): @@ -1334,7 +1348,14 @@ def test_stream_upload_file(tmp_path_factory: pytest.TempPathFactory, transport_ from ansys.geometry.core import Modeler modeler = Modeler(transport_mode=transport_mode) - path_on_server = modeler._upload_file_stream(file) + if modeler.client.services.version == GeometryApiProtos.V0: + path_on_server = modeler._upload_file_stream(file) + else: + with pytest.raises( + GeometryRuntimeError, + match="The '_upload_file_stream' method is not supported with backend v1 and beyond.", + ): + modeler._upload_file_stream(file) assert path_on_server is not None finally: pygeom_defaults.MAX_MESSAGE_LENGTH = old_value @@ -4059,6 +4080,9 @@ def test_legacy_export_download( modeler: Modeler, tmp_path_factory: pytest.TempPathFactory, use_grpc_client_old_backend: Modeler ): # Test is meant to add test coverage for using an old backend to export and download + if modeler.client.services.version != GeometryApiProtos.V0: + pytest.skip("Test only applies to v0 backend") + # Creating the directory and file to export working_directory = tmp_path_factory.mktemp("test_import_export_reimport") original_file = Path(FILES_DIR, "reactorWNS.scdocx") diff --git a/tests/integration/test_runscript.py b/tests/integration/test_runscript.py index 5c63f2ee15..3d4035e04e 100644 --- a/tests/integration/test_runscript.py +++ b/tests/integration/test_runscript.py @@ -25,6 +25,7 @@ import pytest from ansys.geometry.core import Modeler +from ansys.geometry.core._grpc._version import GeometryApiProtos from ansys.geometry.core.connection.backend import ApiVersions, BackendType from ansys.geometry.core.errors import GeometryRuntimeError from ansys.geometry.core.math.point import Point2D @@ -36,6 +37,8 @@ # Python (.py) @pytest.mark.skip(reason="New failure to be investigated.") def test_python_simple_script(modeler: Modeler): + if modeler.client.services.version != GeometryApiProtos.V0: + modeler.create_design("test_design") result, _ = modeler.run_discovery_script_file(DSCOSCRIPTS_FILES_DIR / "simple_script.py") pattern_db = re.compile(r"SpaceClaim\.Api\.[A-Za-z0-9]+\.DesignBody", re.IGNORECASE) pattern_doc = re.compile(r"SpaceClaim\.Api\.[A-Za-z0-9]+\.Document", re.IGNORECASE) @@ -48,6 +51,8 @@ def test_python_simple_script(modeler: Modeler): def test_python_simple_script_ignore_api_version( modeler: Modeler, caplog: pytest.LogCaptureFixture ): + if modeler.client.services.version != GeometryApiProtos.V0: + modeler.create_design("test_design") result, _ = modeler.run_discovery_script_file( DSCOSCRIPTS_FILES_DIR / "simple_script.py", api_version=ApiVersions.LATEST, @@ -69,6 +74,8 @@ def test_python_simple_script_ignore_api_version( def test_python_failing_script(modeler: Modeler): if modeler.client.backend_type == BackendType.CORE_LINUX: pytest.skip(reason="Skipping test_python_failing_script. Operation fails on github.") + if modeler.client.services.version != GeometryApiProtos.V0: + modeler.create_design("test_design") with pytest.raises(GeometryRuntimeError): modeler.run_discovery_script_file(DSCOSCRIPTS_FILES_DIR / "failing_script.py") @@ -94,6 +101,8 @@ def test_python_integrated_script(modeler: Modeler): # SpaceClaim (.scscript) def test_scscript_simple_script(modeler: Modeler): + if modeler.client.services.version != GeometryApiProtos.V0: + modeler.create_design("test_design") result, _ = modeler.run_discovery_script_file(DSCOSCRIPTS_FILES_DIR / "simple_script.scscript") assert len(result) == 2 pattern_db = re.compile(r"SpaceClaim\.Api\.[A-Za-z0-9]+\.DesignBody", re.IGNORECASE) @@ -105,6 +114,8 @@ def test_scscript_simple_script(modeler: Modeler): # Discovery (.dscript) def test_dscript_simple_script(modeler: Modeler): + if modeler.client.services.version != GeometryApiProtos.V0: + modeler.create_design("test_design") result, _ = modeler.run_discovery_script_file(DSCOSCRIPTS_FILES_DIR / "simple_script.dscript") assert len(result) == 2 pattern_db = re.compile(r"SpaceClaim\.Api\.[A-Za-z0-9]+\.DesignBody", re.IGNORECASE) From cba23e24410ad5562e8d00c55843f558f03c91c8 Mon Sep 17 00:00:00 2001 From: Jacob Kerstetter Date: Thu, 4 Dec 2025 13:32:45 -0500 Subject: [PATCH 09/22] fixing if statements to run correct versions --- src/ansys/geometry/core/modeler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ansys/geometry/core/modeler.py b/src/ansys/geometry/core/modeler.py index c8ce75295f..dfba153ed9 100644 --- a/src/ansys/geometry/core/modeler.py +++ b/src/ansys/geometry/core/modeler.py @@ -442,7 +442,7 @@ def open_file( if full_path != fp_path: if full_path.stat().st_size < pygeom_defaults.MAX_MESSAGE_LENGTH: self._upload_file(full_path) - elif self.client.backend_version == (25, 2, 0): + elif self.client.backend_version >= (25, 2, 0): self._upload_file_stream(full_path) else: # pragma: no cover raise RuntimeError( @@ -598,7 +598,7 @@ def run_discovery_script_file( api_version = ApiVersions.parse_input(api_version) # Prepare the script path - #f self.client.services.version == GeometryApiProtos.V0: + if self.client.services.version == GeometryApiProtos.V0: serv_path = self._upload_file(file_path) else: # Check if a design exists From 320e50110a07334c38fca2a1761f13ab5d42a10d Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Thu, 4 Dec 2025 18:35:27 +0000 Subject: [PATCH 10/22] chore: adding changelog file 2443.maintenance.md [dependabot-skip] --- doc/changelog.d/2443.maintenance.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changelog.d/2443.maintenance.md diff --git a/doc/changelog.d/2443.maintenance.md b/doc/changelog.d/2443.maintenance.md new file mode 100644 index 0000000000..c07e008acb --- /dev/null +++ b/doc/changelog.d/2443.maintenance.md @@ -0,0 +1 @@ +Chore: v1 implementation of file/designs stub From db9f0d4291056e02457d85279b368487a64aa5bd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 4 Dec 2025 18:35:32 +0000 Subject: [PATCH 11/22] chore: auto fixes from pre-commit hooks --- .../core/_grpc/_services/v1/conversions.py | 47 ++++++++++--------- .../core/_grpc/_services/v1/designs.py | 14 +++--- .../core/_grpc/_services/v1/unsupported.py | 2 +- src/ansys/geometry/core/designer/design.py | 4 +- src/ansys/geometry/core/modeler.py | 2 +- tests/integration/test_design.py | 2 +- 6 files changed, 36 insertions(+), 35 deletions(-) diff --git a/src/ansys/geometry/core/_grpc/_services/v1/conversions.py b/src/ansys/geometry/core/_grpc/_services/v1/conversions.py index 52cd1fc571..06088b7d55 100644 --- a/src/ansys/geometry/core/_grpc/_services/v1/conversions.py +++ b/src/ansys/geometry/core/_grpc/_services/v1/conversions.py @@ -1427,12 +1427,10 @@ def from_import_options_definitions_to_grpc_import_options_definition( """ definitions = {} for key, definition in import_options_definitions.to_dict().items(): - definitions[key] = GRPCImportOptionDefinition( - string_option=str(definition) - ) - + definitions[key] = GRPCImportOptionDefinition(string_option=str(definition)) + return definitions - + def _nurbs_curves_compatibility(backend_version: "semver.Version", grpc_geometries: GRPCGeometries): """Check if the backend version is compatible with NURBS curves in sketches. @@ -1576,24 +1574,27 @@ def serialize_entity_identifier(entity): ], } + def _check_write_body_facets_input(backend_version: "semver.Version", write_body_facets: bool): - """Check if the backend version is compatible with NURBS curves in sketches. - - Parameters - ---------- - backend_version : semver.Version - The version of the backend. - write_body_facets : bool - Option to write out body facets. - """ - if write_body_facets and backend_version < (26, 1, 0): - from ansys.geometry.core.logger import LOG - - LOG.warning( - "The usage of write_body_facets requires a minimum Ansys release version of " - + "26.1.0, but the current version used is " - + f"{backend_version}." - ) + """Check if the backend version is compatible with NURBS curves in sketches. + + Parameters + ---------- + backend_version : semver.Version + The version of the backend. + write_body_facets : bool + Option to write out body facets. + """ + if write_body_facets and backend_version < (26, 1, 0): + from ansys.geometry.core.logger import LOG + + LOG.warning( + "The usage of write_body_facets requires a minimum Ansys release version of " + + "26.1.0, but the current version used is " + + f"{backend_version}." + ) + + def get_standard_tracker_response(response) -> dict: """Get a standard dictionary response from a TrackerCommandResponse gRPC object. @@ -1630,4 +1631,4 @@ def get_tracker_response_with_created_bodies(response) -> dict: serialized_response["created_bodies"] = serialized_response["tracker_response"].get( "created_bodies", [] ) - return serialized_response \ No newline at end of file + return serialized_response diff --git a/src/ansys/geometry/core/_grpc/_services/v1/designs.py b/src/ansys/geometry/core/_grpc/_services/v1/designs.py index dbb98f6713..0fb9c5c9c6 100644 --- a/src/ansys/geometry/core/_grpc/_services/v1/designs.py +++ b/src/ansys/geometry/core/_grpc/_services/v1/designs.py @@ -91,7 +91,9 @@ def request_generator( file_name=file_name, open_mode=open_mode, import_options=import_options.to_dict(), - import_options_definitions=from_import_options_definitions_to_grpc_import_options_definition(import_options_definitions), + import_options_definitions=from_import_options_definitions_to_grpc_import_options_definition( + import_options_definitions + ), ) yield test_req @@ -109,7 +111,7 @@ def request_generator( file_name=kwargs["original_file_name"], import_options=kwargs["import_options"], import_options_definitions=kwargs["import_options_definitions"], - open_mode=open_mode + open_mode=open_mode, ) ) @@ -187,7 +189,7 @@ def save_as(self, **kwargs) -> dict: # noqa: D102 data = bytes() for response in response_stream: data += response.data - + return { "data": data, } @@ -273,7 +275,7 @@ def stream_design_tessellation(self, **kwargs) -> dict: # noqa: D102 @protect_grpc def download_file(self, **kwargs) -> dict: # noqa: D102 return self.save_as(**kwargs) - + def _serialize_assembly_response(self, response): def serialize_body(body): return { @@ -329,8 +331,6 @@ def serialize_named_selection(named_selection): return {"id": named_selection.id.id, "name": named_selection.name} def serialize_coordinate_systems(coordinate_systems): - - serialized_cs = [] for cs in coordinate_systems.coordinate_systems: serialized_cs.append( @@ -456,4 +456,4 @@ def serialize_design_point(design_point): ), "beams": [serialize_beam(beam) for beam in beams], "design_points": [serialize_design_point(dp) for dp in design_points], - } \ No newline at end of file + } diff --git a/src/ansys/geometry/core/_grpc/_services/v1/unsupported.py b/src/ansys/geometry/core/_grpc/_services/v1/unsupported.py index 4d0c7ba651..6d6afe90db 100644 --- a/src/ansys/geometry/core/_grpc/_services/v1/unsupported.py +++ b/src/ansys/geometry/core/_grpc/_services/v1/unsupported.py @@ -81,7 +81,7 @@ def set_export_ids(self, **kwargs) -> dict: # noqa: D102 return {} @protect_grpc - def set_single_export_id(self, **kwargs) -> dict: # noqa: D102 + def set_single_export_id(self, **kwargs) -> dict: # noqa: D102 # Create the request - assumes all inputs are valid and of the proper type request = SetExportIdRequest( export_data=[ diff --git a/src/ansys/geometry/core/designer/design.py b/src/ansys/geometry/core/designer/design.py index 34b62ec761..7afe91d0f1 100644 --- a/src/ansys/geometry/core/designer/design.py +++ b/src/ansys/geometry/core/designer/design.py @@ -1041,7 +1041,7 @@ def insert_file( try: # Create zip archive - with ZipFile(temp_zip_path, 'w') as zipf: + with ZipFile(temp_zip_path, "w") as zipf: # Add the main file zipf.write(fp_path, fp_path.name) @@ -1058,7 +1058,7 @@ def insert_file( filepath=temp_zip_path, original_file_name=fp_path.name, import_options=import_options, - import_options_definitions=import_options_definitions + import_options_definitions=import_options_definitions, ) finally: diff --git a/src/ansys/geometry/core/modeler.py b/src/ansys/geometry/core/modeler.py index dfba153ed9..e3f16246f1 100644 --- a/src/ansys/geometry/core/modeler.py +++ b/src/ansys/geometry/core/modeler.py @@ -471,7 +471,7 @@ def open_file( try: # Create zip archive - with ZipFile(temp_zip_path, 'w') as zipf: + with ZipFile(temp_zip_path, "w") as zipf: # Add the main file zipf.write(fp_path, fp_path.name) diff --git a/tests/integration/test_design.py b/tests/integration/test_design.py index e8ba6d3d48..903a1dad84 100644 --- a/tests/integration/test_design.py +++ b/tests/integration/test_design.py @@ -103,7 +103,7 @@ def test_error_opening_file(modeler: Modeler, tmp_path_factory: pytest.TempPathF else: fake_path = Path("C:\\Users\\FakeUser\\Documents\\FakeProject\\FakeFile.scdocx") temp_dir = tmp_path_factory.mktemp("test_design") - + with pytest.raises(ValueError, match="Could not find file:"): modeler._upload_file(fake_path) with pytest.raises(ValueError, match="File path must lead to a file, not a directory"): From e57731f4fe3302a83879d8ba132cf348eb19eed2 Mon Sep 17 00:00:00 2001 From: Jacob Kerstetter Date: Thu, 4 Dec 2025 15:05:33 -0500 Subject: [PATCH 12/22] adding noqa to long line --- tests/integration/test_design.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/integration/test_design.py b/tests/integration/test_design.py index e8ba6d3d48..9098554a34 100644 --- a/tests/integration/test_design.py +++ b/tests/integration/test_design.py @@ -97,7 +97,9 @@ def test_error_opening_file(modeler: Modeler, tmp_path_factory: pytest.TempPathF modeler._upload_file(fake_path) with pytest.raises( GeometryRuntimeError, - match="The '_upload_file_stream' method is not supported with backend v1 and beyond.", + match=( + "The '_upload_file_stream' method is not supported with backend v1 and beyond." + ), ): modeler._upload_file_stream(fake_path) else: @@ -1353,7 +1355,7 @@ def test_stream_upload_file(tmp_path_factory: pytest.TempPathFactory, transport_ else: with pytest.raises( GeometryRuntimeError, - match="The '_upload_file_stream' method is not supported with backend v1 and beyond.", + match="The '_upload_file_stream' method is not supported with backend v1 and beyond.", # noqa: E501 ): modeler._upload_file_stream(file) assert path_on_server is not None From 7710d06a54da7912680082274765806038413418 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 4 Dec 2025 20:06:14 +0000 Subject: [PATCH 13/22] chore: auto fixes from pre-commit hooks --- tests/integration/test_design.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/integration/test_design.py b/tests/integration/test_design.py index 5a2f0f6f4f..fe4c7e342d 100644 --- a/tests/integration/test_design.py +++ b/tests/integration/test_design.py @@ -97,9 +97,7 @@ def test_error_opening_file(modeler: Modeler, tmp_path_factory: pytest.TempPathF modeler._upload_file(fake_path) with pytest.raises( GeometryRuntimeError, - match=( - "The '_upload_file_stream' method is not supported with backend v1 and beyond." - ), + match=("The '_upload_file_stream' method is not supported with backend v1 and beyond."), ): modeler._upload_file_stream(fake_path) else: From 3a86f7df9ce0e484dad44760974ce3eb3bb32864 Mon Sep 17 00:00:00 2001 From: Jacob Kerstetter Date: Fri, 5 Dec 2025 08:38:23 -0500 Subject: [PATCH 14/22] removing redef of body facets function --- .../core/_grpc/_services/v1/conversions.py | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/src/ansys/geometry/core/_grpc/_services/v1/conversions.py b/src/ansys/geometry/core/_grpc/_services/v1/conversions.py index 06088b7d55..3f9fcddb14 100644 --- a/src/ansys/geometry/core/_grpc/_services/v1/conversions.py +++ b/src/ansys/geometry/core/_grpc/_services/v1/conversions.py @@ -1575,26 +1575,6 @@ def serialize_entity_identifier(entity): } -def _check_write_body_facets_input(backend_version: "semver.Version", write_body_facets: bool): - """Check if the backend version is compatible with NURBS curves in sketches. - - Parameters - ---------- - backend_version : semver.Version - The version of the backend. - write_body_facets : bool - Option to write out body facets. - """ - if write_body_facets and backend_version < (26, 1, 0): - from ansys.geometry.core.logger import LOG - - LOG.warning( - "The usage of write_body_facets requires a minimum Ansys release version of " - + "26.1.0, but the current version used is " - + f"{backend_version}." - ) - - def get_standard_tracker_response(response) -> dict: """Get a standard dictionary response from a TrackerCommandResponse gRPC object. From 9e565e3cc04117bfbd9a8e3b3e0680d0998388ec Mon Sep 17 00:00:00 2001 From: Jacob Kerstetter Date: Mon, 8 Dec 2025 07:21:41 -0500 Subject: [PATCH 15/22] fixing tessellation --- .../core/_grpc/_services/v1/bodies.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/ansys/geometry/core/_grpc/_services/v1/bodies.py b/src/ansys/geometry/core/_grpc/_services/v1/bodies.py index e00b65d1f5..973462902e 100644 --- a/src/ansys/geometry/core/_grpc/_services/v1/bodies.py +++ b/src/ansys/geometry/core/_grpc/_services/v1/bodies.py @@ -31,6 +31,8 @@ from .conversions import ( build_grpc_id, from_frame_to_grpc_frame, + from_grpc_edge_tess_to_pd, + from_grpc_edge_tess_to_raw_data, from_grpc_point_to_point3d, from_grpc_tess_to_pd, from_grpc_tess_to_raw_data, @@ -1359,4 +1361,19 @@ def get_full_tessellation(self, **kwargs): # noqa: D102 resp = self.stub.GetTessellationStream(request=request) # Return the response - formatted as a dictionary - return {"tessellation": from_grpc_tess_to_pd(resp)} + tess_map = {} + for elem in resp: + for face_id, face_tess in elem.response_data[0].face_tessellation.items(): + tess_map[face_id] = ( + from_grpc_tess_to_raw_data(face_tess) + if kwargs["raw_data"] + else from_grpc_tess_to_pd(face_tess) + ) + for edge_id, edge_tess in elem.response_data[0].edge_tessellation.items(): + tess_map[edge_id] = ( + from_grpc_edge_tess_to_raw_data(edge_tess) + if kwargs["raw_data"] + else from_grpc_edge_tess_to_pd(edge_tess) + ) + + return {"tessellation": tess_map} From 199f56aabea2087ed028badb38bc6b4fe672d527 Mon Sep 17 00:00:00 2001 From: Jacob Kerstetter Date: Mon, 8 Dec 2025 07:24:30 -0500 Subject: [PATCH 16/22] removing duplicate conversion --- .../core/_grpc/_services/v1/conversions.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/ansys/geometry/core/_grpc/_services/v1/conversions.py b/src/ansys/geometry/core/_grpc/_services/v1/conversions.py index 5405e411f9..dd9098738b 100644 --- a/src/ansys/geometry/core/_grpc/_services/v1/conversions.py +++ b/src/ansys/geometry/core/_grpc/_services/v1/conversions.py @@ -258,24 +258,6 @@ def from_point3d_to_grpc_design_point(point: "Point3D") -> GRPCDesignPoint: ) -def from_point3d_to_grpc_design_point(point: "Point3D") -> GRPCDesignPoint: - """Convert a ``Point3D`` class to a design point gRPC message. - - Parameters - ---------- - point : Point3D - Source point data. - - Returns - ------- - GRPCDesignPoint - Geometry service gRPC design point message. The unit is meters. - """ - return GRPCDesignPoint( - position=from_point3d_to_grpc_point(point), - ) - - def from_unit_vector_to_grpc_direction(unit_vector: "UnitVector3D") -> GRPCDirection: """Convert a ``UnitVector3D`` class to a unit vector gRPC message. From d4567b3102fb4b7dd7af0bb6e44d8c5cae905b36 Mon Sep 17 00:00:00 2001 From: Jacob Kerstetter Date: Mon, 8 Dec 2025 07:57:36 -0500 Subject: [PATCH 17/22] putting reused code into auxiliary function --- src/ansys/geometry/core/designer/design.py | 21 ++---------- src/ansys/geometry/core/misc/auxiliary.py | 37 ++++++++++++++++++++++ src/ansys/geometry/core/modeler.py | 22 ++----------- 3 files changed, 42 insertions(+), 38 deletions(-) diff --git a/src/ansys/geometry/core/designer/design.py b/src/ansys/geometry/core/designer/design.py index 7afe91d0f1..59ddeab801 100644 --- a/src/ansys/geometry/core/designer/design.py +++ b/src/ansys/geometry/core/designer/design.py @@ -54,6 +54,7 @@ from ansys.geometry.core.math.plane import Plane from ansys.geometry.core.math.point import Point3D from ansys.geometry.core.math.vector import UnitVector3D, Vector3D +from ansys.geometry.core.misc.auxiliary import write_zip_file from ansys.geometry.core.misc.checks import ( ensure_design_is_active, min_backend_version, @@ -1030,28 +1031,10 @@ def insert_file( ) else: # Zip file and pass filepath to service to open - import tempfile - from zipfile import ZipFile - fp_path = Path(file_location).resolve() - # Create a temporary zip file with the same name as the original file - temp_dir = Path(tempfile.gettempdir()) - temp_zip_path = temp_dir / f"{fp_path.stem}.zip" - try: - # Create zip archive - with ZipFile(temp_zip_path, "w") as zipf: - # Add the main file - zipf.write(fp_path, fp_path.name) - - # If it's an assembly format, add all files from the same directory - assembly_extensions = [".CATProduct", ".asm", ".solution", ".sldasm"] - if any(ext in str(file_location) for ext in assembly_extensions): - dir_path = fp_path.parent - for file in dir_path.iterdir(): - if file.is_file() and file != fp_path: - zipf.write(file, file.name) + temp_zip_path = write_zip_file(fp_path) # Pass the zip file path to the service self._grpc_client.services.designs.insert( diff --git a/src/ansys/geometry/core/misc/auxiliary.py b/src/ansys/geometry/core/misc/auxiliary.py index 2ddfbf73c0..b47b9b565c 100644 --- a/src/ansys/geometry/core/misc/auxiliary.py +++ b/src/ansys/geometry/core/misc/auxiliary.py @@ -21,6 +21,7 @@ # SOFTWARE. """Auxiliary functions for the PyAnsys Geometry library.""" +from pathlib import Path from typing import TYPE_CHECKING, Union if TYPE_CHECKING: # pragma: no cover @@ -399,3 +400,39 @@ def convert_opacity_to_hex(opacity: float) -> str: raise ValueError("Opacity value must be between 0 and 1.") except ValueError as err: raise ValueError(f"Invalid color value: {err}") + + +def write_zip_file(file_path: Path) -> Path: + """Create a zip file from the given file path. + + Parameters + ---------- + file_path : str + The path to the file to be zipped. + + Returns + ------- + Path + The path to the created zip file. + """ + import tempfile + from zipfile import ZipFile + + # Create a temporary zip file with the same name as the original file + temp_dir = Path(tempfile.gettempdir()) + temp_zip_path = temp_dir / f"{file_path.stem}.zip" + + # Create zip archive + with ZipFile(temp_zip_path, "w") as zipf: + # Add the main file + zipf.write(file_path, file_path.name) + + # If it's an assembly format, add all files from the same directory + assembly_extensions = [".CATProduct", ".asm", ".solution", ".sldasm"] + if any(ext in str(file_path) for ext in assembly_extensions): + dir_path = file_path.parent + for file in dir_path.iterdir(): + if file.is_file() and file != file_path: + zipf.write(file, file.name) + + return temp_zip_path \ No newline at end of file diff --git a/src/ansys/geometry/core/modeler.py b/src/ansys/geometry/core/modeler.py index e3f16246f1..0e0a1c038a 100644 --- a/src/ansys/geometry/core/modeler.py +++ b/src/ansys/geometry/core/modeler.py @@ -32,6 +32,7 @@ from ansys.geometry.core.connection.client import GrpcClient import ansys.geometry.core.connection.defaults as pygeom_defaults from ansys.geometry.core.errors import GeometryRuntimeError +from ansys.geometry.core.misc.auxiliary import write_zip_file from ansys.geometry.core.misc.checks import check_type, min_backend_version from ansys.geometry.core.misc.options import ImportOptions, ImportOptionsDefinitions from ansys.geometry.core.tools.measurement_tools import MeasurementTools @@ -460,28 +461,10 @@ def open_file( ) else: # Zip file and pass filepath to service to open - import tempfile - from zipfile import ZipFile - fp_path = Path(file_path).resolve() - # Create a temporary zip file with the same name as the original file - temp_dir = Path(tempfile.gettempdir()) - temp_zip_path = temp_dir / f"{fp_path.stem}.zip" - try: - # Create zip archive - with ZipFile(temp_zip_path, "w") as zipf: - # Add the main file - zipf.write(fp_path, fp_path.name) - - # If it's an assembly format, add all files from the same directory - assembly_extensions = [".CATProduct", ".asm", ".solution", ".sldasm"] - if any(ext in str(file_path) for ext in assembly_extensions): - dir_path = fp_path.parent - for file in dir_path.iterdir(): - if file.is_file() and file != fp_path: - zipf.write(file, file.name) + temp_zip_path = write_zip_file(fp_path) # Pass the zip file path to the service self.client.services.designs.open( @@ -491,6 +474,7 @@ def open_file( import_options_definitions=import_options_definitions, open_mode="new", ) + finally: # Clean up the temporary zip file if temp_zip_path.exists(): From 8a23eaf815037993a65798735fa89731eda00dcd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 12:57:54 +0000 Subject: [PATCH 18/22] chore: auto fixes from pre-commit hooks --- src/ansys/geometry/core/misc/auxiliary.py | 6 +++--- src/ansys/geometry/core/modeler.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ansys/geometry/core/misc/auxiliary.py b/src/ansys/geometry/core/misc/auxiliary.py index b47b9b565c..bb7f41075a 100644 --- a/src/ansys/geometry/core/misc/auxiliary.py +++ b/src/ansys/geometry/core/misc/auxiliary.py @@ -404,7 +404,7 @@ def convert_opacity_to_hex(opacity: float) -> str: def write_zip_file(file_path: Path) -> Path: """Create a zip file from the given file path. - + Parameters ---------- file_path : str @@ -434,5 +434,5 @@ def write_zip_file(file_path: Path) -> Path: for file in dir_path.iterdir(): if file.is_file() and file != file_path: zipf.write(file, file.name) - - return temp_zip_path \ No newline at end of file + + return temp_zip_path diff --git a/src/ansys/geometry/core/modeler.py b/src/ansys/geometry/core/modeler.py index 0e0a1c038a..f1575734b1 100644 --- a/src/ansys/geometry/core/modeler.py +++ b/src/ansys/geometry/core/modeler.py @@ -474,7 +474,7 @@ def open_file( import_options_definitions=import_options_definitions, open_mode="new", ) - + finally: # Clean up the temporary zip file if temp_zip_path.exists(): From df270b5e95acceabfcb47b63bf7dae86ee486053 Mon Sep 17 00:00:00 2001 From: Jacob Kerstetter Date: Mon, 8 Dec 2025 09:02:57 -0500 Subject: [PATCH 19/22] revert runscript changes --- src/ansys/geometry/core/modeler.py | 5 ----- tests/integration/test_runscript.py | 10 ++-------- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/src/ansys/geometry/core/modeler.py b/src/ansys/geometry/core/modeler.py index 0e0a1c038a..519883747b 100644 --- a/src/ansys/geometry/core/modeler.py +++ b/src/ansys/geometry/core/modeler.py @@ -585,11 +585,6 @@ def run_discovery_script_file( if self.client.services.version == GeometryApiProtos.V0: serv_path = self._upload_file(file_path) else: - # Check if a design exists - if self._design is None or not self._design.is_active: - raise GeometryRuntimeError( - "No active design available. Create or open a design before running a script." - ) serv_path = file_path self.client.log.debug(f"Running Discovery script file at {file_path}...") diff --git a/tests/integration/test_runscript.py b/tests/integration/test_runscript.py index 3d4035e04e..b19360fa1f 100644 --- a/tests/integration/test_runscript.py +++ b/tests/integration/test_runscript.py @@ -37,8 +37,6 @@ # Python (.py) @pytest.mark.skip(reason="New failure to be investigated.") def test_python_simple_script(modeler: Modeler): - if modeler.client.services.version != GeometryApiProtos.V0: - modeler.create_design("test_design") result, _ = modeler.run_discovery_script_file(DSCOSCRIPTS_FILES_DIR / "simple_script.py") pattern_db = re.compile(r"SpaceClaim\.Api\.[A-Za-z0-9]+\.DesignBody", re.IGNORECASE) pattern_doc = re.compile(r"SpaceClaim\.Api\.[A-Za-z0-9]+\.Document", re.IGNORECASE) @@ -51,8 +49,6 @@ def test_python_simple_script(modeler: Modeler): def test_python_simple_script_ignore_api_version( modeler: Modeler, caplog: pytest.LogCaptureFixture ): - if modeler.client.services.version != GeometryApiProtos.V0: - modeler.create_design("test_design") result, _ = modeler.run_discovery_script_file( DSCOSCRIPTS_FILES_DIR / "simple_script.py", api_version=ApiVersions.LATEST, @@ -101,8 +97,7 @@ def test_python_integrated_script(modeler: Modeler): # SpaceClaim (.scscript) def test_scscript_simple_script(modeler: Modeler): - if modeler.client.services.version != GeometryApiProtos.V0: - modeler.create_design("test_design") + # Testing running a simple scscript file result, _ = modeler.run_discovery_script_file(DSCOSCRIPTS_FILES_DIR / "simple_script.scscript") assert len(result) == 2 pattern_db = re.compile(r"SpaceClaim\.Api\.[A-Za-z0-9]+\.DesignBody", re.IGNORECASE) @@ -114,8 +109,7 @@ def test_scscript_simple_script(modeler: Modeler): # Discovery (.dscript) def test_dscript_simple_script(modeler: Modeler): - if modeler.client.services.version != GeometryApiProtos.V0: - modeler.create_design("test_design") + # Testing running a simple dscript file result, _ = modeler.run_discovery_script_file(DSCOSCRIPTS_FILES_DIR / "simple_script.dscript") assert len(result) == 2 pattern_db = re.compile(r"SpaceClaim\.Api\.[A-Za-z0-9]+\.DesignBody", re.IGNORECASE) From 75b1b95fa492d8255e32faec26f724291235daba Mon Sep 17 00:00:00 2001 From: Jacob Kerstetter Date: Mon, 8 Dec 2025 10:47:01 -0500 Subject: [PATCH 20/22] resolving comments --- tests/integration/test_runscript.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/integration/test_runscript.py b/tests/integration/test_runscript.py index b19360fa1f..bf6d57ee0d 100644 --- a/tests/integration/test_runscript.py +++ b/tests/integration/test_runscript.py @@ -70,8 +70,6 @@ def test_python_simple_script_ignore_api_version( def test_python_failing_script(modeler: Modeler): if modeler.client.backend_type == BackendType.CORE_LINUX: pytest.skip(reason="Skipping test_python_failing_script. Operation fails on github.") - if modeler.client.services.version != GeometryApiProtos.V0: - modeler.create_design("test_design") with pytest.raises(GeometryRuntimeError): modeler.run_discovery_script_file(DSCOSCRIPTS_FILES_DIR / "failing_script.py") From 58959737f9c82a5f92d1920b0133bea77c9cb943 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Dec 2025 15:47:17 +0000 Subject: [PATCH 21/22] chore: auto fixes from pre-commit hooks --- tests/integration/test_runscript.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/integration/test_runscript.py b/tests/integration/test_runscript.py index bf6d57ee0d..84d03e1426 100644 --- a/tests/integration/test_runscript.py +++ b/tests/integration/test_runscript.py @@ -25,7 +25,6 @@ import pytest from ansys.geometry.core import Modeler -from ansys.geometry.core._grpc._version import GeometryApiProtos from ansys.geometry.core.connection.backend import ApiVersions, BackendType from ansys.geometry.core.errors import GeometryRuntimeError from ansys.geometry.core.math.point import Point2D From 8a8fcfe90c827111b07705dbecc001b4ec04debf Mon Sep 17 00:00:00 2001 From: Jacob Kerstetter Date: Mon, 8 Dec 2025 10:52:10 -0500 Subject: [PATCH 22/22] renaming function --- src/ansys/geometry/core/designer/design.py | 4 ++-- src/ansys/geometry/core/misc/auxiliary.py | 2 +- src/ansys/geometry/core/modeler.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ansys/geometry/core/designer/design.py b/src/ansys/geometry/core/designer/design.py index 59ddeab801..b958f7056d 100644 --- a/src/ansys/geometry/core/designer/design.py +++ b/src/ansys/geometry/core/designer/design.py @@ -54,7 +54,7 @@ from ansys.geometry.core.math.plane import Plane from ansys.geometry.core.math.point import Point3D from ansys.geometry.core.math.vector import UnitVector3D, Vector3D -from ansys.geometry.core.misc.auxiliary import write_zip_file +from ansys.geometry.core.misc.auxiliary import prepare_file_for_server_upload from ansys.geometry.core.misc.checks import ( ensure_design_is_active, min_backend_version, @@ -1034,7 +1034,7 @@ def insert_file( fp_path = Path(file_location).resolve() try: - temp_zip_path = write_zip_file(fp_path) + temp_zip_path = prepare_file_for_server_upload(fp_path) # Pass the zip file path to the service self._grpc_client.services.designs.insert( diff --git a/src/ansys/geometry/core/misc/auxiliary.py b/src/ansys/geometry/core/misc/auxiliary.py index bb7f41075a..25e2390206 100644 --- a/src/ansys/geometry/core/misc/auxiliary.py +++ b/src/ansys/geometry/core/misc/auxiliary.py @@ -402,7 +402,7 @@ def convert_opacity_to_hex(opacity: float) -> str: raise ValueError(f"Invalid color value: {err}") -def write_zip_file(file_path: Path) -> Path: +def prepare_file_for_server_upload(file_path: Path) -> Path: """Create a zip file from the given file path. Parameters diff --git a/src/ansys/geometry/core/modeler.py b/src/ansys/geometry/core/modeler.py index 9534984ec3..0e9a1a5e01 100644 --- a/src/ansys/geometry/core/modeler.py +++ b/src/ansys/geometry/core/modeler.py @@ -32,7 +32,7 @@ from ansys.geometry.core.connection.client import GrpcClient import ansys.geometry.core.connection.defaults as pygeom_defaults from ansys.geometry.core.errors import GeometryRuntimeError -from ansys.geometry.core.misc.auxiliary import write_zip_file +from ansys.geometry.core.misc.auxiliary import prepare_file_for_server_upload from ansys.geometry.core.misc.checks import check_type, min_backend_version from ansys.geometry.core.misc.options import ImportOptions, ImportOptionsDefinitions from ansys.geometry.core.tools.measurement_tools import MeasurementTools @@ -464,7 +464,7 @@ def open_file( fp_path = Path(file_path).resolve() try: - temp_zip_path = write_zip_file(fp_path) + temp_zip_path = prepare_file_for_server_upload(fp_path) # Pass the zip file path to the service self.client.services.designs.open(