Skip to content

Commit e91ffe2

Browse files
feat: conservative approach to single design per modeler (#1740)
Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com>
1 parent dd39086 commit e91ffe2

File tree

9 files changed

+110
-137
lines changed

9 files changed

+110
-137
lines changed

doc/changelog.d/1740.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
conservative approach to single design per modeler

src/ansys/geometry/core/__init__.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,17 @@
5555
"""Global constant for checking whether to use service colors for plotting
5656
purposes. If set to False, the default colors will be used (speed gain)."""
5757

58-
DISABLE_MULTIPLE_DESIGN_CHECK: bool = False
58+
# TODO: This is a deprecated constant. Not used in the codebase.
59+
# Left here for backwards compatibility (with older versions of the codebase).
60+
# https://github.com/ansys/pyansys-geometry/issues/1319
61+
DISABLE_MULTIPLE_DESIGN_CHECK: bool = True
62+
"""Deprecated constant. Use ``DISABLE_ACTIVE_DESIGN_CHECK`` instead."""
63+
64+
DISABLE_ACTIVE_DESIGN_CHECK: bool = False
5965
"""Global constant for disabling the ``ensure_design_is_active`` check.
6066
6167
Only set this to false if you are sure you want to disable this check
62-
and you will ONLY be working with one design.
68+
and your objects will always exist on the server side.
6369
"""
6470

6571
DOCUMENTATION_BUILD: bool = os.environ.get("PYANSYS_GEOMETRY_DOC_BUILD", "false").lower() == "true"

src/ansys/geometry/core/connection/client.py

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import warnings
3030

3131
from ansys.geometry.core.errors import protect_grpc
32+
from ansys.geometry.core.misc.checks import deprecated_method
3233

3334
# TODO: Remove this context and filter once the protobuf UserWarning issue is downgraded to INFO
3435
# https://github.com/grpc/grpc/issues/37609
@@ -224,18 +225,6 @@ def __init__(
224225

225226
# Store the backend type
226227
self._backend_type = backend_type
227-
self._multiple_designs_allowed = (
228-
False
229-
if backend_type
230-
in (
231-
BackendType.DISCOVERY,
232-
BackendType.LINUX_SERVICE,
233-
BackendType.CORE_LINUX,
234-
BackendType.CORE_WINDOWS,
235-
BackendType.DISCOVERY_HEADLESS,
236-
)
237-
else True
238-
)
239228

240229
# retrieve the backend version
241230
if hasattr(grpc_backend_response, "version"):
@@ -277,15 +266,18 @@ def backend_version(self) -> semver.version.Version:
277266
return self._backend_version
278267

279268
@property
269+
@deprecated_method(info="Multiple designs for the same service are no longer supported.")
280270
def multiple_designs_allowed(self) -> bool:
281271
"""Flag indicating whether multiple designs are allowed.
282272
273+
Deprecated since version 0.8.X.
274+
283275
Notes
284276
-----
285-
This method will return ``False`` if the backend type is ``Discovery`` or
286-
``Linux Service``. Otherwise, it will return ``True``.
277+
Currently, only one design is allowed per service. This method will always
278+
return ``False``.
287279
"""
288-
return self._multiple_designs_allowed
280+
return False
289281

290282
@property
291283
def channel(self) -> grpc.Channel:

src/ansys/geometry/core/designer/design.py

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,6 @@ def __init__(self, name: str, modeler: Modeler, read_existing_design: bool = Fal
143143
self._beam_profiles = {}
144144
self._design_id = ""
145145
self._is_active = False
146-
self._is_closed = False
147146
self._modeler = modeler
148147

149148
# Check whether we want to process an existing design or create a new one.
@@ -189,13 +188,13 @@ def is_active(self) -> bool:
189188

190189
@property
191190
def is_closed(self) -> bool:
192-
"""Whether the design is closed."""
193-
return self._is_closed
191+
"""Whether the design is closed (i.e. not active)."""
192+
return not self._is_active
194193

195194
def close(self) -> None:
196195
"""Close the design."""
197196
# Check if the design is already closed
198-
if self._is_closed:
197+
if self.is_closed:
199198
self._grpc_client.log.warning(f"Design {self.name} is already closed.")
200199
return
201200

@@ -207,15 +206,11 @@ def close(self) -> None:
207206
self._grpc_client.log.warning("Ignoring response and assuming the design is closed.")
208207

209208
# Consider the design closed (even if the close request failed)
210-
self._is_closed = True
209+
self._is_active = False
211210

212211
@protect_grpc
213212
def _activate(self, called_after_design_creation: bool = False) -> None:
214213
"""Activate the design."""
215-
# Deactivate all designs first
216-
for design in self._modeler._designs.values():
217-
design._is_active = False
218-
219214
# Activate the current design
220215
if not called_after_design_creation:
221216
self._design_stub.PutActive(EntityIdentifier(id=self._design_id))
@@ -1199,13 +1194,5 @@ def _update_design_inplace(self) -> None:
11991194
self._named_selections = {}
12001195
self._coordinate_systems = {}
12011196

1202-
# Get the previous design id
1203-
previous_design_id = self._design_id
1204-
12051197
# Read the existing design
12061198
self.__read_existing_design()
1207-
1208-
# If the design id has changed, update the design id in the Modeler
1209-
if previous_design_id != self._design_id:
1210-
self._modeler._designs[self._design_id] = self
1211-
self._modeler._designs.pop(previous_design_id)

src/ansys/geometry/core/misc/checks.py

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def wrapper(self, *args, **kwargs):
4545
import ansys.geometry.core as pyansys_geometry
4646
from ansys.geometry.core.errors import GeometryRuntimeError
4747

48-
if pyansys_geometry.DISABLE_MULTIPLE_DESIGN_CHECK:
48+
if pyansys_geometry.DISABLE_ACTIVE_DESIGN_CHECK:
4949
# If the user has disabled the check, then we can skip it
5050
return method(self, *args, **kwargs)
5151

@@ -73,17 +73,6 @@ def get_design_ref(obj) -> "Design":
7373
"The design has been closed on the backend. Cannot perform any operations on it."
7474
)
7575

76-
# Activate the design if it is not active
77-
if not design.is_active:
78-
# First, check the backend allows for multiple documents
79-
if not design._grpc_client.multiple_designs_allowed:
80-
raise GeometryRuntimeError(
81-
"The design is not active and multiple designs are "
82-
"not allowed with the current backend."
83-
)
84-
else:
85-
design._activate()
86-
8776
# Finally, call method
8877
return method(self, *args, **kwargs)
8978

src/ansys/geometry/core/modeler.py

Lines changed: 60 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,7 @@
3737
from ansys.geometry.core.connection.client import GrpcClient
3838
from ansys.geometry.core.connection.defaults import DEFAULT_HOST, DEFAULT_PORT
3939
from ansys.geometry.core.errors import GeometryRuntimeError, protect_grpc
40-
from ansys.geometry.core.logger import LOG
41-
from ansys.geometry.core.misc.checks import check_type, min_backend_version
40+
from ansys.geometry.core.misc.checks import check_type, deprecated_method, min_backend_version
4241
from ansys.geometry.core.misc.options import ImportOptions
4342
from ansys.geometry.core.tools.measurement_tools import MeasurementTools
4443
from ansys.geometry.core.tools.prepare_tools import PrepareTools
@@ -120,15 +119,15 @@ def __init__(
120119
backend_type=backend_type,
121120
)
122121

123-
# Maintaining references to all designs within the modeler workspace
124-
self._designs: dict[str, "Design"] = {}
122+
# Single design for the Modeler
123+
self._design: Optional["Design"] = None
125124

126125
# Initialize the RepairTools - Not available on Linux
127126
# TODO: delete "if" when Linux service is able to use repair tools
128127
# https://github.com/ansys/pyansys-geometry/issues/1319
129128
if BackendType.is_core_service(self.client.backend_type):
130129
self._measurement_tools = None
131-
LOG.warning("CoreService backend does not support measurement tools.")
130+
self.client.log.warning("CoreService backend does not support measurement tools.")
132131
else:
133132
self._measurement_tools = MeasurementTools(self._grpc_client)
134133

@@ -138,28 +137,26 @@ def __init__(
138137
self._geometry_commands = GeometryCommands(self._grpc_client)
139138
self._unsupported = UnsupportedCommands(self._grpc_client, self)
140139

141-
# Check if the backend allows for multiple designs and throw warning if needed
142-
if not self.client.multiple_designs_allowed:
143-
LOG.warning(
144-
"Linux and Ansys Discovery backends do not support multiple "
145-
"designs open in the same session. Only the last design created "
146-
"will be available to perform modeling operations."
147-
)
148-
149140
@property
150141
def client(self) -> GrpcClient:
151142
"""``Modeler`` instance client."""
152143
return self._grpc_client
153144

154145
@property
146+
def design(self) -> "Design":
147+
"""Retrieve the design within the modeler workspace."""
148+
return self._design
149+
150+
@property
151+
@deprecated_method(alternative="design")
155152
def designs(self) -> dict[str, "Design"]:
156-
"""All designs within the modeler workspace.
153+
"""Retrieve the design within the modeler workspace.
157154
158155
Notes
159156
-----
160-
This property is read-only. **DO NOT** modify the dictionary.
157+
This method is deprecated. Use the :func:`design` property instead.
161158
"""
162-
return self._designs
159+
return {self._design.id: self._design}
163160

164161
def create_design(self, name: str) -> "Design":
165162
"""Initialize a new design with the connected client.
@@ -177,14 +174,20 @@ def create_design(self, name: str) -> "Design":
177174
from ansys.geometry.core.designer.design import Design
178175

179176
check_type(name, str)
177+
178+
# If a previous design was available... inform the user that it will be deleted
179+
# when creating a new design.
180+
if self._design is not None and self._design.is_active:
181+
self.client.log.warning("Closing previous design before creating a new one.")
182+
self._design.close()
183+
184+
# Create the new design
180185
design = Design(name, self)
181-
self._designs[design.design_id] = design
182-
if len(self._designs) > 1:
183-
LOG.warning(
184-
"Some backends only support one design. "
185-
+ "Previous designs may be deleted (on the service) when creating a new one."
186-
)
187-
return self._designs[design.design_id]
186+
187+
# Update the design stored in the modeler
188+
self._design = design
189+
190+
return self._design
188191

189192
def get_active_design(self, sync_with_backend: bool = True) -> "Design":
190193
"""Get the active design on the modeler object.
@@ -201,14 +204,13 @@ def get_active_design(self, sync_with_backend: bool = True) -> "Design":
201204
Design
202205
Design object already existing on the modeler.
203206
"""
204-
for _, design in self._designs.items():
205-
if design._is_active:
206-
# Check if sync_with_backend is requested
207-
if sync_with_backend:
208-
design._update_design_inplace()
209-
210-
# Return the active design
211-
return design
207+
if self._design is not None and self._design.is_active:
208+
# Check if sync_with_backend is requested
209+
if sync_with_backend:
210+
self._design._update_design_inplace()
211+
return self._design
212+
else:
213+
self.client.log.warning("No active design available.")
212214

213215
return None
214216

@@ -222,43 +224,45 @@ def read_existing_design(self) -> "Design":
222224
"""
223225
from ansys.geometry.core.designer.design import Design
224226

225-
design = Design("", self, read_existing_design=True)
226-
self._designs[design.design_id] = design
227-
if len(self._designs) > 1:
228-
LOG.warning(
229-
"Some backends only support one design. "
230-
+ "Previous designs may be deleted (on the service) when reading a new one."
231-
)
232-
return self._designs[design.design_id]
227+
# Simply deactivate the existing design in case it is active...
228+
# - If it is the same design, it will be re-read (but we should not close it on the server)
229+
# - If it is a different design, it was closed previously (via open_file or create_design)
230+
if self._design is not None and self._design.is_active:
231+
self._design._is_active = False
233232

234-
def close(self, close_designs: bool = True) -> None:
233+
self._design = Design("", self, read_existing_design=True)
234+
235+
return self._design
236+
237+
def close(self, close_design: bool = True) -> None:
235238
"""Access the client's close method.
236239
237240
Parameters
238241
----------
239-
close_designs : bool, default: True
240-
Whether to close all designs before closing the client.
242+
close_design : bool, default: True
243+
Whether to close the design before closing the client.
241244
"""
242-
# Close all designs (if requested)
243-
[design.close() for design in self._designs.values() if close_designs]
245+
# Close design (if requested)
246+
if close_design and self._design is not None:
247+
self._design.close()
244248

245249
# Close the client
246250
self.client.close()
247251

248-
def exit(self, close_designs: bool = True) -> None:
252+
def exit(self, close_design: bool = True) -> None:
249253
"""Access the client's close method.
250254
251255
Parameters
252256
----------
253-
close_designs : bool, default: True
254-
Whether to close all designs before closing the client.
257+
close_design : bool, default: True
258+
Whether to close the design before closing the client.
255259
256260
Notes
257261
-----
258262
This method is calling the same method as
259263
:func:`close() <ansys.geometry.core.modeler.Modeler.close>`.
260264
"""
261-
self.close(close_designs=close_designs)
265+
self.close(close_design=close_design)
262266

263267
def _upload_file(
264268
self,
@@ -301,7 +305,7 @@ def _upload_file(
301305
with fp_path.open(mode="rb") as file:
302306
data = file.read()
303307

304-
c_stub = CommandsStub(self._grpc_client.channel)
308+
c_stub = CommandsStub(self.client.channel)
305309

306310
response = c_stub.UploadFile(
307311
UploadFileRequest(
@@ -348,6 +352,10 @@ def open_file(
348352
# Use str format of Path object here
349353
file_path = str(file_path) if isinstance(file_path, Path) else file_path
350354

355+
# Close the existing design if it is active
356+
if self._design is not None and self._design.is_active:
357+
self._design.close()
358+
351359
# Format-specific logic - upload the whole containing folder for assemblies
352360
if upload_to_server:
353361
if any(
@@ -361,7 +369,7 @@ def open_file(
361369
self._upload_file(full_path)
362370
self._upload_file(file_path, True, import_options)
363371
else:
364-
DesignsStub(self._grpc_client.channel).Open(
372+
DesignsStub(self.client.channel).Open(
365373
OpenRequest(filepath=file_path, import_options=import_options.to_dict())
366374
)
367375

@@ -372,7 +380,7 @@ def __repr__(self) -> str:
372380
lines = []
373381
lines.append(f"Ansys Geometry Modeler ({hex(id(self))})")
374382
lines.append("")
375-
lines.append(str(self._grpc_client))
383+
lines.append(str(self.client))
376384
return "\n".join(lines)
377385

378386
@protect_grpc
@@ -468,7 +476,7 @@ def run_discovery_script_file(
468476
api_version = ApiVersions.parse_input(api_version)
469477

470478
serv_path = self._upload_file(file_path)
471-
ga_stub = DbuApplicationStub(self._grpc_client.channel)
479+
ga_stub = DbuApplicationStub(self.client.channel)
472480
request = RunScriptFileRequest(
473481
script_path=serv_path,
474482
script_args=script_args,

tests/integration/conftest.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -161,11 +161,9 @@ def modeler(session_modeler: Modeler):
161161
# Yield the modeler
162162
yield session_modeler
163163

164-
# Cleanup on exit
165-
[design.close() for design in session_modeler.designs.values()]
166-
167-
# Empty the designs dictionary
168-
session_modeler._designs = {}
164+
# Cleanup on exit (if design exists)
165+
if session_modeler.design:
166+
session_modeler.design.close()
169167

170168

171169
@pytest.fixture(scope="session", autouse=True)

0 commit comments

Comments
 (0)