morphopt 3.2.1__tar.gz → 4.0.0__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.
Files changed (157) hide show
  1. {morphopt-3.2.1 → morphopt-4.0.0}/PKG-INFO +5 -3
  2. {morphopt-3.2.1 → morphopt-4.0.0}/README.md +0 -0
  3. {morphopt-3.2.1 → morphopt-4.0.0}/pyproject.toml +17 -3
  4. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/__init__.py +50 -28
  5. morphopt-4.0.0/src/morphopt/codesign/__init__.py +23 -0
  6. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/codesign/constraints.py +157 -51
  7. morphopt-4.0.0/src/morphopt/codesign/feaparams.py +66 -0
  8. morphopt-4.0.0/src/morphopt/codesign/geometry.py +265 -0
  9. morphopt-4.0.0/src/morphopt/codesign/geometryparams.py +47 -0
  10. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/opt_runner.py +36 -19
  11. morphopt-4.0.0/src/morphopt/optcore/__init__.py +21 -0
  12. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/optcore/controller.py +181 -120
  13. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/optcore/history.py +174 -141
  14. morphopt-4.0.0/src/morphopt/optcore/modelparams/__init__.py +14 -0
  15. morphopt-4.0.0/src/morphopt/optcore/modelparams/baseparam.py +170 -0
  16. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/optcore/modelparams/feainterface/__init__.py +0 -0
  17. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/optcore/modelparams/feainterface/basefeainterface.py +23 -2
  18. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/optcore/modelparams/feainterface/bodyforceinterface.py +24 -14
  19. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/optcore/modelparams/feainterface/boundaryconditioninterface.py +0 -0
  20. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/optcore/modelparams/feainterface/contactinterface.py +0 -0
  21. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/optcore/modelparams/feainterface/coupleinterface.py +0 -0
  22. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/optcore/modelparams/feainterface/pointinterface.py +0 -0
  23. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/optcore/modelparams/feainterface/pressureinterface.py +0 -0
  24. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/optcore/modelparams/feainterface/referencepointinterface.py +0 -0
  25. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/optcore/modelparams/feainterface/springinterface.py +0 -0
  26. morphopt-4.0.0/src/morphopt/optcore/modelparams/feaparams.py +134 -0
  27. morphopt-4.0.0/src/morphopt/optcore/modelparams/geometry.py +64 -0
  28. morphopt-4.0.0/src/morphopt/optcore/modelparams/materialinterface/__init__.py +11 -0
  29. morphopt-4.0.0/src/morphopt/optcore/modelparams/materialinterface/basematerialinterface.py +91 -0
  30. morphopt-4.0.0/src/morphopt/optcore/modelparams/materialinterface/homogeneousmaterial.py +21 -0
  31. morphopt-4.0.0/src/morphopt/optcore/modelparams/materialinterface/materialmodels.py +111 -0
  32. morphopt-4.0.0/src/morphopt/optcore/modelparams/materials.py +25 -0
  33. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/optcore/modelparams/params.py +62 -51
  34. morphopt-4.0.0/src/morphopt/optcore/modelparams/partinterface/__init__.py +17 -0
  35. morphopt-4.0.0/src/morphopt/optcore/modelparams/partinterface/basepartinterface.py +205 -0
  36. morphopt-4.0.0/src/morphopt/optcore/modelparams/partinterface/inppartinterface.py +72 -0
  37. morphopt-4.0.0/src/morphopt/optcore/modelparams/partinterface/torchfeapartinterface.py +177 -0
  38. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/optcore/objfunc.py +14 -10
  39. morphopt-4.0.0/src/morphopt/optcore/protocal.py +122 -0
  40. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/optcore/solver.py +107 -49
  41. morphopt-4.0.0/src/morphopt/optcore/updaters.py +779 -0
  42. morphopt-4.0.0/src/morphopt/shapeopt/__init__.py +12 -0
  43. morphopt-4.0.0/src/morphopt/shapeopt/boundarypartinterface.py +502 -0
  44. morphopt-4.0.0/src/morphopt/shapeopt/meshgenerator.py +309 -0
  45. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/shapeopt/objectivefuncs/__init__.py +0 -0
  46. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/shapeopt/objectivefuncs/basefuncs.py +0 -0
  47. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/shapeopt/objectivefuncs/boundarys.py +0 -0
  48. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/shapeopt/objectivefuncs/shapederivative.py +88 -63
  49. morphopt-4.0.0/src/morphopt/shapeopt/objectivefuncs/surfacefairness.py +66 -0
  50. morphopt-4.0.0/src/morphopt/shapeopt/objectivefuncs/volumemaximization.py +58 -0
  51. morphopt-4.0.0/src/morphopt/shapeopt/surfaceinterfaces/__init__.py +21 -0
  52. {morphopt-3.2.1/src/morphopt/shapeopt/geometryinterfaces → morphopt-4.0.0/src/morphopt/shapeopt/surfaceinterfaces}/basesurfaceinterface.py +80 -76
  53. {morphopt-3.2.1/src/morphopt/shapeopt/geometryinterfaces → morphopt-4.0.0/src/morphopt/shapeopt/surfaceinterfaces}/bspsurfaceinterface.py +14 -67
  54. {morphopt-3.2.1/src/morphopt/shapeopt/geometryinterfaces → morphopt-4.0.0/src/morphopt/shapeopt/surfaceinterfaces}/cpgeosurfaceinterface.py +9 -32
  55. morphopt-4.0.0/src/morphopt/shapeopt/update_boundarypart.py +452 -0
  56. morphopt-4.0.0/src/morphopt/simp/__init__.py +10 -0
  57. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/simp/objectivefuncs/__init__.py +0 -0
  58. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/simp/objectivefuncs/basefuncs.py +0 -0
  59. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/simp/objectivefuncs/boundarys.py +0 -0
  60. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/simp/objectivefuncs/densityfield.py +0 -0
  61. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/simp/objectivefuncs/sensitivity.py +0 -0
  62. morphopt-4.0.0/src/morphopt/simp/objectivefuncs/volfrac.py +83 -0
  63. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/simp/simpmaterial.py +587 -321
  64. morphopt-4.0.0/src/morphopt/simp/update_simpmaterial.py +403 -0
  65. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/taskoptmization.py +44 -17
  66. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/ui/__init__.py +3 -1
  67. morphopt-4.0.0/src/morphopt/ui/application/__init__.py +31 -0
  68. morphopt-4.0.0/src/morphopt/ui/application/definitions.py +171 -0
  69. morphopt-4.0.0/src/morphopt/ui/application/editor_routing.py +52 -0
  70. morphopt-4.0.0/src/morphopt/ui/application/results.py +128 -0
  71. morphopt-4.0.0/src/morphopt/ui/application/runs.py +138 -0
  72. morphopt-4.0.0/src/morphopt/ui/codegen/generator.py +816 -0
  73. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/ui/launcher.py +3 -2
  74. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/ui/mainwindow.py +30 -34
  75. morphopt-4.0.0/src/morphopt/ui/model/__init__.py +20 -0
  76. morphopt-4.0.0/src/morphopt/ui/model/modelinfo.py +118 -0
  77. morphopt-4.0.0/src/morphopt/ui/model/problem.py +2487 -0
  78. morphopt-4.0.0/src/morphopt/ui/model/schemas.py +1343 -0
  79. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/ui/observe_panel.py +127 -307
  80. morphopt-4.0.0/src/morphopt/ui/schemes/__init__.py +16 -0
  81. morphopt-4.0.0/src/morphopt/ui/schemes/base.py +353 -0
  82. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/ui/schemes/snippets.py +3 -3
  83. morphopt-4.0.0/src/morphopt/ui/templates/cantilever_beam.npz +0 -0
  84. morphopt-4.0.0/src/morphopt/ui/templates/multidesign.morph +443 -0
  85. morphopt-4.0.0/src/morphopt/ui/templates/shapeopt.morph +202 -0
  86. morphopt-4.0.0/src/morphopt/ui/templates/simp.morph +262 -0
  87. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/ui/widgets/code_completion.py +14 -20
  88. morphopt-4.0.0/src/morphopt/ui/widgets/editor.py +565 -0
  89. morphopt-4.0.0/src/morphopt/ui/widgets/model_tree.py +868 -0
  90. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/ui/widgets/objective_editor.py +6 -3
  91. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/ui/widgets/observation_pages.py +148 -14
  92. morphopt-4.0.0/src/morphopt/ui/widgets/run_dialogs.py +149 -0
  93. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/ui/widgets/stepmatrix.py +22 -23
  94. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/ui/widgets/template_insert_dialog.py +23 -17
  95. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/ui/widgets/torchfea_model_editor.py +182 -102
  96. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/ui/widgets/updater_editor.py +277 -150
  97. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/ui/widgets/values.py +1 -0
  98. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/ui/widgets/viewer.py +235 -83
  99. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/ui/workbench.py +274 -184
  100. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/utils/__init__.py +0 -0
  101. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/utils/gradient_check.py +0 -0
  102. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/utils/history_read.py +0 -0
  103. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt.egg-info/PKG-INFO +5 -3
  104. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt.egg-info/SOURCES.txt +30 -18
  105. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt.egg-info/dependency_links.txt +0 -0
  106. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt.egg-info/requires.txt +4 -1
  107. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt.egg-info/top_level.txt +0 -0
  108. morphopt-3.2.1/src/morphopt/codesign/__init__.py +0 -18
  109. morphopt-3.2.1/src/morphopt/codesign/feaparams.py +0 -75
  110. morphopt-3.2.1/src/morphopt/codesign/geometry.py +0 -359
  111. morphopt-3.2.1/src/morphopt/codesign/material.py +0 -53
  112. morphopt-3.2.1/src/morphopt/optcore/__init__.py +0 -4
  113. morphopt-3.2.1/src/morphopt/optcore/baseobject.py +0 -48
  114. morphopt-3.2.1/src/morphopt/optcore/modelparams/__init__.py +0 -5
  115. morphopt-3.2.1/src/morphopt/optcore/modelparams/baseparam.py +0 -133
  116. morphopt-3.2.1/src/morphopt/optcore/modelparams/feaparams.py +0 -239
  117. morphopt-3.2.1/src/morphopt/optcore/modelparams/geometry.py +0 -251
  118. morphopt-3.2.1/src/morphopt/optcore/modelparams/materials.py +0 -119
  119. morphopt-3.2.1/src/morphopt/optcore/updaters.py +0 -384
  120. morphopt-3.2.1/src/morphopt/shapeopt/__init__.py +0 -8
  121. morphopt-3.2.1/src/morphopt/shapeopt/geometryinterfaces/__init__.py +0 -3
  122. morphopt-3.2.1/src/morphopt/shapeopt/geometryparams.py +0 -780
  123. morphopt-3.2.1/src/morphopt/shapeopt/objectivefuncs/surfacefairness.py +0 -38
  124. morphopt-3.2.1/src/morphopt/shapeopt/objectivefuncs/volumemaximization.py +0 -25
  125. morphopt-3.2.1/src/morphopt/shapeopt/objfunc.py +0 -13
  126. morphopt-3.2.1/src/morphopt/shapeopt/update_geometry.py +0 -335
  127. morphopt-3.2.1/src/morphopt/simp/__init__.py +0 -9
  128. morphopt-3.2.1/src/morphopt/simp/objectivefuncs/volfrac.py +0 -47
  129. morphopt-3.2.1/src/morphopt/simp/solver.py +0 -41
  130. morphopt-3.2.1/src/morphopt/simp/update_material.py +0 -313
  131. morphopt-3.2.1/src/morphopt/ui/codegen/generator.py +0 -498
  132. morphopt-3.2.1/src/morphopt/ui/model/__init__.py +0 -16
  133. morphopt-3.2.1/src/morphopt/ui/model/problem.py +0 -1119
  134. morphopt-3.2.1/src/morphopt/ui/model/schemas.py +0 -801
  135. morphopt-3.2.1/src/morphopt/ui/schemes/__init__.py +0 -18
  136. morphopt-3.2.1/src/morphopt/ui/schemes/base.py +0 -186
  137. morphopt-3.2.1/src/morphopt/ui/schemes/codesign.py +0 -143
  138. morphopt-3.2.1/src/morphopt/ui/schemes/shapeopt.py +0 -100
  139. morphopt-3.2.1/src/morphopt/ui/schemes/simp.py +0 -107
  140. morphopt-3.2.1/src/morphopt/ui/widgets/editor.py +0 -344
  141. morphopt-3.2.1/src/morphopt/ui/widgets/model_tree.py +0 -485
  142. morphopt-3.2.1/tests/test_fixedGeometry.py +0 -24
  143. morphopt-3.2.1/tests/test_path.py +0 -12
  144. morphopt-3.2.1/tests/test_ui.py +0 -62
  145. {morphopt-3.2.1 → morphopt-4.0.0}/setup.cfg +0 -0
  146. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/codesign/params.py +0 -0
  147. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/shapeopt/objectivefuncs/distancesurface.py +0 -0
  148. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/ui/__main__.py +0 -0
  149. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/ui/app.py +0 -0
  150. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/ui/codegen/__init__.py +0 -0
  151. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/ui/i18n.py +0 -0
  152. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/ui/model/loaders.py +0 -0
  153. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/ui/widgets/__init__.py +0 -0
  154. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/ui/widgets/codeeditor.py +0 -0
  155. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/ui/widgets/param_form.py +0 -0
  156. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt/ui/widgets/solver_editor.py +0 -0
  157. {morphopt-3.2.1 → morphopt-4.0.0}/src/morphopt.egg-info/entry_points.txt +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: morphopt
3
- Version: 3.2.1
4
- Summary: A morphing optimization framework using PyTorch and Abaqus.
3
+ Version: 4.0.0
4
+ Summary: A morphing optimization framework using PyTorch and torchfea.
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
7
7
  Requires-Dist: torch>=2.9.1
@@ -16,11 +16,13 @@ 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.1
19
+ Requires-Dist: torchfea>=1.1.2
20
20
  Requires-Dist: trimesh>=4.11.5
21
21
  Requires-Dist: networkx>=3.6.1
22
22
  Requires-Dist: scipy>=1.17.1
23
23
  Requires-Dist: cpgeo>=1.0.12
24
+ Provides-Extra: test
25
+ Requires-Dist: pytest>=8.0; extra == "test"
24
26
 
25
27
  # MorphOpt — A Differentiable Optimization Framework for Soft Structure & Morphology Design
26
28
 
File without changes
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "morphopt"
3
- version = "3.2.1"
4
- description = "A morphing optimization framework using PyTorch and Abaqus."
3
+ version = "4.0.0"
4
+ description = "A morphing optimization framework using PyTorch and torchfea."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
7
7
  dependencies = [
@@ -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.1",
20
+ "torchfea>=1.1.2",
21
21
  "trimesh>=4.11.5",
22
22
  "networkx>=3.6.1",
23
23
  "scipy>=1.17.1",
@@ -27,3 +27,17 @@ dependencies = [
27
27
  [project.scripts]
28
28
  # console entry for the graphical UI (after `pip install morphopt`)
29
29
  morphopt-ui = "morphopt.ui:run_app"
30
+
31
+ [project.optional-dependencies]
32
+ # Install with: python -m pip install -e ".[test]"
33
+ test = [
34
+ "pytest>=8.0",
35
+ ]
36
+
37
+ [tool.pytest.ini_options]
38
+ testpaths = ["tests"]
39
+ pythonpath = ["src"]
40
+ addopts = "-ra"
41
+
42
+ [tool.setuptools.package-data]
43
+ "morphopt.ui" = ["templates/*.morph", "templates/*.npz"]
@@ -2,10 +2,18 @@
2
2
  A differentiable structural optimization framework that eliminates manual sensitivity derivation by fusing the adjoint method with automatic differentiation (AD). The sensitivity corresponds to the virtual work of residual force derivatives on the adjoint displacement field, computed via a single backpropagation through the residual graph. MorphOpt provides a unified interface for geometry, load, and material definition, calls torchfea for GPU-accelerated differentiable nonlinear FEA and adjoint-AD sensitivity, and employs a trust-region optimizer with L-BFGS for design updates. Validated on SIMP topology optimization with B-spline density fields and pneumatic soft robot shape optimization with deformation-dependent follower loads.
3
3
  """
4
4
 
5
+ # Torch and TorchFEA use GNU OpenMP in this environment. Set MKL's matching
6
+ # threading layer before importing any module that can load NumPy or Torch.
7
+ import os as __os
8
+ import sys as __sys
9
+
10
+ if __sys.platform.startswith("linux"):
11
+ __os.environ["MKL_THREADING_LAYER"] = "GNU"
5
12
 
6
13
  # region: Logging Configuration
7
14
  import logging as __logging
8
15
 
16
+
9
17
  def enable_logging(level=__logging.INFO, log_file=None, file_log_level=__logging.DEBUG):
10
18
  """
11
19
  Enable logging for the FEA package.
@@ -18,7 +26,7 @@ def enable_logging(level=__logging.INFO, log_file=None, file_log_level=__logging
18
26
  the path to a log file where logs will be written. If None, logs will only be printed to the console.
19
27
  file_log_level : int
20
28
  the logging level for the log file. Default is logging.INFO.
21
-
29
+
22
30
  Examples
23
31
  --------
24
32
  >>> import torchfea
@@ -26,56 +34,70 @@ def enable_logging(level=__logging.INFO, log_file=None, file_log_level=__logging
26
34
  """
27
35
  logger = __logging.getLogger(__name__)
28
36
  logger.setLevel(min(level, file_log_level))
29
-
37
+
30
38
  # clear existing handlers to avoid duplicate logs
31
39
  logger.handlers.clear()
32
-
40
+
33
41
  # logging to console
34
42
  console = __logging.StreamHandler()
35
43
  console.setLevel(level)
36
- console.setFormatter(__logging.Formatter(
37
- '%(asctime)s | %(levelname)-7s | %(message)s',
38
- datefmt='%Y-%m-%d %H:%M:%S'
39
- ))
44
+ console.setFormatter(
45
+ __logging.Formatter(
46
+ "%(asctime)s | %(levelname)-7s | %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
47
+ )
48
+ )
40
49
  logger.addHandler(console)
41
-
50
+
42
51
  # logging to file if log_file is provided
43
52
  if log_file:
44
- file_handler = __logging.FileHandler(log_file, encoding='utf-8')
53
+ file_handler = __logging.FileHandler(log_file, encoding="utf-8")
45
54
  file_handler.setLevel(file_log_level)
46
- file_handler.setFormatter(__logging.Formatter(
47
- '%(asctime)s | %(levelname)-7s | %(name)s | %(message)s'
48
- ))
55
+ file_handler.setFormatter(
56
+ __logging.Formatter(
57
+ "%(asctime)s | %(levelname)-7s | %(name)s | %(message)s"
58
+ )
59
+ )
49
60
  logger.addHandler(file_handler)
50
61
 
51
62
  return logger
52
- # endregion
53
-
54
63
 
55
64
 
65
+ # endregion
56
66
 
57
67
 
58
- # region optcore imports
68
+ # region modules (loaded after the core classes they subclass)
69
+ from . import codesign, shapeopt, simp
70
+ from .opt_runner import debug_optimization, start_optimization
59
71
  from .optcore.controller import Controller
60
- from .optcore.modelparams import FEAParams, FixedGeometryINP, FixedGeometryTorchFEA, FixedGeometry, HomogeneousMaterial, Params
72
+ from .optcore.history import History
73
+ from .optcore.modelparams import (
74
+ BasePartInterface,
75
+ FEAParams,
76
+ GeometryParams,
77
+ INPPartInterface,
78
+ MaterialsParams,
79
+ Params,
80
+ TorchFEAPartInterface,
81
+ load_model_assembly,
82
+ resolve_model_path,
83
+ )
84
+ from .optcore.modelparams.materialinterface import HomogeneousMaterial
85
+ from .optcore.objfunc import ObjectiveFunction
86
+ from .optcore.protocal import (
87
+ ProtocalInitializable,
88
+ ProtocalSavable,
89
+ ProtocalUpdatable,
90
+ ProtocalVisualizable,
91
+ )
61
92
  from .optcore.solver import Solver
62
- from .optcore.modelparams import Params
63
93
  from .optcore.updaters import Updaters
64
- from .optcore.objfunc import ObjectiveFunction
65
- from .optcore.history import History
66
- from .optcore.baseobject import BaseObject
67
- # endregion
68
-
69
- from .opt_runner import start_optimization, debug_optimization
94
+ from .shapeopt import BoundaryPartInterface, UpdaterBoundaryPart
95
+ from .simp import SIMP_BSPFieldMaterials, UpdaterSIMPMaterial
70
96
 
71
- # region modules
72
- from . import shapeopt
73
- from . import codesign
74
- from . import simp
75
97
  # endregion
76
-
77
98
  # region scripts
78
99
  from .utils import check_gradients, get_controller
100
+
79
101
  # endregion
80
102
 
81
103
  controller: Controller = None
@@ -0,0 +1,23 @@
1
+ """Co-design (codesign) scheme: shape optimization coupled with SIMP materials."""
2
+
3
+ from ..optcore import MaterialsParams, ObjectiveFunction, Solver, Updaters
4
+ from ..shapeopt import UpdaterBoundaryPart
5
+ from ..simp import SIMP_BSPFieldMaterials, UpdaterSIMPMaterial
6
+ from .constraints import InwardCurvatureRadius, OffsetSurfaceMinThickness
7
+ from .feaparams import CodesignFEAParams
8
+ from .geometry import CodesignBoundaryPartInterface
9
+ from .geometryparams import GeometryParams
10
+ from .params import Params
11
+
12
+
13
+ def plot_geometry_and_materials(geometry: GeometryParams, materials: MaterialsParams):
14
+ """Plot every geometry interface together with the material fields."""
15
+ import pyvista as pv
16
+
17
+ plotter = pv.Plotter(window_size=(1400, 900))
18
+ plotter.set_background("white")
19
+
20
+ plotter = materials.plot(plotter=plotter)
21
+ geometry.plot(plotter=plotter)
22
+
23
+ plotter.show()
@@ -1,7 +1,7 @@
1
1
  import torch
2
2
 
3
3
  from ..shapeopt.objectivefuncs.basefuncs import BaseConstraints
4
- from .geometry import CodesignGeometry
4
+ from .geometry import CodesignBoundaryPartInterface
5
5
 
6
6
 
7
7
  class InwardCurvatureRadius(BaseConstraints):
@@ -15,7 +15,6 @@ class InwardCurvatureRadius(BaseConstraints):
15
15
 
16
16
  def __init__(
17
17
  self,
18
- geometry: CodesignGeometry,
19
18
  surface_start: int = 1,
20
19
  surface_ids: list[int] | None = None,
21
20
  margin: float = 0.4,
@@ -31,7 +30,6 @@ class InwardCurvatureRadius(BaseConstraints):
31
30
  Initialize the inward curvature radius constraint.
32
31
 
33
32
  Args:
34
- geometry (CodesignGeometry): The geometry parameters containing the surfaces.
35
33
  surface_start (int, optional): The starting index of surfaces to apply the constraint. Defaults to 1.
36
34
  surface_ids (list[int] | None, optional): The specific surface indices to apply the constraint. If None, applies to all surfaces starting from surface_start. Defaults to None.
37
35
  margin (float, optional): The absolute margin added to thickness for curvature limit. Defaults to 0.4.
@@ -42,27 +40,39 @@ class InwardCurvatureRadius(BaseConstraints):
42
40
  penalty_scale (float, optional): Global multiplier for this constraint penalty. Defaults to 1e3.
43
41
  hard_violation_beta (float, optional): Exponential amplification factor for large violations. Defaults to 8.0.
44
42
  normal_sign (float, optional): The sign to determine inward direction. Use 1.0 if normals point outward, -1.0 if normals point inward. Defaults to 1.0.
45
-
46
- """
47
43
 
44
+ """
48
45
 
49
46
  super().__init__()
50
47
 
51
- self.geometry = geometry
52
48
  self.surface_start = int(surface_start)
53
- self.surface_ids = None if surface_ids is None else [int(i) for i in surface_ids]
49
+ """Stores the surface start used by InwardCurvatureRadius."""
50
+ self.surface_ids = (
51
+ None if surface_ids is None else [int(i) for i in surface_ids]
52
+ )
53
+ """Stores the surface ids used by InwardCurvatureRadius."""
54
54
 
55
55
  self.margin = margin
56
+ """Stores the margin used by InwardCurvatureRadius."""
56
57
  self.margin_ratio = float(margin_ratio)
58
+ """Stores the margin ratio used by InwardCurvatureRadius."""
57
59
  self.barrier_thre_ratio = float(barrier_thre_ratio)
60
+ """Stores the barrier thre ratio used by InwardCurvatureRadius."""
58
61
  self.barrier_ratio = float(barrier_ratio)
62
+ """Stores the barrier ratio used by InwardCurvatureRadius."""
59
63
  self.p = int(p)
64
+ """Stores the p used by InwardCurvatureRadius."""
60
65
  self.penalty_scale = float(penalty_scale)
66
+ """Stores the penalty scale used by InwardCurvatureRadius."""
61
67
  self.hard_violation_beta = float(hard_violation_beta)
68
+ """Stores the hard violation beta used by InwardCurvatureRadius."""
62
69
  self.normal_sign = 1.0 if normal_sign >= 0 else -1.0
70
+ """Stores the normal sign used by InwardCurvatureRadius."""
63
71
 
64
72
  @staticmethod
65
- def _principal_curvatures(rdu: torch.Tensor, rdu2: torch.Tensor, eps: float = 1e-12) -> tuple[torch.Tensor, torch.Tensor]:
73
+ def _principal_curvatures(
74
+ rdu: torch.Tensor, rdu2: torch.Tensor, eps: float = 1e-12
75
+ ) -> tuple[torch.Tensor, torch.Tensor]:
66
76
  normal0 = torch.cross(rdu[:, :, 1], rdu[:, :, 0], dim=1)
67
77
  normal = normal0 / (normal0.norm(dim=1, keepdim=True) + eps)
68
78
 
@@ -82,26 +92,45 @@ class InwardCurvatureRadius(BaseConstraints):
82
92
  k2 = 0.5 * (tr - root)
83
93
  return k1, k2
84
94
 
95
+ def initialize(self, r0, rdu0, *args, part_interface=None, **kwargs) -> None:
96
+ """Take the Part this constraint is evaluated for.
97
+
98
+ The geometry updater hands over the Part it evaluates; only a
99
+ :class:`~morphopt.codesign.CodesignBoundaryPartInterface` carries a
100
+ shell, so anything else is rejected here.
101
+ """
102
+ super().initialize(r0=r0, rdu0=rdu0, *args, **kwargs)
103
+ if not isinstance(part_interface, CodesignBoundaryPartInterface):
104
+ raise TypeError(
105
+ f"{type(self).__name__} is evaluated for one co-design Part "
106
+ f"(got {type(part_interface).__name__})."
107
+ )
108
+ self.shell_thickness = float(part_interface.shell_thickness)
109
+
85
110
  def _selected_surface_ids(self, n_surface: int) -> list[int]:
86
111
  if self.surface_ids is None:
87
112
  return [i for i in range(max(self.surface_start, 0), n_surface)]
88
113
  return [i for i in self.surface_ids if 0 <= i < n_surface]
89
114
 
90
- def __call__(self, r: list[torch.Tensor], rdu: list[torch.Tensor], rdu2: list[torch.Tensor], *args, **kwargs) -> torch.Tensor:
115
+ def __call__(
116
+ self,
117
+ r: list[torch.Tensor],
118
+ rdu: list[torch.Tensor],
119
+ rdu2: list[torch.Tensor],
120
+ *args,
121
+ **kwargs,
122
+ ) -> torch.Tensor:
91
123
  loss = torch.zeros(1, device=r[0].device, dtype=r[0].dtype)
92
124
 
93
- t = float(self.geometry.shell_thickness)
94
- if t <= 0:
95
- return loss
96
-
97
- margin_abs = (self.margin_ratio * t) + self.margin
98
- margin_abs = max(float(margin_abs), 1e-12)
99
- t_eff = t + margin_abs
125
+ for i in self._selected_surface_ids(len(r)):
126
+ t = self.shell_thickness
127
+ if t <= 0:
128
+ continue
100
129
 
101
- k_limit = 1.0 / t_eff
102
- barrier_thre = max(k_limit * self.barrier_thre_ratio, 1e-12)
130
+ margin_abs = max(float((self.margin_ratio * t) + self.margin), 1e-12)
131
+ k_limit = 1.0 / (t + margin_abs)
132
+ barrier_thre = max(k_limit * self.barrier_thre_ratio, 1e-12)
103
133
 
104
- for i in self._selected_surface_ids(len(r)):
105
134
  k1, k2 = self._principal_curvatures(rdu[i], rdu2[i])
106
135
 
107
136
  k1_in = torch.relu(self.normal_sign * k1)
@@ -109,11 +138,17 @@ class InwardCurvatureRadius(BaseConstraints):
109
138
  k_inward = torch.maximum(k1_in, k2_in)
110
139
 
111
140
  violation = k_inward - k_limit
112
- index, penalty = self.barrier_function(violation, barrier_thre, self.barrier_ratio, self.p)
141
+ index, penalty = self.barrier_function(
142
+ violation, barrier_thre, self.barrier_ratio, self.p
143
+ )
113
144
  if index.numel() > 0:
114
145
  # Strong amplification: base polynomial barrier + exponential hard-violation boost.
115
146
  rel_violation = (violation[index] / barrier_thre).clamp(min=0.0)
116
- hard_boost = torch.exp((self.hard_violation_beta * (rel_violation - 1.0)).clamp(min=0.0, max=20.0))
147
+ hard_boost = torch.exp(
148
+ (self.hard_violation_beta * (rel_violation - 1.0)).clamp(
149
+ min=0.0, max=20.0
150
+ )
151
+ )
117
152
  penalty_strong = self.penalty_scale * penalty * hard_boost
118
153
  loss = loss + (self.scaler[i][index] * penalty_strong).sum()
119
154
 
@@ -130,7 +165,6 @@ class OffsetSurfaceMinThickness(BaseConstraints):
130
165
 
131
166
  def __init__(
132
167
  self,
133
- geometry: CodesignGeometry,
134
168
  surface_start: int = 1,
135
169
  surface_ids: list[int] | None = None,
136
170
  min_distance: float | None = None,
@@ -145,26 +179,51 @@ class OffsetSurfaceMinThickness(BaseConstraints):
145
179
  normal_min: float = -0.7,
146
180
  normal_max: float = -0.5,
147
181
  ) -> None:
182
+ """
183
+ Initialize the minimum thickness constraint for the offset surface.
184
+
185
+ Args:
186
+ surface_start (int, optional): The starting index of surfaces to apply the constraint. Defaults to 1.
187
+ surface_ids (list[int] | None, optional): The specific surface indices to apply the constraint. If None, applies to all surfaces starting from surface_start. Defaults to None.
188
+ min_distance (float | None, optional): The required minimum distance.
189
+ Defaults to twice the shell thickness.
190
+ """
148
191
  super().__init__()
149
192
 
150
- self.geometry = geometry
151
193
  self.surface_start = int(surface_start)
152
- self.surface_ids = None if surface_ids is None else [int(i) for i in surface_ids]
194
+ """Stores the surface start used by OffsetSurfaceMinThickness."""
195
+ self.surface_ids = (
196
+ None if surface_ids is None else [int(i) for i in surface_ids]
197
+ )
198
+ """Stores the surface ids used by OffsetSurfaceMinThickness."""
153
199
 
154
200
  self.min_distance = min_distance
201
+ """Stores the min distance used by OffsetSurfaceMinThickness."""
155
202
  self.search_ratio = float(search_ratio)
203
+ """Stores the search ratio used by OffsetSurfaceMinThickness."""
156
204
  self.barrier_thre_ratio = float(barrier_thre_ratio)
205
+ """Stores the barrier thre ratio used by OffsetSurfaceMinThickness."""
157
206
  self.barrier_ratio = float(barrier_ratio)
207
+ """Stores the barrier ratio used by OffsetSurfaceMinThickness."""
158
208
  self.p = int(p)
209
+ """Stores the p used by OffsetSurfaceMinThickness."""
159
210
  self.penalty_scale = float(penalty_scale)
211
+ """Stores the penalty scale used by OffsetSurfaceMinThickness."""
160
212
  self.hard_violation_beta = float(hard_violation_beta)
213
+ """Stores the hard violation beta used by OffsetSurfaceMinThickness."""
161
214
  self.normal_sign = 1.0 if normal_sign >= 0 else -1.0
215
+ """Stores the normal sign used by OffsetSurfaceMinThickness."""
162
216
  self.normal_opposition_thre = float(normal_opposition_thre)
217
+ """Stores the normal opposition thre used by OffsetSurfaceMinThickness."""
163
218
  self.normal_min = float(normal_min)
219
+ """Stores the normal min used by OffsetSurfaceMinThickness."""
164
220
  self.normal_max = float(normal_max)
221
+ """Stores the normal max used by OffsetSurfaceMinThickness."""
165
222
 
166
223
  self._neighbor_pairs: dict[int, torch.Tensor] = {}
224
+ """Stores the neighbor pairs used by OffsetSurfaceMinThickness."""
167
225
  self._neighbor_mindist: dict[int, torch.Tensor] = {}
226
+ """Stores the neighbor mindist used by OffsetSurfaceMinThickness."""
168
227
 
169
228
  def _selected_surface_ids(self, n_surface: int) -> list[int]:
170
229
  if self.surface_ids is None:
@@ -177,33 +236,60 @@ class OffsetSurfaceMinThickness(BaseConstraints):
177
236
  normals = normal0 / (normal0.norm(dim=1, keepdim=True) + 1e-12)
178
237
  return normals
179
238
 
180
- def _offset_points(self, r_sf: torch.Tensor, rdu_sf: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
239
+ def _offset_points(
240
+ self, r_sf: torch.Tensor, rdu_sf: torch.Tensor, thickness: float
241
+ ) -> tuple[torch.Tensor, torch.Tensor]:
242
+ """Offset surface points and the normals they were offset along."""
181
243
  normals = self._surface_normals(rdu_sf=rdu_sf)
182
- thickness = float(self.geometry.shell_thickness)
183
244
  return r_sf + self.normal_sign * thickness * normals, normals
184
245
 
185
- def initialize(self, r0, rdu0, *args, **kwargs) -> None:
246
+ def _min_distance(self, thickness: float) -> float:
247
+ dmin = (
248
+ float(self.min_distance)
249
+ if self.min_distance is not None
250
+ else (2.0 * thickness)
251
+ )
252
+ return max(dmin, 1e-12)
253
+
254
+ def initialize(self, r0, rdu0, *args, part_interface=None, **kwargs) -> None:
255
+ """Take the Part this constraint is evaluated for and pair its points.
256
+
257
+ The geometry updater hands over the Part it evaluates; only a
258
+ :class:`~morphopt.codesign.CodesignBoundaryPartInterface` carries a
259
+ shell, so anything else is rejected here.
260
+ """
186
261
  super().initialize(r0=r0, rdu0=rdu0, *args, **kwargs)
262
+ if not isinstance(part_interface, CodesignBoundaryPartInterface):
263
+ raise TypeError(
264
+ f"{type(self).__name__} is evaluated for one co-design Part "
265
+ f"(got {type(part_interface).__name__})."
266
+ )
267
+ self.shell_thickness = float(part_interface.shell_thickness)
187
268
 
188
269
  self._neighbor_pairs = {}
189
270
  self._neighbor_mindist = {}
190
271
 
191
- import scipy.spatial
272
+ if self.shell_thickness <= 0:
273
+ return
192
274
 
193
- thickness = float(self.geometry.shell_thickness)
194
- dmin = float(self.min_distance) if self.min_distance is not None else (2.0 * thickness)
195
- dmin = max(dmin, 1e-12)
196
- search_radius = max(dmin * self.search_ratio, dmin)
275
+ import scipy.spatial
197
276
 
198
277
  for sf_idx in self._selected_surface_ids(len(r0)):
199
- roff, normals = self._offset_points(r_sf=r0[sf_idx], rdu_sf=rdu0[sf_idx])
278
+ dmin = self._min_distance(self.shell_thickness)
279
+ search_radius = max(dmin * self.search_ratio, dmin)
280
+
281
+ roff, normals = self._offset_points(
282
+ r_sf=r0[sf_idx], rdu_sf=rdu0[sf_idx], thickness=self.shell_thickness
283
+ )
200
284
 
201
285
  tree = scipy.spatial.KDTree(roff.detach().cpu().numpy())
202
- pairs_np = tree.query_pairs(r=search_radius, output_type='ndarray')
286
+ pairs_np = tree.query_pairs(r=search_radius, output_type="ndarray")
203
287
  if pairs_np.size == 0:
204
288
  continue
205
289
 
206
- pairs = torch.from_numpy(pairs_np.T).to(dtype=torch.int64, device=r0[sf_idx].device)
290
+ pairs = torch.from_numpy(pairs_np.T).to(
291
+ dtype=torch.int64, device=r0[sf_idx].device
292
+ )
207
293
 
208
294
  dotn = (normals[pairs[0]] * normals[pairs[1]]).sum(dim=1)
209
295
  opposition = (-dotn).clamp(min=0.0)
@@ -213,22 +299,28 @@ class OffsetSurfaceMinThickness(BaseConstraints):
213
299
  self._neighbor_pairs[sf_idx] = pairs
214
300
  self._neighbor_mindist[sf_idx] = dmin * opposition[keep]
215
301
 
216
- def __call__(self, r: list[torch.Tensor], rdu: list[torch.Tensor], rdu2: list[torch.Tensor], *args, **kwargs) -> torch.Tensor:
302
+ def __call__(
303
+ self,
304
+ r: list[torch.Tensor],
305
+ rdu: list[torch.Tensor],
306
+ rdu2: list[torch.Tensor],
307
+ *args,
308
+ **kwargs,
309
+ ) -> torch.Tensor:
217
310
  loss = torch.zeros(1, device=r[0].device, dtype=r[0].dtype)
218
311
 
219
- if float(self.geometry.shell_thickness) <= 0:
220
- return loss
221
-
222
- thickness = float(self.geometry.shell_thickness)
223
- dmin = float(self.min_distance) if self.min_distance is not None else (2.0 * thickness)
224
- dmin = max(dmin, 1e-12)
225
- barrier_thre = max(dmin * self.barrier_thre_ratio, 1e-12)
226
-
227
312
  for sf_idx in self._selected_surface_ids(len(r)):
228
313
  pairs = self._neighbor_pairs.get(sf_idx, None)
229
314
  if pairs is None or pairs.numel() == 0:
230
315
  continue
231
316
 
317
+ thickness = self.shell_thickness
318
+ if thickness <= 0:
319
+ continue
320
+ barrier_thre = max(
321
+ self._min_distance(thickness) * self.barrier_thre_ratio, 1e-12
322
+ )
323
+
232
324
  # Base surface normals (offset surface shares same normal direction)
233
325
  normals = self._surface_normals(rdu_sf=rdu[sf_idx])
234
326
  dotn = (normals[pairs[0]] * normals[pairs[1]]).sum(dim=1)
@@ -241,28 +333,42 @@ class OffsetSurfaceMinThickness(BaseConstraints):
241
333
  pairs_n = pairs[:, mask_n]
242
334
  dotn_n = dotn[mask_n]
243
335
  mindist_n = self._neighbor_mindist[sf_idx][mask_n].to(
244
- device=r[sf_idx].device, dtype=r[sf_idx].dtype)
336
+ device=r[sf_idx].device, dtype=r[sf_idx].dtype
337
+ )
245
338
  scaler_n0 = self.scaler[sf_idx][pairs_n[0]]
246
339
  scaler_n1 = self.scaler[sf_idx][pairs_n[1]]
247
340
 
248
341
  # Offset points and distance
249
- roff, _ = self._offset_points(r_sf=r[sf_idx], rdu_sf=rdu[sf_idx])
342
+ roff, _ = self._offset_points(
343
+ r_sf=r[sf_idx], rdu_sf=rdu[sf_idx], thickness=thickness
344
+ )
250
345
  delta = roff[pairs_n[0]] - roff[pairs_n[1]]
251
346
  dist = torch.sqrt((delta * delta).sum(dim=1) + 1e-18)
252
347
 
253
348
  violation = mindist_n - dist + barrier_thre
254
- indexl, l = self.barrier_function(violation, barrier_thre, self.barrier_ratio, self.p)
349
+ indexl, l = self.barrier_function(
350
+ violation, barrier_thre, self.barrier_ratio, self.p
351
+ )
255
352
 
256
353
  if indexl.numel() > 0:
257
354
  # Smooth normal penalty (same as Distance class)
258
- np_val = (self.normal_max - torch.clamp(dotn_n[indexl], self.normal_min, self.normal_max)) / (self.normal_max - self.normal_min)
355
+ np_val = (
356
+ self.normal_max
357
+ - torch.clamp(dotn_n[indexl], self.normal_min, self.normal_max)
358
+ ) / (self.normal_max - self.normal_min)
259
359
  np_smooth = (6 * np_val**2 - 15 * np_val + 10) * np_val**3
260
360
 
261
361
  rel_violation = (violation[indexl] / barrier_thre).clamp(min=0.0)
262
- hard_boost = torch.exp((self.hard_violation_beta * (rel_violation - 1.0)).clamp(min=0.0, max=20.0))
362
+ hard_boost = torch.exp(
363
+ (self.hard_violation_beta * (rel_violation - 1.0)).clamp(
364
+ min=0.0, max=20.0
365
+ )
366
+ )
263
367
  penalty_strong = self.penalty_scale * l * hard_boost * np_smooth
264
368
 
265
- loss = loss + (penalty_strong * scaler_n0[indexl] * scaler_n1[indexl]).sum()
369
+ loss = (
370
+ loss
371
+ + (penalty_strong * scaler_n0[indexl] * scaler_n1[indexl]).sum()
372
+ )
266
373
 
267
374
  return loss
268
-
@@ -0,0 +1,66 @@
1
+ """FEA parameter handling for the co-design (``codesign``) scheme.
2
+
3
+ Co-design builds extra ``C3D6``/``C3D15`` shell elements on the cavity surfaces
4
+ of every shell-carrying geometry interface. Those elements need to be
5
+ initialized (integration points and weights) against their Part before the
6
+ analysis runs, which is what this specialization of ``FEAParams`` does.
7
+ """
8
+
9
+ import torchfea
10
+
11
+ import morphopt
12
+
13
+ from ..optcore.modelparams.feaparams import FEAParams
14
+ from ..optcore.modelparams.geometry import GeometryParams
15
+ from .geometry import CodesignBoundaryPartInterface
16
+
17
+ __all__ = ["CodesignFEAParams"]
18
+
19
+
20
+ class CodesignFEAParams(FEAParams):
21
+ """FEA parameters that initialize the co-design offset shells."""
22
+
23
+ def shell_interfaces(
24
+ self, geometry: GeometryParams | None = None
25
+ ) -> list[CodesignBoundaryPartInterface]:
26
+ """Geometry interfaces carrying an offset shell."""
27
+ if geometry is None:
28
+ controller = morphopt.controller
29
+ geometry = controller.params.geometry if controller is not None else None
30
+ if geometry is None:
31
+ return []
32
+ return [
33
+ interface
34
+ for interface in geometry.interfaces.values()
35
+ if isinstance(interface, CodesignBoundaryPartInterface)
36
+ ]
37
+
38
+ def create_fea(self, assembly: torchfea.Assembly) -> torchfea.FEAController:
39
+ """Create the FEA controller and initialize every offset shell."""
40
+ fe = super().create_fea(assembly=assembly)
41
+
42
+ for interface in self.shell_interfaces():
43
+ if float(interface.shell_thickness) <= 0:
44
+ continue
45
+
46
+ part = fe.assembly.get_part(interface.part_name)
47
+ element_shell = None
48
+ for name in ("C3D6", "C3D15"):
49
+ if name in part.elems:
50
+ element_shell = part.elems[name]
51
+ break
52
+ if element_shell is None:
53
+ raise KeyError(
54
+ f"Shell wedge elements not found on Part "
55
+ f"{interface.part_name!r} (expected 'C3D6' or 'C3D15')."
56
+ )
57
+
58
+ element_shell.initialize(nodes=part.nodes)
59
+ if element_shell.gaussian_weight.min() < 0:
60
+ raise ValueError(
61
+ f"The Gaussian weights of the shell elements on Part "
62
+ f"{interface.part_name!r} are negative. Check the mesh "
63
+ "quality of the offset shell."
64
+ )
65
+
66
+ return fe