morphopt 3.2.0__tar.gz → 3.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of morphopt might be problematic. Click here for more details.

Files changed (110) hide show
  1. {morphopt-3.2.0 → morphopt-3.2.1}/PKG-INFO +2 -2
  2. {morphopt-3.2.0 → morphopt-3.2.1}/pyproject.toml +2 -2
  3. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/__init__.py +1 -2
  4. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/__init__.py +2 -2
  5. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/controller.py +16 -2
  6. morphopt-3.2.1/src/morphopt/optcore/modelparams/__init__.py +5 -0
  7. morphopt-3.2.1/src/morphopt/optcore/modelparams/geometry.py +251 -0
  8. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/simp/__init__.py +1 -1
  9. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/simp/objectivefuncs/volfrac.py +1 -1
  10. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/simp/simpmaterial.py +40 -8
  11. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/codegen/generator.py +68 -1
  12. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/model/problem.py +60 -10
  13. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/model/schemas.py +16 -12
  14. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/schemes/simp.py +40 -29
  15. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/widgets/editor.py +34 -12
  16. morphopt-3.2.1/src/morphopt/ui/widgets/torchfea_model_editor.py +428 -0
  17. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/widgets/viewer.py +94 -18
  18. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/workbench.py +47 -4
  19. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt.egg-info/PKG-INFO +2 -2
  20. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt.egg-info/SOURCES.txt +1 -0
  21. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt.egg-info/requires.txt +1 -1
  22. morphopt-3.2.0/src/morphopt/optcore/modelparams/__init__.py +0 -5
  23. morphopt-3.2.0/src/morphopt/optcore/modelparams/geometry.py +0 -92
  24. {morphopt-3.2.0 → morphopt-3.2.1}/README.md +0 -0
  25. {morphopt-3.2.0 → morphopt-3.2.1}/setup.cfg +0 -0
  26. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/codesign/__init__.py +0 -0
  27. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/codesign/constraints.py +0 -0
  28. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/codesign/feaparams.py +0 -0
  29. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/codesign/geometry.py +0 -0
  30. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/codesign/material.py +0 -0
  31. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/codesign/params.py +0 -0
  32. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/opt_runner.py +0 -0
  33. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/baseobject.py +0 -0
  34. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/history.py +0 -0
  35. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/modelparams/baseparam.py +0 -0
  36. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/modelparams/feainterface/__init__.py +0 -0
  37. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/modelparams/feainterface/basefeainterface.py +0 -0
  38. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/modelparams/feainterface/bodyforceinterface.py +0 -0
  39. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/modelparams/feainterface/boundaryconditioninterface.py +0 -0
  40. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/modelparams/feainterface/contactinterface.py +0 -0
  41. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/modelparams/feainterface/coupleinterface.py +0 -0
  42. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/modelparams/feainterface/pointinterface.py +0 -0
  43. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/modelparams/feainterface/pressureinterface.py +0 -0
  44. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/modelparams/feainterface/referencepointinterface.py +0 -0
  45. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/modelparams/feainterface/springinterface.py +0 -0
  46. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/modelparams/feaparams.py +0 -0
  47. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/modelparams/materials.py +0 -0
  48. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/modelparams/params.py +0 -0
  49. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/objfunc.py +0 -0
  50. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/solver.py +0 -0
  51. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/updaters.py +0 -0
  52. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/shapeopt/__init__.py +0 -0
  53. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/shapeopt/geometryinterfaces/__init__.py +0 -0
  54. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/shapeopt/geometryinterfaces/basesurfaceinterface.py +0 -0
  55. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/shapeopt/geometryinterfaces/bspsurfaceinterface.py +0 -0
  56. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/shapeopt/geometryinterfaces/cpgeosurfaceinterface.py +0 -0
  57. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/shapeopt/geometryparams.py +0 -0
  58. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/shapeopt/objectivefuncs/__init__.py +0 -0
  59. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/shapeopt/objectivefuncs/basefuncs.py +0 -0
  60. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/shapeopt/objectivefuncs/boundarys.py +0 -0
  61. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/shapeopt/objectivefuncs/distancesurface.py +0 -0
  62. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/shapeopt/objectivefuncs/shapederivative.py +0 -0
  63. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/shapeopt/objectivefuncs/surfacefairness.py +0 -0
  64. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/shapeopt/objectivefuncs/volumemaximization.py +0 -0
  65. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/shapeopt/objfunc.py +0 -0
  66. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/shapeopt/update_geometry.py +0 -0
  67. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/simp/objectivefuncs/__init__.py +0 -0
  68. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/simp/objectivefuncs/basefuncs.py +0 -0
  69. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/simp/objectivefuncs/boundarys.py +0 -0
  70. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/simp/objectivefuncs/densityfield.py +0 -0
  71. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/simp/objectivefuncs/sensitivity.py +0 -0
  72. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/simp/solver.py +0 -0
  73. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/simp/update_material.py +0 -0
  74. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/taskoptmization.py +0 -0
  75. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/__init__.py +0 -0
  76. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/__main__.py +0 -0
  77. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/app.py +0 -0
  78. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/codegen/__init__.py +0 -0
  79. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/i18n.py +0 -0
  80. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/launcher.py +0 -0
  81. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/mainwindow.py +0 -0
  82. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/model/__init__.py +0 -0
  83. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/model/loaders.py +0 -0
  84. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/observe_panel.py +0 -0
  85. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/schemes/__init__.py +0 -0
  86. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/schemes/base.py +0 -0
  87. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/schemes/codesign.py +0 -0
  88. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/schemes/shapeopt.py +0 -0
  89. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/schemes/snippets.py +0 -0
  90. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/widgets/__init__.py +0 -0
  91. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/widgets/code_completion.py +0 -0
  92. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/widgets/codeeditor.py +0 -0
  93. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/widgets/model_tree.py +0 -0
  94. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/widgets/objective_editor.py +0 -0
  95. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/widgets/observation_pages.py +0 -0
  96. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/widgets/param_form.py +0 -0
  97. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/widgets/solver_editor.py +0 -0
  98. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/widgets/stepmatrix.py +0 -0
  99. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/widgets/template_insert_dialog.py +0 -0
  100. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/widgets/updater_editor.py +0 -0
  101. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/widgets/values.py +0 -0
  102. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/utils/__init__.py +0 -0
  103. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/utils/gradient_check.py +0 -0
  104. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/utils/history_read.py +0 -0
  105. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt.egg-info/dependency_links.txt +0 -0
  106. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt.egg-info/entry_points.txt +0 -0
  107. {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt.egg-info/top_level.txt +0 -0
  108. {morphopt-3.2.0 → morphopt-3.2.1}/tests/test_fixedGeometry.py +0 -0
  109. {morphopt-3.2.0 → morphopt-3.2.1}/tests/test_path.py +0 -0
  110. {morphopt-3.2.0 → morphopt-3.2.1}/tests/test_ui.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: morphopt
3
- Version: 3.2.0
3
+ Version: 3.2.1
4
4
  Summary: A morphing optimization framework using PyTorch and Abaqus.
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
@@ -16,7 +16,7 @@ Requires-Dist: gmsh>=4.15.0
16
16
  Requires-Dist: tabulate>=0.9.0
17
17
  Requires-Dist: imageio>=2.37.3
18
18
  Requires-Dist: bspmap>=1.0.3
19
- Requires-Dist: torchfea>=1.1.0
19
+ Requires-Dist: torchfea>=1.1.1
20
20
  Requires-Dist: trimesh>=4.11.5
21
21
  Requires-Dist: networkx>=3.6.1
22
22
  Requires-Dist: scipy>=1.17.1
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "morphopt"
3
- version = "3.2.0"
3
+ version = "3.2.1"
4
4
  description = "A morphing optimization framework using PyTorch and Abaqus."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -17,7 +17,7 @@ dependencies = [
17
17
  "tabulate>=0.9.0",
18
18
  "imageio>=2.37.3",
19
19
  "bspmap>=1.0.3",
20
- "torchfea>=1.1.0",
20
+ "torchfea>=1.1.1",
21
21
  "trimesh>=4.11.5",
22
22
  "networkx>=3.6.1",
23
23
  "scipy>=1.17.1",
@@ -57,7 +57,7 @@ def enable_logging(level=__logging.INFO, log_file=None, file_log_level=__logging
57
57
 
58
58
  # region optcore imports
59
59
  from .optcore.controller import Controller
60
- from .optcore.modelparams import FEAParams, FixedGeometryINP, FixedGeometry, HomogeneousMaterial, Params
60
+ from .optcore.modelparams import FEAParams, FixedGeometryINP, FixedGeometryTorchFEA, FixedGeometry, HomogeneousMaterial, Params
61
61
  from .optcore.solver import Solver
62
62
  from .optcore.modelparams import Params
63
63
  from .optcore.updaters import Updaters
@@ -79,4 +79,3 @@ from .utils import check_gradients, get_controller
79
79
  # endregion
80
80
 
81
81
  controller: Controller = None
82
-
@@ -1,4 +1,4 @@
1
- from .modelparams import Params, BaseGeometry, FEAParams, HomogeneousMaterial, BaseParams, FixedGeometryINP, FixedGeometry
1
+ from .modelparams import Params, BaseGeometry, FEAParams, HomogeneousMaterial, BaseParams, FixedGeometryINP, FixedGeometryTorchFEA, FixedGeometry
2
2
  from .updaters import BaseUpdater, Updaters
3
3
  from .solver import Solver
4
- from .objfunc import ObjectiveFunction
4
+ from .objfunc import ObjectiveFunction
@@ -323,8 +323,22 @@ class Controller:
323
323
  self.history.append('objective', loss.item())
324
324
  self.history.append('metrics', self.objfun.get_metrics())
325
325
  self.history.append('time', [t1-t0, t2-t1, t3-t2, t4-t3])
326
- self.history.append('num_elements', sum([elem._elems.shape[0] for elem in self.objfun.fe.assembly.get_instance('final_model').elems.values()]))
327
- self.history.append('num_nodes', self.objfun.fe.assembly.get_instance('final_model').nodes.shape[0])
326
+ # Shape/codesign jobs historically used one ``final_model`` instance,
327
+ # while a SIMP job may import an Assembly from torchfea-ui with
328
+ # arbitrary Part/Instance names (for example ``Part-1-1``). Record
329
+ # statistics from the actual Assembly instead of assuming a name.
330
+ instances = self.objfun.fe.assembly._instances.values()
331
+ num_elements = sum(
332
+ element._elems.shape[0]
333
+ for instance in instances
334
+ for element in instance.elems.values()
335
+ )
336
+ num_nodes = sum(
337
+ instance.nodes.shape[0]
338
+ for instance in self.objfun.fe.assembly._instances.values()
339
+ )
340
+ self.history.append('num_elements', num_elements)
341
+ self.history.append('num_nodes', num_nodes)
328
342
 
329
343
  def print_info(self, t0, t1, t2, t3, t4) -> None:
330
344
  """
@@ -0,0 +1,5 @@
1
+ from .geometry import FixedGeometryINP, FixedGeometryTorchFEA, FixedGeometry, BaseGeometry
2
+ from .feaparams import FEAParams
3
+ from .materials import HomogeneousMaterial, BaseMaterials
4
+ from .params import Params
5
+ from .baseparam import BaseParams
@@ -0,0 +1,251 @@
1
+ """Geometry parameter classes and the TorchFEA Assembly import helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from functools import lru_cache
7
+ from pathlib import Path
8
+
9
+ import numpy as np
10
+ import torchfea
11
+
12
+ from .baseparam import BaseParams
13
+
14
+
15
+ # ---------------------------------------------------------------------------
16
+ # TorchFEA model import helpers
17
+ # ---------------------------------------------------------------------------
18
+
19
+ @dataclass(frozen=True)
20
+ class PartSummary:
21
+ name: str
22
+ node_sets: tuple[str, ...]
23
+ element_sets: tuple[str, ...]
24
+ surface_sets: tuple[str, ...]
25
+ element_types: tuple[str, ...]
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class InstanceSummary:
30
+ name: str
31
+ part_name: str
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class TorchFEAModelSummary:
36
+ path: str
37
+ parts: tuple[PartSummary, ...]
38
+ instances: tuple[InstanceSummary, ...]
39
+
40
+ def part_for_instance(self, instance_name: str) -> PartSummary | None:
41
+ instance = next(
42
+ (item for item in self.instances if item.name == instance_name), None)
43
+ if instance is None:
44
+ return None
45
+ return next(
46
+ (item for item in self.parts if item.name == instance.part_name), None)
47
+
48
+
49
+ def resolve_model_path(model_directory: str, model_filename: str = "") -> Path:
50
+ """Resolve one exported TorchFEA ``.npz`` inside ``model_directory``."""
51
+ directory = Path(model_directory).expanduser().resolve()
52
+ if not directory.is_dir():
53
+ raise FileNotFoundError(
54
+ f"TorchFEA model directory does not exist: {directory}")
55
+
56
+ if model_filename:
57
+ candidate = Path(model_filename)
58
+ path = candidate if candidate.is_absolute() else directory / candidate
59
+ path = path.expanduser().resolve()
60
+ if not path.is_file():
61
+ raise FileNotFoundError(f"TorchFEA model file does not exist: {path}")
62
+ return path
63
+
64
+ candidates = sorted(
65
+ directory.glob("*.npz"), key=lambda item: item.stat().st_mtime_ns,
66
+ reverse=True)
67
+ if not candidates:
68
+ raise FileNotFoundError(
69
+ f"No TorchFEA .npz model was found in: {directory}")
70
+ return candidates[0].resolve()
71
+
72
+
73
+ def inspect_model(model_directory: str,
74
+ model_filename: str = "") -> TorchFEAModelSummary:
75
+ """Return selectable Part/Instance/set names from an exported model."""
76
+ path = resolve_model_path(model_directory, model_filename)
77
+ return _inspect_model_cached(str(path), path.stat().st_mtime_ns)
78
+
79
+
80
+ @lru_cache(maxsize=16)
81
+ def _inspect_model_cached(path: str, _mtime_ns: int) -> TorchFEAModelSummary:
82
+ controller = _read_controller(path)
83
+ source = controller.assembly
84
+ if source is None:
85
+ raise ValueError("The TorchFEA model does not contain an Assembly.")
86
+
87
+ parts = tuple(
88
+ PartSummary(
89
+ name=name,
90
+ node_sets=tuple(part.set_nodes.keys()),
91
+ element_sets=tuple(part.set_elements.keys()),
92
+ surface_sets=tuple(part.surfaces.keys()),
93
+ element_types=tuple(part.elems.keys()),
94
+ )
95
+ for name, part in source._parts.items()
96
+ )
97
+ instances = tuple(
98
+ InstanceSummary(name=name, part_name=instance.part_name)
99
+ for name, instance in source._instances.items()
100
+ )
101
+ if not parts:
102
+ raise ValueError("The TorchFEA Assembly contains no Parts.")
103
+ if not instances:
104
+ raise ValueError("The TorchFEA Assembly contains no Instances.")
105
+ return TorchFEAModelSummary(str(Path(path).resolve()), parts, instances)
106
+
107
+
108
+ def load_geometry_assembly(model_directory: str,
109
+ model_filename: str = "") -> torchfea.Assembly:
110
+ """Load a clean Assembly containing only cloned Parts and Instances."""
111
+ path = resolve_model_path(model_directory, model_filename)
112
+ controller = _read_controller(str(path))
113
+ source = controller.assembly
114
+ if source is None:
115
+ raise ValueError("The TorchFEA model does not contain an Assembly.")
116
+
117
+ assembly = torchfea.Assembly()
118
+ for name, part in source._parts.items():
119
+ cloned_part = torchfea.Serializable._deserialize(part._serialize())
120
+ if (cloned_part.exterior_surface == "extern"
121
+ and "extern" not in cloned_part.surfaces):
122
+ boundary = []
123
+ for element in cloned_part.elems.values():
124
+ boundary.extend(element.extract_boundary_surface_set())
125
+ if boundary:
126
+ cloned_part.surfaces["extern"] = boundary
127
+ assembly.add_part(cloned_part, name=name)
128
+ for name, instance in source._instances.items():
129
+ cloned_instance = torchfea.Serializable._deserialize(instance._serialize())
130
+ assembly.add_instance(cloned_instance, name=name)
131
+
132
+ if not assembly._parts:
133
+ raise ValueError("The TorchFEA Assembly contains no Parts.")
134
+ if not assembly._instances:
135
+ raise ValueError("The TorchFEA Assembly contains no Instances.")
136
+ return assembly
137
+
138
+
139
+ def _read_controller(path: str) -> torchfea.FEAController:
140
+ """Deserialize a controller without initializing its analysis objects."""
141
+ with np.load(path, allow_pickle=True) as loaded:
142
+ if "data" not in loaded:
143
+ raise ValueError("The .npz file is not a TorchFEA model archive.")
144
+ return torchfea.FEAController._deserialize(loaded["data"])
145
+
146
+ class BaseGeometry(BaseParams):
147
+ """
148
+ Base class for geometry parameter classes.
149
+
150
+ Subclasses must provide the mesh/geometry generation and assembly modification
151
+ behavior used by the optimization pipeline.
152
+ """
153
+
154
+ def __init__(self, *args, **kwargs):
155
+ super().__init__(**kwargs)
156
+
157
+ def generate(self, path_result: str, pools=None) -> torchfea.Assembly:
158
+ """Generate the FEA part for the current geometry."""
159
+ raise NotImplementedError
160
+
161
+
162
+
163
+ class FixedGeometry(BaseGeometry):
164
+ """
165
+ Base class for geometry whose FE assembly stays fixed during optimization.
166
+
167
+ Subclasses may read an INP mesh or a saved TorchFEA model. In both cases
168
+ the assembly is built once and reused across optimization iterations.
169
+ """
170
+ def __init__(self):
171
+ super().__init__()
172
+ self._assembly: torchfea.Assembly = None
173
+ """
174
+ The FEA assembly containing the geometry for the optimization problem. This assembly is generated from a fixed mesh and does not change during optimization iterations.
175
+ """
176
+
177
+ @property
178
+ def assembly(self) -> torchfea.Assembly:
179
+ """
180
+ Get the FEA assembly containing the geometry for the optimization problem.
181
+
182
+ Returns:
183
+ torchfea.Assembly: The FEA assembly containing the geometry.
184
+ """
185
+ if self._assembly is None:
186
+ self._assembly = self.define_assembly()
187
+ return self._assembly
188
+
189
+ def define_assembly(self)-> torchfea.Assembly:
190
+ """
191
+ Define the assembly for the geometry using node and element data.
192
+
193
+ This method should be implemented in subclasses to create the assembly based on specific node and element data.
194
+
195
+ Returns:
196
+ torchfea.Assembly: The defined assembly for the geometry.
197
+ """
198
+ raise NotImplementedError
199
+
200
+ def generate(self, *args, **kwargs):
201
+ """Return the lazily built fixed torchfea assembly."""
202
+ return self.assembly
203
+
204
+ class FixedGeometryINP(FixedGeometry):
205
+ """
206
+ Geometry parameters class for fixed mesh geometry using an Abaqus .inp file.
207
+ """
208
+ def __init__(self, mesh_file: str, part_name: str = 'final_model'):
209
+ super().__init__()
210
+ self._mesh_file = mesh_file
211
+ """
212
+ The file path of the mesh to be loaded for the geometry.
213
+ """
214
+
215
+ self._part_name = part_name
216
+ """The name of the part in the Abaqus INP file to be used for the geometry."""
217
+
218
+ def define_assembly(self):
219
+ inp = torchfea.FEA_INP()
220
+ inp.read_inp(self._mesh_file)
221
+
222
+ fe_ext = torchfea.from_inp(inp)
223
+
224
+ assembly = torchfea.Assembly()
225
+
226
+ assembly.add_part(part=fe_ext.assembly.get_part(self._part_name), name='final_model')
227
+ instance = torchfea.Instance(part_name='final_model')
228
+ assembly.add_instance(instance=instance, name='final_model')
229
+ instance.exterior_surface = 'surface_0_All'
230
+ return assembly
231
+
232
+
233
+ class FixedGeometryTorchFEA(FixedGeometry):
234
+ """Fixed geometry imported from a model exported by ``torchfea-ui``.
235
+
236
+ Only the saved Parts and Instances are used. MorphOpt creates its own
237
+ loads, constraints, boundary conditions, reference points, and solver.
238
+ """
239
+
240
+ def __init__(self, model_directory: str, model_filename: str):
241
+ super().__init__()
242
+ self.model_directory = str(model_directory)
243
+ self.model_filename = str(model_filename)
244
+ # Fail early with a path-specific message while keeping assembly
245
+ # construction lazy.
246
+ self.model_path = str(resolve_model_path(
247
+ self.model_directory, self.model_filename))
248
+
249
+ def define_assembly(self) -> torchfea.Assembly:
250
+ return load_geometry_assembly(
251
+ self.model_directory, self.model_filename)
@@ -6,4 +6,4 @@ This module implements the SIMP (Solid Isotropic Material with Penalization) met
6
6
  from .solver import SIMPSolver
7
7
  from .update_material import UpdaterMaterials
8
8
  from .simpmaterial import SIMP_BSPFieldMaterials
9
- from ..optcore import FEAParams, HomogeneousMaterial, Solver, Updaters, ObjectiveFunction, Params, FixedGeometry, FixedGeometryINP
9
+ from ..optcore import FEAParams, HomogeneousMaterial, Solver, Updaters, ObjectiveFunction, Params, FixedGeometry, FixedGeometryINP, FixedGeometryTorchFEA
@@ -22,7 +22,7 @@ class VolFrac(BaseConstraints):
22
22
 
23
23
 
24
24
  def initialize(self, material_params: SIMP_BSPFieldMaterials, *args, **kwargs):
25
- part = morphopt.controller.objfun.fe.assembly.get_part('final_model')
25
+ part = morphopt.controller.objfun.fe.assembly.get_part(material_params.part_name)
26
26
  elems = part.elems[self.element_name]
27
27
  self.gaussian_points = elems.get_gaussian_points(part.nodes).reshape(-1, 3)
28
28
  self.gaussian_weights = elems.gaussian_weight.flatten()
@@ -298,6 +298,18 @@ class SIMPElementHuHu_LuLu(torchfea.elements.Element_3D):
298
298
 
299
299
  return EmdUe + result0[0], self._EmdUe_2 + result0[1]
300
300
 
301
+ class SIMPElementC3D4(torchfea.elements.C3D4, SIMPElementFskew):
302
+ """C3D4 adapter for the optional SIMP Fskew penalty.
303
+
304
+ C3D4 is a linear tetrahedral element, so its second shape-function
305
+ derivatives are zero and the Fskew term evaluates to zero. It still
306
+ needs the adapter when the penalty option is enabled because the
307
+ sensitivity update refreshes the penalty caches uniformly for all SIMP
308
+ elements.
309
+ """
310
+ pass
311
+
312
+
301
313
  class SIMPElementC3D10(torchfea.elements.C3D10, SIMPElementFskew):
302
314
  pass
303
315
 
@@ -355,6 +367,7 @@ class SIMP_BSPFieldMaterials(BaseParams):
355
367
  voidpenalfactor: float = 1e-2,
356
368
  materialpenalty: int = 8,
357
369
  elementname: str = "C3D4",
370
+ part_name: str = "",
358
371
  ) -> None:
359
372
  """
360
373
  Initialize the SIMPMaterials class.
@@ -439,6 +452,9 @@ class SIMP_BSPFieldMaterials(BaseParams):
439
452
  self.elementname = elementname
440
453
  """ The name of the element type to which the SIMP material will be applied. """
441
454
 
455
+ self.part_name = str(part_name).strip()
456
+ """Name of the imported Part carrying the SIMP design field."""
457
+
442
458
 
443
459
 
444
460
 
@@ -675,22 +691,37 @@ class SIMP_BSPFieldMaterials(BaseParams):
675
691
  """
676
692
 
677
693
  # Set the SIMP materials for the solid elements
678
- elements = fe.assembly.get_part('final_model').elems[self.elementname]
694
+ if not self.part_name:
695
+ raise ValueError("Select the imported Part for the SIMP material.")
696
+ part = fe.assembly.get_part(self.part_name)
697
+ elements = part.elems[self.elementname]
679
698
 
680
699
  if self.if_use_simppenalty:
681
- if elements.__class__.__name__ == "C3D10":
700
+ # The fixed geometry is reused between optimization iterations;
701
+ # once an element has been wrapped, keep that wrapper instead of
702
+ # trying to wrap it again based on its new class name.
703
+ if isinstance(elements, SIMPElementFskew):
704
+ elements_new = elements
705
+ elif elements.__class__.__name__ == "C3D4":
706
+ elements_new = SIMPElementC3D4(elems_index=elements._elems_index, elems=elements._elems, penalfactor=self.voidpenalfactor)
707
+ elif elements.__class__.__name__ == "C3D10":
682
708
  elements_new = SIMPElementC3D10(elems_index=elements._elems_index, elems=elements._elems, penalfactor=self.voidpenalfactor)
709
+ elif elements.__class__.__name__ == "C3D8":
710
+ elements_new = SIMPElementC3D8(elems_index=elements._elems_index, elems=elements._elems, penalfactor=self.voidpenalfactor)
683
711
  elif elements.__class__.__name__ == "C3D20":
684
712
  elements_new = SIMPElementC3D20(elems_index=elements._elems_index, elems=elements._elems, penalfactor=self.voidpenalfactor)
685
713
  else:
686
- elements_new = elements
714
+ raise ValueError(
715
+ f"SIMP Fskew penalty is not implemented for element type "
716
+ f"'{elements.__class__.__name__}'. Set voidpenalfactor=0 "
717
+ "or use a supported solid element type.")
687
718
  else:
688
719
  elements_new = elements
689
720
 
690
721
 
691
- fe.assembly.get_part('final_model').elems[self.elementname] = elements_new
722
+ part.elems[self.elementname] = elements_new
692
723
 
693
- nodes = fe.assembly.get_part('final_model').nodes
724
+ nodes = part.nodes
694
725
  elements_new._pre_load_gaussian(nodes=nodes)
695
726
 
696
727
  gaussian_points_locations = elements_new.get_gaussian_points(nodes=nodes)
@@ -742,7 +773,7 @@ class SIMP_BSPFieldMaterials(BaseParams):
742
773
  ratio_now = self.get_material_ratio(designfield)
743
774
 
744
775
 
745
- elems: SIMPElementC3D10 = assembly.get_part("final_model").elems[self.elementname]
776
+ elems: torchfea.elements.Element_3D = assembly.get_part(self.part_name).elems[self.elementname]
746
777
 
747
778
  elems.materials['material-0']._mu = ratio_now * self._mumax
748
779
  elems.materials['material-0']._kappa = ratio_now * self._kappamax
@@ -886,6 +917,7 @@ class SIMP_BSPFieldMaterials(BaseParams):
886
917
  Returns:
887
918
  torch.Tensor: The normalized Gaussian points.
888
919
  """
889
- gaussian_points_locations = assembly.get_part('final_model').elems[self.elementname].get_gaussian_points(assembly.get_part('final_model').nodes)
920
+ part = assembly.get_part(self.part_name)
921
+ gaussian_points_locations = part.elems[self.elementname].get_gaussian_points(part.nodes)
890
922
 
891
- return gaussian_points_locations
923
+ return gaussian_points_locations
@@ -16,6 +16,7 @@ from ..model.schemas import (
16
16
  SURFACE_TYPES, INTERFACE_TYPES, UPDATER_CATALOG,
17
17
  )
18
18
  from ..schemes.base import get_template
19
+ from ...optcore.modelparams.geometry import inspect_model
19
20
 
20
21
 
21
22
  # --------------------------------------------------------------------------
@@ -220,7 +221,23 @@ def _emit_geometry_init(a, problem: ProblemDefinition, template) -> None:
220
221
  surfaces = problem.surfaces()
221
222
  if problem.scheme == "simp":
222
223
  geo = problem.geometry or GeometryNode()
223
- a(f" super().__init__(mesh_file={_literal(geo.mesh_file)})")
224
+ if geo.model_directory and geo.model_filename:
225
+ model = inspect_model(geo.model_directory, geo.model_filename)
226
+ _validate_simp_interface_selections(problem, model)
227
+ material = problem.material
228
+ if material is not None:
229
+ parts = {item.name: item for item in model.parts}
230
+ part_name = str(material.part_name or "").strip()
231
+ if part_name not in parts:
232
+ raise ValueError("Select an imported Part for the SIMP material.")
233
+ if material.elementname not in parts[part_name].element_types:
234
+ raise ValueError(
235
+ f"Element {material.elementname!r} does not exist on imported "
236
+ f"Part {part_name!r}.")
237
+ a(" super().__init__(")
238
+ a(f" model_directory={_literal(geo.model_directory)},")
239
+ a(f" model_filename={_literal(geo.model_filename)},")
240
+ a(" )")
224
241
  return
225
242
  kw = _geometry_kwargs(problem)
226
243
  a(f" super().__init__({kw})" if kw else " super().__init__()")
@@ -230,6 +247,56 @@ def _emit_geometry_init(a, problem: ProblemDefinition, template) -> None:
230
247
  a(f" self.add_surface({render_surface_call(srf)})")
231
248
 
232
249
 
250
+ def _validate_simp_interface_selections(problem: ProblemDefinition, model) -> None:
251
+ """Validate all Assembly-backed names before emitting runnable source."""
252
+ instances = {item.name: item for item in model.instances}
253
+ parts = {item.name: item for item in model.parts}
254
+ for interface in problem.interfaces():
255
+ spec = INTERFACE_TYPES.get(interface.interface_type, {})
256
+ fields_for_type = {field["key"] for field in spec.get("params", [])}
257
+ for instance_key in ("instance_name", "instance_name1", "instance_name2"):
258
+ if instance_key not in fields_for_type:
259
+ continue
260
+ selected = str(interface.get_field(instance_key, "") or "").strip()
261
+ if not selected:
262
+ raise ValueError(
263
+ f"Interface {interface.name!r} must select {instance_key}.")
264
+ if selected not in instances:
265
+ raise ValueError(
266
+ f"Interface {interface.name!r} references unknown TorchFEA "
267
+ f"Instance {selected!r}.")
268
+
269
+ selections = (
270
+ ("set_nodes_name", "instance_name", "node_sets"),
271
+ ("surface_name", "instance_name", "surface_sets"),
272
+ ("surface_name1", "instance_name1", "surface_sets"),
273
+ ("surface_name2", "instance_name2", "surface_sets"),
274
+ )
275
+ for key, instance_key, set_attribute in selections:
276
+ if key not in fields_for_type:
277
+ continue
278
+ selected = str(interface.get_field(key, "") or "").strip()
279
+ instance_name = str(interface.get_field(instance_key, "") or "").strip()
280
+ if not selected:
281
+ raise ValueError(f"Interface {interface.name!r} must select {key}.")
282
+ instance = instances.get(instance_name)
283
+ part = parts.get(instance.part_name) if instance is not None else None
284
+ available = getattr(part, set_attribute, ()) if part is not None else ()
285
+ if selected not in available:
286
+ raise ValueError(
287
+ f"Interface {interface.name!r} references unknown {key} "
288
+ f"{selected!r} on Instance {instance_name!r}.")
289
+
290
+ if "element_name" in fields_for_type:
291
+ element_name = str(interface.element_name or "").strip()
292
+ instance = instances.get(str(interface.instance_name or "").strip())
293
+ part = parts.get(instance.part_name) if instance is not None else None
294
+ if part is None or element_name not in part.element_types:
295
+ raise ValueError(
296
+ f"Interface {interface.name!r} references unknown element "
297
+ f"{element_name!r} on Instance {interface.instance_name!r}.")
298
+
299
+
233
300
  def _emit_apply_constraints(a, problem: ProblemDefinition, template) -> None:
234
301
  geo = problem.geometry or GeometryNode()
235
302
  updater = problem.updater