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.
- {morphopt-3.2.0 → morphopt-3.2.1}/PKG-INFO +2 -2
- {morphopt-3.2.0 → morphopt-3.2.1}/pyproject.toml +2 -2
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/__init__.py +1 -2
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/__init__.py +2 -2
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/controller.py +16 -2
- morphopt-3.2.1/src/morphopt/optcore/modelparams/__init__.py +5 -0
- morphopt-3.2.1/src/morphopt/optcore/modelparams/geometry.py +251 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/simp/__init__.py +1 -1
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/simp/objectivefuncs/volfrac.py +1 -1
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/simp/simpmaterial.py +40 -8
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/codegen/generator.py +68 -1
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/model/problem.py +60 -10
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/model/schemas.py +16 -12
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/schemes/simp.py +40 -29
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/widgets/editor.py +34 -12
- morphopt-3.2.1/src/morphopt/ui/widgets/torchfea_model_editor.py +428 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/widgets/viewer.py +94 -18
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/workbench.py +47 -4
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt.egg-info/PKG-INFO +2 -2
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt.egg-info/SOURCES.txt +1 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt.egg-info/requires.txt +1 -1
- morphopt-3.2.0/src/morphopt/optcore/modelparams/__init__.py +0 -5
- morphopt-3.2.0/src/morphopt/optcore/modelparams/geometry.py +0 -92
- {morphopt-3.2.0 → morphopt-3.2.1}/README.md +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/setup.cfg +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/codesign/__init__.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/codesign/constraints.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/codesign/feaparams.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/codesign/geometry.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/codesign/material.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/codesign/params.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/opt_runner.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/baseobject.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/history.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/modelparams/baseparam.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/modelparams/feainterface/__init__.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/modelparams/feainterface/basefeainterface.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/modelparams/feainterface/bodyforceinterface.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/modelparams/feainterface/boundaryconditioninterface.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/modelparams/feainterface/contactinterface.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/modelparams/feainterface/coupleinterface.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/modelparams/feainterface/pointinterface.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/modelparams/feainterface/pressureinterface.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/modelparams/feainterface/referencepointinterface.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/modelparams/feainterface/springinterface.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/modelparams/feaparams.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/modelparams/materials.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/modelparams/params.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/objfunc.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/solver.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/optcore/updaters.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/shapeopt/__init__.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/shapeopt/geometryinterfaces/__init__.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/shapeopt/geometryinterfaces/basesurfaceinterface.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/shapeopt/geometryinterfaces/bspsurfaceinterface.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/shapeopt/geometryinterfaces/cpgeosurfaceinterface.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/shapeopt/geometryparams.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/shapeopt/objectivefuncs/__init__.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/shapeopt/objectivefuncs/basefuncs.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/shapeopt/objectivefuncs/boundarys.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/shapeopt/objectivefuncs/distancesurface.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/shapeopt/objectivefuncs/shapederivative.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/shapeopt/objectivefuncs/surfacefairness.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/shapeopt/objectivefuncs/volumemaximization.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/shapeopt/objfunc.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/shapeopt/update_geometry.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/simp/objectivefuncs/__init__.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/simp/objectivefuncs/basefuncs.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/simp/objectivefuncs/boundarys.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/simp/objectivefuncs/densityfield.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/simp/objectivefuncs/sensitivity.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/simp/solver.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/simp/update_material.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/taskoptmization.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/__init__.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/__main__.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/app.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/codegen/__init__.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/i18n.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/launcher.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/mainwindow.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/model/__init__.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/model/loaders.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/observe_panel.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/schemes/__init__.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/schemes/base.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/schemes/codesign.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/schemes/shapeopt.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/schemes/snippets.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/widgets/__init__.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/widgets/code_completion.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/widgets/codeeditor.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/widgets/model_tree.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/widgets/objective_editor.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/widgets/observation_pages.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/widgets/param_form.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/widgets/solver_editor.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/widgets/stepmatrix.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/widgets/template_insert_dialog.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/widgets/updater_editor.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/ui/widgets/values.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/utils/__init__.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/utils/gradient_check.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt/utils/history_read.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt.egg-info/dependency_links.txt +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt.egg-info/entry_points.txt +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/src/morphopt.egg-info/top_level.txt +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/tests/test_fixedGeometry.py +0 -0
- {morphopt-3.2.0 → morphopt-3.2.1}/tests/test_path.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
327
|
-
|
|
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,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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
722
|
+
part.elems[self.elementname] = elements_new
|
|
692
723
|
|
|
693
|
-
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|