morphopt 4.0.2__tar.gz → 4.0.4__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 (214) hide show
  1. {morphopt-4.0.2 → morphopt-4.0.4}/PKG-INFO +2 -1
  2. {morphopt-4.0.2 → morphopt-4.0.4}/pyproject.toml +2 -1
  3. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/constants.py +2 -4
  4. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/optcore/controller.py +3 -2
  5. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/optcore/modelparams/baseparam.py +6 -2
  6. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/optcore/modelparams/feaparams.py +21 -46
  7. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/optcore/modelparams/parts/base_part.py +2 -3
  8. morphopt-4.0.4/src/morphopt/optcore/modelparams/parts/cad_part.py +370 -0
  9. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/optcore/objfunc.py +19 -13
  10. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/optcore/solver.py +39 -34
  11. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/shapeopt/boundary_part.py +151 -140
  12. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/shapeopt/objectivefuncs/basefuncs.py +25 -13
  13. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/shapeopt/objectivefuncs/boundarys.py +2 -2
  14. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/shapeopt/objectivefuncs/shapederivative.py +3 -6
  15. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/shapeopt/objectivefuncs/surfacefairness.py +1 -25
  16. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/shapeopt/objectivefuncs/volumemaximization.py +0 -20
  17. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/shapeopt/surfaces/__init__.py +7 -7
  18. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/shapeopt/surfaces/base_constraints.py +1 -3
  19. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/shapeopt/surfaces/base_surface.py +439 -292
  20. morphopt-4.0.4/src/morphopt/shapeopt/surfaces/brep_surface.py +695 -0
  21. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/shapeopt/surfaces/bsp_constraints.py +1 -3
  22. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/shapeopt/surfaces/bsp_surface.py +355 -218
  23. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/shapeopt/surfaces/cpgeo_constraints.py +4 -12
  24. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/shapeopt/surfaces/cpgeo_surface.py +212 -126
  25. morphopt-4.0.4/src/morphopt/shapeopt/surfaces/mesh_surface.py +86 -0
  26. morphopt-4.0.4/src/morphopt/shapeopt/surfaces/nurbs_face.py +414 -0
  27. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/shapeopt/update_boundary_part.py +10 -25
  28. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/__init__.py +3 -1
  29. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/application/cad.py +46 -2
  30. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/application/completion.py +8 -9
  31. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/application/definitions.py +3 -1
  32. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/application/edit_history.py +25 -33
  33. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/application/editing.py +55 -58
  34. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/application/fields.py +63 -13
  35. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/application/finite_element.py +9 -5
  36. morphopt-4.0.4/src/morphopt/ui/application/load_cases.py +243 -0
  37. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/application/part_sets.py +28 -3
  38. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/application/quickstart.py +58 -77
  39. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/application/results.py +11 -10
  40. morphopt-4.0.4/src/morphopt/ui/application/surface_export.py +105 -0
  41. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/application/updaters.py +0 -13
  42. morphopt-4.0.4/src/morphopt/ui/application/validation.py +638 -0
  43. morphopt-4.0.4/src/morphopt/ui/cad_worker.py +28 -0
  44. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/codegen/assets.py +29 -6
  45. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/codegen/custom_classes.py +4 -3
  46. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/codegen/generator.py +64 -60
  47. morphopt-4.0.4/src/morphopt/ui/codegen/workspace.py +65 -0
  48. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/locales/en-US.json +93 -17
  49. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/locales/zh-CN.json +93 -17
  50. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/mainwindow.py +33 -8
  51. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/model/base_nodes.py +4 -1
  52. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/model/configuration.py +5 -2
  53. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/model/node_types/__init__.py +4 -2
  54. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/model/node_types/structural.py +22 -32
  55. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/model/node_types/surfaces.py +35 -6
  56. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/model/problem.py +38 -35
  57. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/model/registry.py +4 -2
  58. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/observe_panel.py +28 -4
  59. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/presentation/labels.py +1 -0
  60. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/refresh.py +1 -1
  61. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/schemes/base.py +13 -16
  62. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/schemes/snippets.py +11 -11
  63. morphopt-4.0.4/src/morphopt/ui/surface_export_worker.py +22 -0
  64. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/theme/palette.py +2 -9
  65. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/theme/themes/dark.py +2 -0
  66. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/theme/themes/light.py +2 -0
  67. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/visualization/geometry.py +47 -7
  68. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/visualization/loads.py +3 -4
  69. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/widgets/asset_browser.py +16 -1
  70. morphopt-4.0.4/src/morphopt/ui/widgets/case_deletion.py +25 -0
  71. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/widgets/definition_actions.py +93 -1
  72. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/widgets/definition_manager.py +7 -1
  73. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/widgets/edit_history.py +8 -18
  74. morphopt-4.0.4/src/morphopt/ui/widgets/history_dialogs.py +63 -0
  75. morphopt-4.0.4/src/morphopt/ui/widgets/load_cases.py +153 -0
  76. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/widgets/model_tree.py +108 -32
  77. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/widgets/objective_editor.py +55 -25
  78. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/widgets/observation_pages.py +3 -3
  79. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/widgets/solver_editor.py +4 -3
  80. morphopt-4.0.4/src/morphopt/ui/widgets/surface_export.py +163 -0
  81. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/widgets/updater_editor.py +26 -129
  82. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/widgets/viewer.py +5 -7
  83. morphopt-4.0.4/src/morphopt/ui/widgets/workspace_objective.py +92 -0
  84. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/workbench.py +70 -5
  85. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt.egg-info/PKG-INFO +2 -1
  86. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt.egg-info/SOURCES.txt +14 -2
  87. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt.egg-info/requires.txt +1 -0
  88. morphopt-4.0.2/src/morphopt/optcore/modelparams/parts/cad_part.py +0 -128
  89. morphopt-4.0.2/src/morphopt/shapeopt/meshgenerator.py +0 -312
  90. morphopt-4.0.2/src/morphopt/ui/widgets/stepmatrix.py +0 -207
  91. {morphopt-4.0.2 → morphopt-4.0.4}/README.md +0 -0
  92. {morphopt-4.0.2 → morphopt-4.0.4}/setup.cfg +0 -0
  93. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/__init__.py +0 -0
  94. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/codesign/__init__.py +0 -0
  95. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/codesign/constraints.py +0 -0
  96. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/codesign/offset_boundary_part.py +0 -0
  97. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/codesign/update_offset_boundary_part.py +0 -0
  98. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/opt_runner.py +0 -0
  99. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/optcore/__init__.py +0 -0
  100. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/optcore/history.py +0 -0
  101. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/optcore/modelparams/__init__.py +0 -0
  102. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/optcore/modelparams/base_model_component.py +0 -0
  103. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/optcore/modelparams/fea/__init__.py +0 -0
  104. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/optcore/modelparams/fea/base_component.py +0 -0
  105. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/optcore/modelparams/fea/body_force.py +0 -0
  106. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/optcore/modelparams/fea/boundary_condition.py +0 -0
  107. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/optcore/modelparams/fea/contact.py +0 -0
  108. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/optcore/modelparams/fea/couple.py +0 -0
  109. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/optcore/modelparams/fea/point_load.py +0 -0
  110. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/optcore/modelparams/fea/pressure.py +0 -0
  111. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/optcore/modelparams/fea/reference_point.py +0 -0
  112. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/optcore/modelparams/fea/spring.py +0 -0
  113. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/optcore/modelparams/geometry.py +0 -0
  114. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/optcore/modelparams/materials/__init__.py +0 -0
  115. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/optcore/modelparams/materials/base_material.py +0 -0
  116. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/optcore/modelparams/materials/homogeneous_material.py +0 -0
  117. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/optcore/modelparams/materials/models.py +0 -0
  118. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/optcore/modelparams/materialsparams.py +0 -0
  119. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/optcore/modelparams/params.py +0 -0
  120. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/optcore/modelparams/parts/__init__.py +0 -0
  121. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/optcore/modelparams/parts/box_part.py +0 -0
  122. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/optcore/modelparams/parts/inp_part.py +0 -0
  123. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/optcore/protocal.py +0 -0
  124. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/optcore/quadrature.py +0 -0
  125. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/optcore/updaters.py +0 -0
  126. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/postprocess/__init__.py +0 -0
  127. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/postprocess/exporters.py +0 -0
  128. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/postprocess/fields.py +0 -0
  129. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/postprocess/native.py +0 -0
  130. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/shapeopt/__init__.py +0 -0
  131. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/shapeopt/objectivefuncs/__init__.py +0 -0
  132. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/shapeopt/objectivefuncs/distancesurface.py +0 -0
  133. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/simp/__init__.py +0 -0
  134. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/simp/objectivefuncs/__init__.py +0 -0
  135. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/simp/objectivefuncs/basefuncs.py +0 -0
  136. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/simp/objectivefuncs/boundarys.py +0 -0
  137. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/simp/objectivefuncs/densityfield.py +0 -0
  138. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/simp/objectivefuncs/sensitivity.py +0 -0
  139. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/simp/objectivefuncs/volfrac.py +0 -0
  140. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/simp/simpmaterial.py +0 -0
  141. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/simp/update_simpmaterial.py +0 -0
  142. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/taskoptmization.py +0 -0
  143. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/__main__.py +0 -0
  144. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/app.py +0 -0
  145. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/application/__init__.py +0 -0
  146. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/application/devices.py +0 -0
  147. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/application/editor_routing.py +0 -0
  148. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/application/expressions.py +0 -0
  149. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/application/result_fields.py +0 -0
  150. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/application/runs.py +0 -0
  151. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/application/values.py +0 -0
  152. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/application_icon.py +0 -0
  153. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/assets/morphopt.svg +0 -0
  154. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/codegen/__init__.py +0 -0
  155. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/fea_worker.py +0 -0
  156. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/i18n.py +0 -0
  157. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/launcher.py +0 -0
  158. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/model/__init__.py +0 -0
  159. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/model/chamberlayout.py +0 -0
  160. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/model/fields.py +0 -0
  161. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/model/loaders.py +0 -0
  162. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/model/material_models.py +0 -0
  163. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/model/node_types/cad.py +0 -0
  164. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/model/node_types/custom_code.py +0 -0
  165. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/model/node_types/loads.py +0 -0
  166. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/model/node_types/materials.py +0 -0
  167. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/model/node_types/objectives.py +0 -0
  168. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/model/node_types/parts.py +0 -0
  169. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/model/node_types/penalties.py +0 -0
  170. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/model/node_types/symmetry.py +0 -0
  171. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/presentation/__init__.py +0 -0
  172. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/presentation/edit_history.py +0 -0
  173. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/presentation/history.py +0 -0
  174. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/presentation/navigation.py +0 -0
  175. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/schemes/__init__.py +0 -0
  176. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/theme/__init__.py +0 -0
  177. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/theme/manager.py +0 -0
  178. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/theme/styles/__init__.py +0 -0
  179. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/theme/styles/base.qss +0 -0
  180. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/theme/styles/controls.qss +0 -0
  181. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/theme/styles/inputs.qss +0 -0
  182. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/theme/stylesheet.py +0 -0
  183. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/theme/themes/__init__.py +0 -0
  184. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/visualization/__init__.py +0 -0
  185. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/visualization/assets.py +0 -0
  186. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/visualization/scene.py +0 -0
  187. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/widgets/__init__.py +0 -0
  188. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/widgets/codeeditor.py +0 -0
  189. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/widgets/collection_controls.py +0 -0
  190. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/widgets/controller_editor.py +0 -0
  191. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/widgets/custom_classes.py +0 -0
  192. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/widgets/editor.py +0 -0
  193. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/widgets/editor_pages.py +0 -0
  194. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/widgets/elided_label.py +0 -0
  195. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/widgets/file_dialogs.py +0 -0
  196. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/widgets/finite_element.py +0 -0
  197. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/widgets/helper_editor.py +0 -0
  198. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/widgets/language_selector.py +0 -0
  199. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/widgets/param_form.py +0 -0
  200. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/widgets/plot_style.py +0 -0
  201. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/widgets/preview_renderer.py +0 -0
  202. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/widgets/quickstart.py +0 -0
  203. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/widgets/result_path.py +0 -0
  204. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/widgets/run_dialogs.py +0 -0
  205. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/widgets/source_view.py +0 -0
  206. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/widgets/template_insert_dialog.py +0 -0
  207. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/widgets/theme_selector.py +0 -0
  208. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/ui/widgets/values.py +0 -0
  209. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/utils/__init__.py +0 -0
  210. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/utils/gradient_check.py +0 -0
  211. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt/utils/history_read.py +0 -0
  212. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt.egg-info/dependency_links.txt +0 -0
  213. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt.egg-info/entry_points.txt +0 -0
  214. {morphopt-4.0.2 → morphopt-4.0.4}/src/morphopt.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: morphopt
3
- Version: 4.0.2
3
+ Version: 4.0.4
4
4
  Summary: A morphing optimization framework using PyTorch and torchfea.
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
@@ -13,6 +13,7 @@ Requires-Dist: pyvista>=0.47.1
13
13
  Requires-Dist: pyvistaqt>=0.11.3
14
14
  Requires-Dist: vtk>=9.6.1
15
15
  Requires-Dist: gmsh>=4.15.0
16
+ Requires-Dist: cadquery-ocp>=8.0.1.1.0
16
17
  Requires-Dist: tabulate>=0.9.0
17
18
  Requires-Dist: imageio>=2.37.3
18
19
  Requires-Dist: bspmap>=1.0.3
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "morphopt"
3
- version = "4.0.2"
3
+ version = "4.0.4"
4
4
  description = "A morphing optimization framework using PyTorch and torchfea."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -14,6 +14,7 @@ dependencies = [
14
14
  "pyvistaqt>=0.11.3",
15
15
  "vtk>=9.6.1",
16
16
  "gmsh>=4.15.0",
17
+ "cadquery-ocp>=8.0.1.1.0",
17
18
  "tabulate>=0.9.0",
18
19
  "imageio>=2.37.3",
19
20
  "bspmap>=1.0.3",
@@ -18,7 +18,6 @@ class _Constants:
18
18
 
19
19
  class HISTORY:
20
20
  MAX_NODES = 100
21
- MAX_SNAPSHOT_BYTES = 67108864
22
21
 
23
22
  class OBSERVER:
24
23
  OUTPUT_MAXIMUM_BLOCKS = 10000
@@ -29,8 +28,6 @@ class _Constants:
29
28
  DEFAULT_INTER_SURFACE_DISTANCE = 2.5
30
29
 
31
30
  class WIDGETS:
32
- MINIMUM_LOAD_STEPS = 1
33
- MAXIMUM_LOAD_STEPS = 200
34
31
  MAXIMUM_QUICKSTART_OBJECTIVES = 8
35
32
  MODEL_SCAN_DEBOUNCE_MS = 500
36
33
 
@@ -487,7 +484,7 @@ class _Constants:
487
484
  DEFAULT = 1.0
488
485
  MIN = 0.0
489
486
 
490
- class FIXED_STL:
487
+ class MESH_SURFACE:
491
488
  class PATH_STL:
492
489
  DEFAULT = ""
493
490
 
@@ -985,6 +982,7 @@ class _Constants:
985
982
  class OBJECTIVE:
986
983
  TYPE_DISPLACEMENT = "displacement"
987
984
  TYPE_CUSTOM_LOSS = "custom_loss"
985
+ TYPE_WORKSPACE = "workspace"
988
986
  DEFAULT_TYPE = TYPE_DISPLACEMENT
989
987
  DIRECTION_TARGET = "target"
990
988
  DIRECTION_MAXIMIZE = "maximize"
@@ -425,10 +425,11 @@ class Controller:
425
425
  self.updater.save(
426
426
  foldpath=self.path_result + "/log/", iteration=self.history.iteration
427
427
  )
428
- self.history.save(
428
+ self.objfun.save(
429
429
  foldpath=self.path_result + "/log/", iteration=self.history.iteration
430
430
  )
431
- self.objfun.save(
431
+ # Publish a completed iteration only after its model and results exist.
432
+ self.history.save(
432
433
  foldpath=self.path_result + "/log/", iteration=self.history.iteration
433
434
  )
434
435
 
@@ -67,13 +67,17 @@ class BaseParams(
67
67
  def define_interface(self) -> None:
68
68
  """Declare the interfaces owned by this parameter collection."""
69
69
 
70
- def initialize(self, *args, **kwargs) -> None:
71
- """Initialize every registered interface."""
70
+ def initialize_interfaces(self, *args: object, **kwargs: object) -> None:
71
+ """Declare and initialize interfaces independently of analysis tasks."""
72
72
  if not self.interfaces:
73
73
  self.define_interface()
74
74
  for interface in self.interfaces.values():
75
75
  interface.initialize(*args, **kwargs)
76
76
 
77
+ def initialize(self, *args: object, **kwargs: object) -> None:
78
+ """Initialize every registered interface."""
79
+ self.initialize_interfaces(*args, **kwargs)
80
+
77
81
  def reinitialize(self, iteration: int, *args, **kwargs) -> None:
78
82
  """Re-initialize every registered interface."""
79
83
  for interface in self.interfaces.values():
@@ -35,8 +35,8 @@ class FEAParams(BaseParams):
35
35
  """
36
36
  super().__init__()
37
37
 
38
- self.fea_steps_params: list[dict[str, list[float]]] = []
39
- """load_steps (list): A list to hold the load steps."""
38
+ self.fea_steps_params: dict[str, dict[str, list[float]]] = {}
39
+ """Named load cases in registration order."""
40
40
 
41
41
  def define_steps(self):
42
42
  """Configure the amplitude of every load in every load step."""
@@ -47,48 +47,23 @@ class FEAParams(BaseParams):
47
47
  if not self.fea_steps_params:
48
48
  self.define_steps()
49
49
 
50
- def set_step_num(self, num_steps: int) -> None:
51
- """
52
- Set the number of load steps.
53
-
54
- Args:
55
- num_steps (int): The number of load steps.
56
- """
57
- self.fea_steps_params = []
58
- for _ in range(num_steps):
59
- load_step = {}
60
- for name, load_interface in self.interfaces.items():
61
- load_step[name] = load_interface._values.copy()
62
- self.fea_steps_params.append(load_step)
63
-
64
- def set_step_params(
65
- self, step_index: int, load_name: str, values: list[float]
66
- ) -> None:
67
- """
68
- Set the load parameters for a specific load step.
69
-
70
- Args:
71
- step_index (int): The index of the load step.
72
- load_name (str): The name of the load interface.
73
- values (list[float]): The load parameter values.
74
- """
75
- if step_index < 0 or step_index >= len(self.fea_steps_params):
76
- raise IndexError("step_index out of range.")
50
+ def add_step(self, name: str, values: dict[str, list[float]] | None = None) -> None:
51
+ """Register a named load case in execution order."""
52
+ if not isinstance(name, str) or not name.strip() or any(char in name for char in ("/", "\\", "\0")):
53
+ raise ValueError("A load case requires a nonempty string name.")
54
+ if name in self.fea_steps_params:
55
+ raise ValueError(f"Duplicate load case: {name!r}")
56
+ self.fea_steps_params[name] = {
57
+ key: interface._values.copy() for key, interface in self.interfaces.items()
58
+ }
59
+ for load, amplitudes in (values or {}).items():
60
+ self.set_step_params(name, load, amplitudes)
61
+
62
+ def set_step_params(self, step_name: str, load_name: str, values: list[float]) -> None:
63
+ """Assign load parameters to a registered named case."""
77
64
  if load_name not in self.interfaces:
78
- raise KeyError(f"Load interface with name '{load_name}' does not exist.")
79
- self.fea_steps_params[step_index][load_name] = values
80
-
81
- def reinitialize(self, iteration, *args, **kwargs):
82
- # Sort the load interfaces and load steps parameters by their keys
83
- sorted_fea_interfaces = dict(sorted(self.interfaces.items()))
84
- sorted_fea_steps_params = [
85
- dict(sorted(step.items())) for step in self.fea_steps_params
86
- ]
87
-
88
- # Update the dictionaries with the sorted versions
89
- self.interfaces = sorted_fea_interfaces
90
- self.fea_steps_params = sorted_fea_steps_params
91
- super().reinitialize(iteration, *args, **kwargs)
65
+ raise KeyError(f"Unknown load: {load_name!r}")
66
+ self.fea_steps_params[step_name][load_name] = list(values)
92
67
 
93
68
  @property
94
69
  def num_load_steps(self) -> int:
@@ -123,15 +98,15 @@ class FEAParams(BaseParams):
123
98
 
124
99
  return fe
125
100
 
126
- def process_fea(self, fe: FEAController, step_index: int) -> None:
101
+ def process_fea(self, fe: FEAController, step_name: str) -> None:
127
102
  """
128
103
  Process the FEA controller to update load-related information.
129
104
 
130
105
  Args:
131
106
  fe (FEAController): The FEA controller instance.
132
- step_index (int): The index of the load step.
107
+ step_name (str): The registered load-case name.
133
108
  """
134
- load_step_now = self.fea_steps_params[step_index]
109
+ load_step_now = self.fea_steps_params[step_name]
135
110
  for name, load_interface in self.interfaces.items():
136
111
  load_interface._values = load_step_now[name]
137
112
  load_interface.apply_fea_value(fe, name)
@@ -139,10 +139,9 @@ class BasePart(BaseModelComponent):
139
139
  )
140
140
 
141
141
  def initialize(self, *args: object, **kwargs: object) -> None:
142
- """Declare this Part's Instances once before assembly generation."""
142
+ """Declare Instances when absent, preserving existing declarations."""
143
143
  super().initialize(*args, **kwargs)
144
- self._instance_definitions.clear()
145
- self.define_instance()
144
+ self._ensure_instance_definitions()
146
145
 
147
146
  def build_part(
148
147
  self, path_result: str | None = None, pools: Pool | None = None
@@ -0,0 +1,370 @@
1
+ """Shared CAD and closed-boundary Part construction."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ import json
7
+ from collections.abc import Callable, Iterable, Sequence
8
+ from copy import deepcopy
9
+ from dataclasses import dataclass
10
+ from functools import partial
11
+ from pathlib import Path
12
+ from typing import TYPE_CHECKING, Any, ClassVar
13
+
14
+ import gmsh
15
+ import torchfea
16
+ from torchfea import cad as torchfea_cad
17
+ from torchfea.cad.builder import OCCBuilder
18
+
19
+ from ....constants import CONSTANT
20
+ from .base_part import BasePart
21
+
22
+ if TYPE_CHECKING:
23
+ from multiprocessing.pool import Pool
24
+
25
+ from ....shapeopt.boundary_part import BoundaryPart
26
+
27
+
28
+ class CADPart(BasePart):
29
+ """Build a Part from an ordered sequence of TorchFEA CAD operations.
30
+
31
+ Subclasses override :meth:`define_operations` and add native TorchFEA CAD
32
+ objects with :meth:`add_operation`::
33
+
34
+ class Housing(morphopt.CADPart):
35
+ def define_operations(self) -> None:
36
+ body = torchfea.cad.Box(
37
+ id="body", x=0, y=0, z=0, dx=20, dy=12, dz=8
38
+ )
39
+ bore = torchfea.cad.Cylinder(
40
+ id="bore", x=10, y=6, z=0, dz=8, radius=2
41
+ )
42
+ self.add_operation(body)
43
+ self.add_operation(bore)
44
+ self.add_operation(
45
+ torchfea.cad.Cut(
46
+ id="housing", object_id="body", tool_id="bore"
47
+ )
48
+ )
49
+
50
+ Boolean and transform operations refer to earlier operations by their
51
+ IDs. The order in which :meth:`add_operation` is called is the CAD
52
+ history order. Every OCC face and its nodes are named automatically as
53
+ ``face_N`` and ``face_N_nodes`` after meshing.
54
+ """
55
+
56
+ cache_part = True
57
+
58
+ @dataclass
59
+ class _Operation(torchfea_cad.CADOperation):
60
+ """Bridge a Part method into TorchFEA's managed Gmsh execution session."""
61
+
62
+ action: Callable[[OCCBuilder], list[tuple[int, int]]]
63
+ kind: ClassVar[str] = "morphopt_geometry"
64
+ non_parameter_fields: ClassVar[tuple[str, ...]] = ("action",)
65
+
66
+ def apply_occ(self, context: OCCBuilder) -> list[tuple[int, int]]:
67
+ """Execute the callback and return Gmsh (dimension, entity tag) pairs."""
68
+ return self.action(context)
69
+
70
+ def __init__(
71
+ self,
72
+ mesh_size: float = CONSTANT.KERNEL.MESH.DEFAULT_CAD_MESH_SIZE,
73
+ mesh_order: int = CONSTANT.KERNEL.MESH.DEFAULT_ELEMENT_ORDER,
74
+ **kwargs: Any,
75
+ ) -> None:
76
+ super().__init__(**kwargs)
77
+ if not math.isfinite(mesh_size) or mesh_size <= 0:
78
+ raise ValueError("mesh_size must be finite and positive.")
79
+ if mesh_order not in (1, 2):
80
+ raise ValueError("mesh_order must be 1 or 2.")
81
+ self.mesh_size = float(mesh_size)
82
+ self.mesh_order = mesh_order
83
+ self.operations: list[torchfea_cad.CADOperation] = []
84
+ self._face_names: dict[int, str] = {}
85
+
86
+ def define_operations(self) -> None:
87
+ """Override to declare CAD operations in their execution order."""
88
+
89
+ def add_operation(self, operation: torchfea_cad.CADOperation) -> CADPart:
90
+ """Append one native TorchFEA CAD operation to the history."""
91
+ if not isinstance(operation, torchfea_cad.CADOperation):
92
+ raise TypeError("operation must be a TorchFEA CADOperation.")
93
+ self.operations.append(operation)
94
+ return self
95
+
96
+ def initialize(self, *args: object, **kwargs: object) -> None:
97
+ """Declare Instances and the CAD history once during initialization."""
98
+ super().initialize(*args, **kwargs)
99
+ self.operations.clear()
100
+ self.define_operations()
101
+
102
+ def prepare_geometry(
103
+ self, path_result: str | None = None, pools: Pool | None = None,
104
+ ) -> list[torchfea_cad.CADOperation]:
105
+ """Provide the declared CAD operations; subclasses prepare their inputs."""
106
+ self._face_names.clear()
107
+ return self.operations
108
+
109
+ def assemble_geometry(
110
+ self, geometry: Sequence[torchfea_cad.CADOperation],
111
+ ) -> torchfea_cad.CADHistory:
112
+ """Construct the native CAD history used by the shared mesher."""
113
+ return torchfea_cad.CADHistory(
114
+ operations=geometry,
115
+ mesh_size=self.mesh_size,
116
+ exterior_surface=self.exterior_surface or "extern",
117
+ )
118
+
119
+ def mesh_geometry(self, geometry: torchfea_cad.CADHistory) -> torchfea.Part:
120
+ """Use TorchFEA's shared Gmsh mesh extraction for either geometry kernel."""
121
+ return geometry.mesh(self.mesh_size)
122
+
123
+ def define_mesh_sets(self, part: torchfea.Part) -> None:
124
+ """Map geometric face identities onto the FE mesh."""
125
+ self._name_geometry_faces(part)
126
+
127
+ def _postprocess_part(self, part: torchfea.Part) -> torchfea.Part:
128
+ """Extend the linear mesh, for example with offset-layer elements."""
129
+ return part
130
+
131
+ def postprocess_mesh(self, part: torchfea.Part) -> torchfea.Part:
132
+ """Apply element extensions before the shared quadratic conversion."""
133
+ part = self._postprocess_part(part)
134
+ if self.mesh_order == 2:
135
+ element_names = list(part.elems)
136
+ part.convert_linear_to_quadratic_elements(element_names, element_names)
137
+ return part
138
+
139
+ def build_part(
140
+ self, path_result: str | None = None, pools: Pool | None = None,
141
+ ) -> torchfea.Part:
142
+ """Prepare, assemble, mesh, name and postprocess one Part."""
143
+ geometry = self.assemble_geometry(self.prepare_geometry(path_result, pools))
144
+ part = self.mesh_geometry(geometry)
145
+ self.define_mesh_sets(part)
146
+ return self.postprocess_mesh(part)
147
+
148
+ def _complete_face_names(self, tags: Iterable[int]) -> None:
149
+ """Assign unused face names while retaining identities from source CAD."""
150
+ names = set(self._face_names.values())
151
+ index = 0
152
+ for tag in sorted(tags):
153
+ if tag in self._face_names:
154
+ continue
155
+ while f"face_{index}" in names:
156
+ index += 1
157
+ self._face_names[tag] = f"face_{index}"
158
+ names.add(f"face_{index}")
159
+
160
+ def _name_geometry_faces(self, part: torchfea.Part) -> None:
161
+ """Name geometric boundary faces using source identities or tag order."""
162
+ face_names = [
163
+ name for name in part.surfaces.keys() if name.startswith("gmsh_surface_")
164
+ ]
165
+ face_names.sort(key=lambda name: int(name.rsplit("_", 1)[1]))
166
+
167
+ tags = [int(name.rsplit("_", 1)[1]) for name in face_names]
168
+ self._complete_face_names(tags)
169
+ identities = {tag: self._face_names[tag] for tag in tags}
170
+ reserved_surfaces = set(identities.values())
171
+ reserved_nodes = {f"{name}_nodes" for name in reserved_surfaces}
172
+ surface_conflicts = reserved_surfaces.intersection(part.surfaces.keys())
173
+ node_conflicts = reserved_nodes.intersection(part.set_nodes)
174
+ if surface_conflicts or node_conflicts:
175
+ names = ", ".join(sorted(surface_conflicts | node_conflicts))
176
+ raise ValueError(
177
+ f"CAD set names {names} are reserved for OCC faces and nodes."
178
+ )
179
+
180
+ renamed_surfaces = type(part.surfaces)()
181
+ renamed_names = {
182
+ old: identities[int(old.rsplit("_", 1)[1])] for old in face_names
183
+ }
184
+ for old_name in part.surfaces.keys():
185
+ renamed_surfaces[renamed_names.get(old_name, old_name)] = part.surfaces[old_name]
186
+ part.surfaces = renamed_surfaces
187
+
188
+ for old_name in face_names:
189
+ tag = old_name.rsplit("_", 1)[1]
190
+ old_node_name = f"gmsh_nodes_{tag}"
191
+ name = identities[int(tag)]
192
+ nodes = part.set_nodes[old_node_name]
193
+
194
+ part.delete_node_set(old_node_name)
195
+ part.add_node_set(f"{name}_nodes", nodes)
196
+
197
+ def get_face_names(self) -> list[str]:
198
+ """Return geometric face identities in the preview's face order."""
199
+ history = self.assemble_geometry(self.prepare_geometry())
200
+ if not history.operations:
201
+ return []
202
+ records = history.preview()
203
+ self._complete_face_names(record["surface_tag"] for record in records)
204
+ return [self._face_names[record["surface_tag"]] for record in records]
205
+
206
+ @staticmethod
207
+ def _single_solid(
208
+ operations: Sequence[torchfea_cad.CADOperation],
209
+ ) -> tuple[int, list[int]]:
210
+ """Replay operations and return the single volume tag and sorted face tags."""
211
+ volumes = OCCBuilder(gmsh).replay(operations)
212
+ if len(volumes) != 1:
213
+ raise ValueError(
214
+ "CAD-to-boundary conversion requires one final solid. "
215
+ "Define disconnected solids as separate CADParts."
216
+ )
217
+ volume = volumes[0]
218
+ faces = sorted(tag for _, tag in gmsh.model.getBoundary(
219
+ [(3, volume)], oriented=False, recursive=False,
220
+ ))
221
+ return volume, faces
222
+
223
+ @staticmethod
224
+ def load_step_boundary(path: str | Path, context: OCCBuilder) -> list[int]:
225
+ """Import a STEP boundary while retaining its geometric faces."""
226
+ before = set(gmsh.model.getEntities(2))
227
+ imported = context.occ.importShapes(str(path))
228
+ context.occ.synchronize()
229
+ volumes = [(dim, tag) for dim, tag in imported if dim == 3]
230
+ if volumes:
231
+ context.occ.remove(volumes, recursive=False)
232
+ context.occ.synchronize()
233
+ tags = sorted(tag for _, tag in set(gmsh.model.getEntities(2)) - before)
234
+ if not tags:
235
+ raise ValueError(f"No boundary faces in {path}")
236
+ return tags
237
+
238
+ @staticmethod
239
+ def load_mesh_boundary(path: str | Path) -> list[int]:
240
+ """Import a closed STL boundary into the active geometry session."""
241
+ before = set(gmsh.model.getEntities(2))
242
+ gmsh.merge(str(path))
243
+ tags = sorted(tag for _, tag in set(gmsh.model.getEntities(2)) - before)
244
+ if not tags:
245
+ raise ValueError(f"No boundary faces in {path}")
246
+ return tags
247
+
248
+ @staticmethod
249
+ def get_face_signature(tag: int) -> list[float]:
250
+ """Describe a face by its area, centroid and inertia in local coordinates."""
251
+ return [
252
+ float(gmsh.model.occ.getMass(2, tag)),
253
+ *map(float, gmsh.model.occ.getCenterOfMass(2, tag)),
254
+ *map(float, gmsh.model.occ.getMatrixOfInertia(2, tag)),
255
+ ]
256
+
257
+ @staticmethod
258
+ def _inspect_shells(
259
+ context: OCCBuilder,
260
+ *,
261
+ operations: tuple[torchfea_cad.CADOperation, ...],
262
+ shells: list[tuple[int, ...]],
263
+ ) -> list[tuple[int, int]]:
264
+ """Fill shell face indices in exterior-first order inside a CAD session."""
265
+ volume, faces = CADPart._single_solid(operations)
266
+ indices = {tag: index for index, tag in enumerate(faces)}
267
+ loops, shell_faces = context.occ.getSurfaceLoops(volume)
268
+ ordered = []
269
+ for loop, tags in zip(loops, shell_faces):
270
+ solid = context.occ.addVolume([int(loop)])
271
+ mass = abs(context.occ.getMass(3, solid))
272
+ context.occ.remove([(3, solid)], recursive=False)
273
+ ordered.append((mass, tuple(sorted(indices[abs(int(tag))] for tag in tags))))
274
+ context.occ.synchronize()
275
+ ordered.sort(key=lambda item: (-item[0], item[1]))
276
+ shells[:] = [indices for _, indices in ordered]
277
+ if not shells:
278
+ raise ValueError("The CAD solid has no closed boundary shell.")
279
+ return [(3, volume)]
280
+
281
+ def split_shells(self) -> tuple[
282
+ tuple[torchfea_cad.CADOperation, ...],
283
+ list[tuple[int, ...]],
284
+ ]:
285
+ """Return copied operations and exterior-first closed-shell face indices.
286
+
287
+ Each shell contains zero-based indices into the source CAD face order.
288
+ """
289
+ operations = tuple(deepcopy(self.operations))
290
+ if not operations:
291
+ raise ValueError("The CAD history is empty; initialize the CADPart before conversion.")
292
+ shells: list[tuple[int, ...]] = []
293
+ action = partial(self._inspect_shells, operations=operations, shells=shells)
294
+ torchfea_cad.preview_brep([self._Operation("inspect", action)])
295
+ return operations, shells
296
+
297
+ @staticmethod
298
+ def _build_shell(
299
+ context: OCCBuilder,
300
+ *,
301
+ operations: tuple[torchfea_cad.CADOperation, ...],
302
+ face_indices: tuple[int, ...],
303
+ face_map: dict[str, list[float]],
304
+ ) -> list[tuple[int, int]]:
305
+ """Reconstruct one shell as a solid for standalone STEP export."""
306
+ volume, faces = CADPart._single_solid(operations)
307
+ context.occ.remove([(3, volume)], recursive=False)
308
+ selected = [faces[index] for index in face_indices]
309
+ loop = context.occ.addSurfaceLoop(selected)
310
+ solid = context.occ.addVolume([loop])
311
+ context.occ.synchronize()
312
+ unused = [(2, tag) for tag in faces if tag not in selected]
313
+ if unused:
314
+ context.occ.remove(unused, recursive=True)
315
+ context.occ.synchronize()
316
+ face_map.update({
317
+ f"face_{index}": CADPart.get_face_signature(tag)
318
+ for index, tag in zip(face_indices, selected)
319
+ })
320
+ return [(3, solid)]
321
+
322
+ @staticmethod
323
+ def export_shell_step(
324
+ operations: tuple[torchfea_cad.CADOperation, ...],
325
+ face_indices: tuple[int, ...],
326
+ path: str | Path,
327
+ ) -> None:
328
+ """Export a closed shell and its original face identities as STEP assets."""
329
+ path = Path(path)
330
+ face_map: dict[str, list[float]] = {}
331
+ action = partial(
332
+ CADPart._build_shell, operations=operations,
333
+ face_indices=face_indices, face_map=face_map,
334
+ )
335
+ torchfea_cad.export_step([CADPart._Operation("shell", action)], str(path))
336
+ path.with_suffix(".brep.json").write_text(
337
+ json.dumps({"face_map": face_map}, indent=2), encoding="utf-8",
338
+ )
339
+
340
+ def to_boundary_part(
341
+ self, asset_directory: str | Path = "morphopt_assets",
342
+ ) -> BoundaryPart:
343
+ """Export an initialized single-solid CAD Part as fixed STEP boundaries.
344
+
345
+ Write one STEP and face map per shell under asset_directory/part_name.
346
+ Preserve original face identities, mesh settings and Instance poses;
347
+ the resulting surfaces retain file references, not source CAD history.
348
+ """
349
+ from ....shapeopt.boundary_part import BoundaryPart
350
+ from ....shapeopt.surfaces.brep_surface import BREPSurface
351
+
352
+ operations, shells = self.split_shells()
353
+ directory = Path(asset_directory) / self.part_name
354
+ directory.mkdir(parents=True, exist_ok=True)
355
+ boundary = BoundaryPart(
356
+ fea_seed_size=self.mesh_size,
357
+ mesh_order=self.mesh_order,
358
+ part_name=self.part_name,
359
+ exterior_surface=self.exterior_surface or "extern",
360
+ )
361
+ boundary._name = self.name
362
+ boundary._instance_definitions = deepcopy(self._instance_definitions)
363
+ for index, face_indices in enumerate(shells):
364
+ path = directory / f"surface_{index}.stp"
365
+ self.export_shell_step(operations, face_indices, path)
366
+ surface = BREPSurface(path)
367
+ surface.flip = index != 0
368
+ boundary.add_surface_interface(surface)
369
+ boundary.initialize()
370
+ return boundary