ANYfeSolver 0.5.0a1__py3-none-any.whl

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 (751) hide show
  1. anyfesolver/__init__.py +1075 -0
  2. anyfesolver/_b2_attachment_elimination_dev.py +261 -0
  3. anyfesolver/_b2_f10_f11_point_attachment.py +163 -0
  4. anyfesolver/_b2_loaded_point_trace_dev.py +173 -0
  5. anyfesolver/_b2_physical_flexibility_linear.py +707 -0
  6. anyfesolver/_b2_physical_linear_batch_dev.py +491 -0
  7. anyfesolver/_b2_physical_linear_definition.py +415 -0
  8. anyfesolver/_b2_physical_load_bubbles.py +282 -0
  9. anyfesolver/_compensated_contractions.py +497 -0
  10. anyfesolver/_compensated_native.py +437 -0
  11. anyfesolver/_corotational_batch.py +219 -0
  12. anyfesolver/_director_trace.py +214 -0
  13. anyfesolver/_dyadic_bilinear.py +76 -0
  14. anyfesolver/_dyadic_factor_chain.py +25 -0
  15. anyfesolver/_ge_beam3_adaptive_arc_program.py +286 -0
  16. anyfesolver/_ge_beam3_analysis_combined.py +102 -0
  17. anyfesolver/_ge_beam3_analysis_fibre_translation.py +130 -0
  18. anyfesolver/_ge_beam3_analysis_modal_snapshot.py +27 -0
  19. anyfesolver/_ge_beam3_analysis_nodal_program.py +140 -0
  20. anyfesolver/_ge_beam3_analysis_translation.py +147 -0
  21. anyfesolver/_ge_beam3_analysis_translation_modal.py +84 -0
  22. anyfesolver/_ge_beam3_arc_geometry.py +89 -0
  23. anyfesolver/_ge_beam3_arc_program.py +304 -0
  24. anyfesolver/_ge_beam3_authoritative_cell_seed.py +25 -0
  25. anyfesolver/_ge_beam3_cell_identity.py +52 -0
  26. anyfesolver/_ge_beam3_centered_batched_finite_inertia.py +132 -0
  27. anyfesolver/_ge_beam3_centered_finite_inertia.py +171 -0
  28. anyfesolver/_ge_beam3_centered_mixed.py +104 -0
  29. anyfesolver/_ge_beam3_centered_reference.py +187 -0
  30. anyfesolver/_ge_beam3_centered_so3_strain.py +76 -0
  31. anyfesolver/_ge_beam3_complementary_section.py +122 -0
  32. anyfesolver/_ge_beam3_compliance_snapshot_guard.py +20 -0
  33. anyfesolver/_ge_beam3_connected_dynamic.py +486 -0
  34. anyfesolver/_ge_beam3_connected_trajectory.py +258 -0
  35. anyfesolver/_ge_beam3_conservative_backtrack.py +26 -0
  36. anyfesolver/_ge_beam3_controlled_fibre_modes.py +105 -0
  37. anyfesolver/_ge_beam3_coupled_beam_subdomain.py +128 -0
  38. anyfesolver/_ge_beam3_coupled_modes.py +256 -0
  39. anyfesolver/_ge_beam3_deterministic_reduction.py +264 -0
  40. anyfesolver/_ge_beam3_displacement_program.py +255 -0
  41. anyfesolver/_ge_beam3_durable_coupled.py +251 -0
  42. anyfesolver/_ge_beam3_dynamic_elastic_state.py +461 -0
  43. anyfesolver/_ge_beam3_effective_deadline.py +58 -0
  44. anyfesolver/_ge_beam3_elastic_seed_continuation.py +223 -0
  45. anyfesolver/_ge_beam3_elastic_seed_modal.py +139 -0
  46. anyfesolver/_ge_beam3_equilibrium_line_program.py +98 -0
  47. anyfesolver/_ge_beam3_equilibrium_schur_solver.py +96 -0
  48. anyfesolver/_ge_beam3_factor_chain_modes.py +103 -0
  49. anyfesolver/_ge_beam3_fibre_cell.py +408 -0
  50. anyfesolver/_ge_beam3_fibre_distributed_couples.py +191 -0
  51. anyfesolver/_ge_beam3_fibre_kinematic_cell.py +75 -0
  52. anyfesolver/_ge_beam3_fibre_line_program.py +157 -0
  53. anyfesolver/_ge_beam3_fibre_line_work.py +87 -0
  54. anyfesolver/_ge_beam3_fibre_reference_modal.py +104 -0
  55. anyfesolver/_ge_beam3_fibre_schur_solver.py +134 -0
  56. anyfesolver/_ge_beam3_fibre_section.py +286 -0
  57. anyfesolver/_ge_beam3_fibre_static_boundary.py +113 -0
  58. anyfesolver/_ge_beam3_g1_analysis.py +307 -0
  59. anyfesolver/_ge_beam3_g1_elastic.py +161 -0
  60. anyfesolver/_ge_beam3_g1_element.py +185 -0
  61. anyfesolver/_ge_beam3_g1_operator.py +137 -0
  62. anyfesolver/_ge_beam3_g2_analysis.py +176 -0
  63. anyfesolver/_ge_beam3_g2_constraints.py +162 -0
  64. anyfesolver/_ge_beam3_g3_analysis.py +385 -0
  65. anyfesolver/_ge_beam3_g3_constraints.py +168 -0
  66. anyfesolver/_ge_beam3_g3b_owner.py +296 -0
  67. anyfesolver/_ge_beam3_g3b_q4_reference.py +186 -0
  68. anyfesolver/_ge_beam3_g3b_reference.py +206 -0
  69. anyfesolver/_ge_beam3_g3b_result_schema.py +107 -0
  70. anyfesolver/_ge_beam3_g3b_s3_reference.py +185 -0
  71. anyfesolver/_ge_beam3_g3b_weighted_reference.py +193 -0
  72. anyfesolver/_ge_beam3_g3c_affine_q4_chart.py +58 -0
  73. anyfesolver/_ge_beam3_g3c_affine_q4_increment_chart.py +257 -0
  74. anyfesolver/_ge_beam3_g3c_affine_q4_recovery.py +434 -0
  75. anyfesolver/_ge_beam3_g3c_affine_q4_registry.py +127 -0
  76. anyfesolver/_ge_beam3_g3c_b2_physical.py +153 -0
  77. anyfesolver/_ge_beam3_g3c_b2_physical_adapter.py +160 -0
  78. anyfesolver/_ge_beam3_g3c_definition.py +117 -0
  79. anyfesolver/_ge_beam3_g3c_local_beam.py +188 -0
  80. anyfesolver/_ge_beam3_g3c_local_shell.py +371 -0
  81. anyfesolver/_ge_beam3_g3c_owner.py +400 -0
  82. anyfesolver/_ge_beam3_g3c_physical_authority.py +203 -0
  83. anyfesolver/_ge_beam3_g3c_physical_owner.py +556 -0
  84. anyfesolver/_ge_beam3_g3c_recovery.py +91 -0
  85. anyfesolver/_ge_beam3_g3c_so3_numerics.py +81 -0
  86. anyfesolver/_ge_beam3_g3c_stable/__init__.py +1 -0
  87. anyfesolver/_ge_beam3_g3c_stable/authority.py +152 -0
  88. anyfesolver/_ge_beam3_g3c_stable/beam.py +196 -0
  89. anyfesolver/_ge_beam3_g3c_stable/boundary.py +38 -0
  90. anyfesolver/_ge_beam3_g3c_stable/chart.py +23 -0
  91. anyfesolver/_ge_beam3_g3c_stable/compensated.py +46 -0
  92. anyfesolver/_ge_beam3_g3c_stable/element.py +190 -0
  93. anyfesolver/_ge_beam3_g3c_stable/fit.py +54 -0
  94. anyfesolver/_ge_beam3_g3c_stable/joint.py +255 -0
  95. anyfesolver/_ge_beam3_g3c_stable/line_work.py +92 -0
  96. anyfesolver/_ge_beam3_g3c_stable/operator.py +147 -0
  97. anyfesolver/_ge_beam3_g3c_stable/owner.py +415 -0
  98. anyfesolver/_ge_beam3_g3c_stable/shell.py +358 -0
  99. anyfesolver/_ge_beam3_g4_completion.py +186 -0
  100. anyfesolver/_ge_beam3_g4a_station_transaction.py +385 -0
  101. anyfesolver/_ge_beam3_g5_completion.py +68 -0
  102. anyfesolver/_ge_beam3_g6_domain.py +354 -0
  103. anyfesolver/_ge_beam3_generalized_cell.py +172 -0
  104. anyfesolver/_ge_beam3_generalized_ellipsoid_section.py +215 -0
  105. anyfesolver/_ge_beam3_generalized_static_boundary.py +233 -0
  106. anyfesolver/_ge_beam3_lifted_kinetic_factor.py +26 -0
  107. anyfesolver/_ge_beam3_line_fibre_modes.py +121 -0
  108. anyfesolver/_ge_beam3_load_program.py +275 -0
  109. anyfesolver/_ge_beam3_loaded_modal.py +225 -0
  110. anyfesolver/_ge_beam3_mixed_ad.py +286 -0
  111. anyfesolver/_ge_beam3_native_analysis.py +484 -0
  112. anyfesolver/_ge_beam3_native_arc.py +269 -0
  113. anyfesolver/_ge_beam3_native_arc_adaptive.py +251 -0
  114. anyfesolver/_ge_beam3_native_arc_restart.py +88 -0
  115. anyfesolver/_ge_beam3_native_arc_source.py +62 -0
  116. anyfesolver/_ge_beam3_native_buckling.py +206 -0
  117. anyfesolver/_ge_beam3_native_combined_couples.py +179 -0
  118. anyfesolver/_ge_beam3_native_combined_restart.py +205 -0
  119. anyfesolver/_ge_beam3_native_definition.py +238 -0
  120. anyfesolver/_ge_beam3_native_distributed_element.py +220 -0
  121. anyfesolver/_ge_beam3_native_distributed_loading.py +92 -0
  122. anyfesolver/_ge_beam3_native_distributed_program.py +165 -0
  123. anyfesolver/_ge_beam3_native_distributed_recovery.py +34 -0
  124. anyfesolver/_ge_beam3_native_distributed_restart.py +221 -0
  125. anyfesolver/_ge_beam3_native_dynamic_analysis.py +149 -0
  126. anyfesolver/_ge_beam3_native_dynamic_restart.py +261 -0
  127. anyfesolver/_ge_beam3_native_fibre_current_modal.py +104 -0
  128. anyfesolver/_ge_beam3_native_fibre_recovery.py +34 -0
  129. anyfesolver/_ge_beam3_native_fibre_restart.py +211 -0
  130. anyfesolver/_ge_beam3_native_fibre_static_element.py +193 -0
  131. anyfesolver/_ge_beam3_native_force_adaptation.py +56 -0
  132. anyfesolver/_ge_beam3_native_generalized_combined_couples.py +188 -0
  133. anyfesolver/_ge_beam3_native_generalized_combined_restart.py +207 -0
  134. anyfesolver/_ge_beam3_native_generalized_element.py +222 -0
  135. anyfesolver/_ge_beam3_native_generalized_factor_modal.py +122 -0
  136. anyfesolver/_ge_beam3_native_generalized_loading.py +92 -0
  137. anyfesolver/_ge_beam3_native_generalized_modal.py +274 -0
  138. anyfesolver/_ge_beam3_native_generalized_paired_modal.py +13 -0
  139. anyfesolver/_ge_beam3_native_generalized_parameter.py +89 -0
  140. anyfesolver/_ge_beam3_native_generalized_program.py +196 -0
  141. anyfesolver/_ge_beam3_native_generalized_recovery.py +34 -0
  142. anyfesolver/_ge_beam3_native_generalized_restart.py +243 -0
  143. anyfesolver/_ge_beam3_native_history_profile.py +53 -0
  144. anyfesolver/_ge_beam3_native_line_loading.py +93 -0
  145. anyfesolver/_ge_beam3_native_line_program.py +150 -0
  146. anyfesolver/_ge_beam3_native_line_recovery.py +34 -0
  147. anyfesolver/_ge_beam3_native_line_restart.py +197 -0
  148. anyfesolver/_ge_beam3_native_line_static_element.py +220 -0
  149. anyfesolver/_ge_beam3_native_load_admission.py +37 -0
  150. anyfesolver/_ge_beam3_native_midpoint.py +1004 -0
  151. anyfesolver/_ge_beam3_native_multi_shell_contact.py +539 -0
  152. anyfesolver/_ge_beam3_native_pair_contact.py +236 -0
  153. anyfesolver/_ge_beam3_native_shell_contact.py +357 -0
  154. anyfesolver/_ge_beam3_native_spatial_couples.py +176 -0
  155. anyfesolver/_ge_beam3_native_spatial_restart.py +204 -0
  156. anyfesolver/_ge_beam3_native_transient.py +467 -0
  157. anyfesolver/_ge_beam3_native_translation.py +225 -0
  158. anyfesolver/_ge_beam3_native_translation_restart.py +54 -0
  159. anyfesolver/_ge_beam3_operator_validation_scope.py +53 -0
  160. anyfesolver/_ge_beam3_p5/__init__.py +11 -0
  161. anyfesolver/_ge_beam3_p5/algebra.py +91 -0
  162. anyfesolver/_ge_beam3_p5/arrays.py +29 -0
  163. anyfesolver/_ge_beam3_p5/chart.py +45 -0
  164. anyfesolver/_ge_beam3_p5/codec.py +82 -0
  165. anyfesolver/_ge_beam3_p5/committed_modal.py +191 -0
  166. anyfesolver/_ge_beam3_p5/compensated.py +118 -0
  167. anyfesolver/_ge_beam3_p5/compensated_coordinates.py +44 -0
  168. anyfesolver/_ge_beam3_p5/core.py +159 -0
  169. anyfesolver/_ge_beam3_p5/element.py +207 -0
  170. anyfesolver/_ge_beam3_p5/factors.py +61 -0
  171. anyfesolver/_ge_beam3_p5/inertia.py +120 -0
  172. anyfesolver/_ge_beam3_p5/mass.py +70 -0
  173. anyfesolver/_ge_beam3_p5/mixed.py +256 -0
  174. anyfesolver/_ge_beam3_p5/reference_modal.py +73 -0
  175. anyfesolver/_ge_beam3_p5/section.py +139 -0
  176. anyfesolver/_ge_beam3_p5/typed_schema.py +111 -0
  177. anyfesolver/_ge_beam3_p5_centered/__init__.py +6 -0
  178. anyfesolver/_ge_beam3_p5_centered/codec.py +84 -0
  179. anyfesolver/_ge_beam3_p5_centered/committed_modal.py +192 -0
  180. anyfesolver/_ge_beam3_p5_centered/core.py +171 -0
  181. anyfesolver/_ge_beam3_p5_centered/element.py +216 -0
  182. anyfesolver/_ge_beam3_p5_centered/mass.py +99 -0
  183. anyfesolver/_ge_beam3_p5_centered/positions.py +34 -0
  184. anyfesolver/_ge_beam3_p5_centered/reference_modal.py +73 -0
  185. anyfesolver/_ge_beam3_p5_coordinates/__init__.py +6 -0
  186. anyfesolver/_ge_beam3_p5_coordinates/codec.py +84 -0
  187. anyfesolver/_ge_beam3_p5_coordinates/committed_modal.py +192 -0
  188. anyfesolver/_ge_beam3_p5_coordinates/core.py +163 -0
  189. anyfesolver/_ge_beam3_p5_coordinates/element.py +213 -0
  190. anyfesolver/_ge_beam3_p5_coordinates/positions.py +102 -0
  191. anyfesolver/_ge_beam3_p5_coordinates/reference_modal.py +73 -0
  192. anyfesolver/_ge_beam3_p5_loads/__init__.py +7 -0
  193. anyfesolver/_ge_beam3_p5_loads/codec.py +85 -0
  194. anyfesolver/_ge_beam3_p5_loads/core.py +192 -0
  195. anyfesolver/_ge_beam3_p5_loads/element.py +243 -0
  196. anyfesolver/_ge_beam3_p5_loads/state.py +120 -0
  197. anyfesolver/_ge_beam3_p5_loads/work.py +45 -0
  198. anyfesolver/_ge_beam3_p5_seeded/__init__.py +9 -0
  199. anyfesolver/_ge_beam3_p5_seeded/codec.py +87 -0
  200. anyfesolver/_ge_beam3_p5_seeded/core.py +197 -0
  201. anyfesolver/_ge_beam3_p5_seeded/element.py +245 -0
  202. anyfesolver/_ge_beam3_p5_seeded/state.py +122 -0
  203. anyfesolver/_ge_beam3_p5_seeded/work.py +47 -0
  204. anyfesolver/_ge_beam3_pose_joint.py +268 -0
  205. anyfesolver/_ge_beam3_precise_geometric_work.py +147 -0
  206. anyfesolver/_ge_beam3_reassembled_signed_modes.py +101 -0
  207. anyfesolver/_ge_beam3_reference_identity.py +92 -0
  208. anyfesolver/_ge_beam3_refined_fibre_schur_solver.py +104 -0
  209. anyfesolver/_ge_beam3_refinement_capacity.py +19 -0
  210. anyfesolver/_ge_beam3_relative_virgin_reference_modes.py +60 -0
  211. anyfesolver/_ge_beam3_retained_arc.py +350 -0
  212. anyfesolver/_ge_beam3_retained_arc_geometry.py +66 -0
  213. anyfesolver/_ge_beam3_retained_arc_modal.py +125 -0
  214. anyfesolver/_ge_beam3_retained_elastic.py +134 -0
  215. anyfesolver/_ge_beam3_retained_fibre.py +209 -0
  216. anyfesolver/_ge_beam3_retained_fibre_control.py +270 -0
  217. anyfesolver/_ge_beam3_retained_fibre_element.py +58 -0
  218. anyfesolver/_ge_beam3_retained_fibre_modal.py +155 -0
  219. anyfesolver/_ge_beam3_retained_fibre_modes.py +189 -0
  220. anyfesolver/_ge_beam3_retained_fibre_program.py +68 -0
  221. anyfesolver/_ge_beam3_retained_fibre_state.py +268 -0
  222. anyfesolver/_ge_beam3_retained_force_program.py +72 -0
  223. anyfesolver/_ge_beam3_retained_generalized.py +186 -0
  224. anyfesolver/_ge_beam3_retained_generalized_modal.py +171 -0
  225. anyfesolver/_ge_beam3_retained_generalized_program.py +63 -0
  226. anyfesolver/_ge_beam3_retained_generalized_state.py +281 -0
  227. anyfesolver/_ge_beam3_retained_nodal_loading.py +111 -0
  228. anyfesolver/_ge_beam3_retained_plastic.py +116 -0
  229. anyfesolver/_ge_beam3_retained_plastic_modes.py +144 -0
  230. anyfesolver/_ge_beam3_retained_plastic_program.py +69 -0
  231. anyfesolver/_ge_beam3_retained_plastic_state.py +165 -0
  232. anyfesolver/_ge_beam3_retained_state.py +194 -0
  233. anyfesolver/_ge_beam3_retained_translation_control.py +345 -0
  234. anyfesolver/_ge_beam3_retained_translation_modal.py +137 -0
  235. anyfesolver/_ge_beam3_schur_line_program.py +98 -0
  236. anyfesolver/_ge_beam3_seeded_fibre_control.py +175 -0
  237. anyfesolver/_ge_beam3_seeded_load_program.py +301 -0
  238. anyfesolver/_ge_beam3_seeded_loaded_modal.py +226 -0
  239. anyfesolver/_ge_beam3_seeded_signed_modes.py +150 -0
  240. anyfesolver/_ge_beam3_shared_kinematic_split.py +53 -0
  241. anyfesolver/_ge_beam3_shell_joint_state.py +396 -0
  242. anyfesolver/_ge_beam3_shell_joint_trial.py +217 -0
  243. anyfesolver/_ge_beam3_signed_loaded_modes.py +140 -0
  244. anyfesolver/_ge_beam3_spatial_fibre_program.py +113 -0
  245. anyfesolver/_ge_beam3_spatial_nodal_moments.py +38 -0
  246. anyfesolver/_ge_beam3_station_resultant_cell.py +235 -0
  247. anyfesolver/_ge_beam3_variational_shell.py +211 -0
  248. anyfesolver/_ge_beam3_virgin_reference_modes.py +60 -0
  249. anyfesolver/_impact_coupling.py +162 -0
  250. anyfesolver/_native_authority_dag.py +77 -0
  251. anyfesolver/_native_dyadic_interval_inertia.py +64 -0
  252. anyfesolver/_native_exact_shift_chain_modes.py +140 -0
  253. anyfesolver/_native_exact_shift_inertia.py +86 -0
  254. anyfesolver/_native_factor_chain_modes.py +192 -0
  255. anyfesolver/_native_guard.cpp +828 -0
  256. anyfesolver/_native_guard_dispatch.py +140 -0
  257. anyfesolver/_native_large_factor_chain.py +55 -0
  258. anyfesolver/_native_material_protocol.py +51 -0
  259. anyfesolver/_native_midpoint_controller.py +90 -0
  260. anyfesolver/_native_modal_capacity.py +26 -0
  261. anyfesolver/_native_paired_factor_chain_modes.py +244 -0
  262. anyfesolver/_native_reassembled_factor_modes.py +185 -0
  263. anyfesolver/_native_reference_factor_spectrum.py +102 -0
  264. anyfesolver/_native_reference_modal.py +180 -0
  265. anyfesolver/_native_relative_factor_chain_modes.py +204 -0
  266. anyfesolver/_native_relative_reference_factor_spectrum.py +110 -0
  267. anyfesolver/_native_retained_step.py +15 -0
  268. anyfesolver/_native_rotation_state.py +797 -0
  269. anyfesolver/_native_signed_factor_modes.py +257 -0
  270. anyfesolver/_native_signed_kinetic_factor_modes.py +279 -0
  271. anyfesolver/_native_stationary_spectrum.py +79 -0
  272. anyfesolver/_nonlinear_events.py +150 -0
  273. anyfesolver/_nonlinear_seed.py +80 -0
  274. anyfesolver/_paired_modal_arithmetic.py +71 -0
  275. anyfesolver/_plate_stability_continuation.py +171 -0
  276. anyfesolver/_q4_assumed_strain_plasticity.py +502 -0
  277. anyfesolver/_qualified_authority_epoch.py +561 -0
  278. anyfesolver/_relative_reference_svd.py +44 -0
  279. anyfesolver/_stiffened_shell_affine_endpoint.py +232 -0
  280. anyfesolver/_stiffened_shell_algorithmic_pullback.py +38 -0
  281. anyfesolver/_stiffened_shell_arc_checkpoint.py +422 -0
  282. anyfesolver/_stiffened_shell_arc_control.py +108 -0
  283. anyfesolver/_stiffened_shell_arc_metric.py +222 -0
  284. anyfesolver/_stiffened_shell_arc_publication.py +153 -0
  285. anyfesolver/_stiffened_shell_arc_trial.py +310 -0
  286. anyfesolver/_stiffened_shell_b2_affine_resolution.py +101 -0
  287. anyfesolver/_stiffened_shell_b2_assembly_diagnostics.py +172 -0
  288. anyfesolver/_stiffened_shell_b2_definition_ownership.py +189 -0
  289. anyfesolver/_stiffened_shell_b2_experimental.py +153 -0
  290. anyfesolver/_stiffened_shell_b2_nodal_footprint.py +273 -0
  291. anyfesolver/_stiffened_shell_b2_physical_rigid_footprint.py +237 -0
  292. anyfesolver/_stiffened_shell_b2_q4_panel_experimental.py +2447 -0
  293. anyfesolver/_stiffened_shell_b2_q4_point_experimental.py +251 -0
  294. anyfesolver/_stiffened_shell_b2_reference_buckling.py +413 -0
  295. anyfesolver/_stiffened_shell_b3_chart.py +70 -0
  296. anyfesolver/_stiffened_shell_b3_local_adapter.py +141 -0
  297. anyfesolver/_stiffened_shell_beam_history.py +107 -0
  298. anyfesolver/_stiffened_shell_bl5_local_experimental.py +173 -0
  299. anyfesolver/_stiffened_shell_bl5_native_elastic.py +28 -0
  300. anyfesolver/_stiffened_shell_branch_event_trial.py +58 -0
  301. anyfesolver/_stiffened_shell_consumer_capabilities.py +124 -0
  302. anyfesolver/_stiffened_shell_coordinate_support.py +110 -0
  303. anyfesolver/_stiffened_shell_displacement_adaptive.py +327 -0
  304. anyfesolver/_stiffened_shell_displacement_checkpoint.py +360 -0
  305. anyfesolver/_stiffened_shell_displacement_control.py +326 -0
  306. anyfesolver/_stiffened_shell_displacement_corrector.py +293 -0
  307. anyfesolver/_stiffened_shell_displacement_increment.py +121 -0
  308. anyfesolver/_stiffened_shell_distributing_footprint.py +354 -0
  309. anyfesolver/_stiffened_shell_distributing_pose.py +267 -0
  310. anyfesolver/_stiffened_shell_f06_increment_arithmetic.py +205 -0
  311. anyfesolver/_stiffened_shell_ge_body_load.py +300 -0
  312. anyfesolver/_stiffened_shell_ge_checkpoint_binary.py +472 -0
  313. anyfesolver/_stiffened_shell_ge_distributed_load.py +210 -0
  314. anyfesolver/_stiffened_shell_ge_distributed_programme.py +175 -0
  315. anyfesolver/_stiffened_shell_ge_distributing_finite.py +242 -0
  316. anyfesolver/_stiffened_shell_ge_distributing_reference.py +296 -0
  317. anyfesolver/_stiffened_shell_ge_experimental.py +2454 -0
  318. anyfesolver/_stiffened_shell_ge_finite_experimental.py +2714 -0
  319. anyfesolver/_stiffened_shell_ge_mixed_modal_mass.py +33 -0
  320. anyfesolver/_stiffened_shell_ge_point_buckling.py +723 -0
  321. anyfesolver/_stiffened_shell_ge_pressure_load.py +366 -0
  322. anyfesolver/_stiffened_shell_ge_reference_modal.py +580 -0
  323. anyfesolver/_stiffened_shell_generalized_recovery.py +150 -0
  324. anyfesolver/_stiffened_shell_increment_control.py +217 -0
  325. anyfesolver/_stiffened_shell_l1_assembly_experimental.py +420 -0
  326. anyfesolver/_stiffened_shell_l1_local_experimental.py +284 -0
  327. anyfesolver/_stiffened_shell_l1_supported_work_experimental.py +146 -0
  328. anyfesolver/_stiffened_shell_native_distributed_load.py +120 -0
  329. anyfesolver/_stiffened_shell_native_gravity.py +236 -0
  330. anyfesolver/_stiffened_shell_nodal_footprint.py +255 -0
  331. anyfesolver/_stiffened_shell_physical_q4_mass.py +98 -0
  332. anyfesolver/_stiffened_shell_physical_s3_mass.py +53 -0
  333. anyfesolver/_stiffened_shell_point_recovery.py +49 -0
  334. anyfesolver/_stiffened_shell_q4_algorithmic_pullback.py +88 -0
  335. anyfesolver/_stiffened_shell_q4_material_event.py +114 -0
  336. anyfesolver/_stiffened_shell_q4_reference.py +65 -0
  337. anyfesolver/_stiffened_shell_rotation_charts.py +58 -0
  338. anyfesolver/_stiffened_shell_s3_algorithmic_pullback.py +59 -0
  339. anyfesolver/_stiffened_shell_s3_conservative_buckling.py +28 -0
  340. anyfesolver/_stiffened_shell_s3_dead_work_candidate.py +107 -0
  341. anyfesolver/_stiffened_shell_s3_material_event.py +30 -0
  342. anyfesolver/_stiffened_shell_same_parent_trial.py +224 -0
  343. anyfesolver/_stiffened_shell_selected_recovery.py +113 -0
  344. anyfesolver/_stiffened_shell_staged_load_experimental.py +392 -0
  345. anyfesolver/_stiffened_shell_stress_free_reference.py +204 -0
  346. anyfesolver/_wire_types.py +17 -0
  347. anyfesolver/activity.py +1537 -0
  348. anyfesolver/algebraic_dynamics.py +2864 -0
  349. anyfesolver/analysis_session.py +897 -0
  350. anyfesolver/anystructure_fem_mode.py +1637 -0
  351. anyfesolver/arc_length.py +2587 -0
  352. anyfesolver/assembly.py +1830 -0
  353. anyfesolver/b3_ge.py +179 -0
  354. anyfesolver/baselines.py +303 -0
  355. anyfesolver/beam_sections.py +292 -0
  356. anyfesolver/beam_shell_verification.py +4685 -0
  357. anyfesolver/beam_validity.py +110 -0
  358. anyfesolver/benchmarks.py +495 -0
  359. anyfesolver/boundary.py +1680 -0
  360. anyfesolver/buckling.py +2136 -0
  361. anyfesolver/buckling_validity.py +91 -0
  362. anyfesolver/capacity_workflow.py +431 -0
  363. anyfesolver/capsule_runtime_guard.py +99 -0
  364. anyfesolver/cases.py +173 -0
  365. anyfesolver/composite_strip_verification.py +297 -0
  366. anyfesolver/concentrated_mass.py +84 -0
  367. anyfesolver/constraint_audit.py +553 -0
  368. anyfesolver/contact.py +4785 -0
  369. anyfesolver/contact_performance.py +301 -0
  370. anyfesolver/control.py +206 -0
  371. anyfesolver/corotational.py +1935 -0
  372. anyfesolver/corotational_performance.py +157 -0
  373. anyfesolver/current_state_tangent.py +2780 -0
  374. anyfesolver/cylinder_benchmarks.py +369 -0
  375. anyfesolver/damage_matrix_performance.py +617 -0
  376. anyfesolver/dynamics.py +2833 -0
  377. anyfesolver/e4_pl_element.py +8265 -0
  378. anyfesolver/e4_pl_enriched.py +194 -0
  379. anyfesolver/e4_pl_enriched_nonlinear.py +159 -0
  380. anyfesolver/e4_pl_s3_element.py +9752 -0
  381. anyfesolver/e4_pl_s3_initial_fields.py +101 -0
  382. anyfesolver/e4_pl_s3_state.py +4723 -0
  383. anyfesolver/e4_pl_s3_v2_element.py +1844 -0
  384. anyfesolver/e4_pl_s3_v2b_element.py +1936 -0
  385. anyfesolver/e4_pl_s3_v2c_element.py +2196 -0
  386. anyfesolver/e4_pl_s3_v2d_element.py +2621 -0
  387. anyfesolver/e4_pl_s3_v2d_state.py +376 -0
  388. anyfesolver/element_capabilities.py +159 -0
  389. anyfesolver/element_qualification.py +432 -0
  390. anyfesolver/elements.py +6327 -0
  391. anyfesolver/experimental_csr_assembly.py +238 -0
  392. anyfesolver/external_references.py +1253 -0
  393. anyfesolver/fe_core.py +832 -0
  394. anyfesolver/floating_pressure_regions.py +119 -0
  395. anyfesolver/floating_response.py +486 -0
  396. anyfesolver/floating_transient.py +1281 -0
  397. anyfesolver/fracture.py +621 -0
  398. anyfesolver/ge_beam3_curved_reference.py +725 -0
  399. anyfesolver/ge_beam3_element.py +885 -0
  400. anyfesolver/ge_beam3_mixed_element.py +1216 -0
  401. anyfesolver/ge_beam3_mixed_state.py +814 -0
  402. anyfesolver/ge_beam3_native.py +129 -0
  403. anyfesolver/ge_beam3_native_connected_dynamics.py +24 -0
  404. anyfesolver/ge_beam3_native_dynamics.py +70 -0
  405. anyfesolver/ge_beam3_state.py +672 -0
  406. anyfesolver/general_contact/__init__.py +151 -0
  407. anyfesolver/general_contact/_enriched_multitarget_empty.py +202 -0
  408. anyfesolver/general_contact/_enriched_multitarget_trial.py +183 -0
  409. anyfesolver/general_contact/_results_archive_common.py +65 -0
  410. anyfesolver/general_contact/algebraic_path.py +206 -0
  411. anyfesolver/general_contact/arc_contact.py +237 -0
  412. anyfesolver/general_contact/arc_results_archive.py +158 -0
  413. anyfesolver/general_contact/augmentation.py +157 -0
  414. anyfesolver/general_contact/automatic_domain.py +621 -0
  415. anyfesolver/general_contact/beam_chain_consistent.py +357 -0
  416. anyfesolver/general_contact/beam_chain_end_geometry.py +237 -0
  417. anyfesolver/general_contact/beam_chain_ends.py +220 -0
  418. anyfesolver/general_contact/beam_chain_pair.py +416 -0
  419. anyfesolver/general_contact/beam_curve_end_geometry.py +254 -0
  420. anyfesolver/general_contact/beam_curve_ends.py +237 -0
  421. anyfesolver/general_contact/beam_curve_pair.py +794 -0
  422. anyfesolver/general_contact/beam_curve_pair_static.py +303 -0
  423. anyfesolver/general_contact/beam_end_radial_geometry.py +221 -0
  424. anyfesolver/general_contact/beam_end_topology.py +15 -0
  425. anyfesolver/general_contact/beam_hemisphere_coupled_tool.py +61 -0
  426. anyfesolver/general_contact/beam_hemisphere_cylinder.py +134 -0
  427. anyfesolver/general_contact/beam_hemisphere_cylinder_motion.py +89 -0
  428. anyfesolver/general_contact/beam_hemisphere_cylinder_work.py +216 -0
  429. anyfesolver/general_contact/beam_hemisphere_plane.py +175 -0
  430. anyfesolver/general_contact/beam_hemisphere_s3_support.py +152 -0
  431. anyfesolver/general_contact/beam_hemisphere_sphere.py +135 -0
  432. anyfesolver/general_contact/beam_hemisphere_sphere_work.py +114 -0
  433. anyfesolver/general_contact/beam_hemisphere_support.py +211 -0
  434. anyfesolver/general_contact/beam_pair.py +523 -0
  435. anyfesolver/general_contact/beam_pair_end_geometry.py +269 -0
  436. anyfesolver/general_contact/beam_pair_ends.py +222 -0
  437. anyfesolver/general_contact/beam_plane.py +425 -0
  438. anyfesolver/general_contact/beam_radial.py +500 -0
  439. anyfesolver/general_contact/beam_radial_ends.py +223 -0
  440. anyfesolver/general_contact/beam_s3_directional_rates.py +196 -0
  441. anyfesolver/general_contact/beam_s3_rotational_transient.py +126 -0
  442. anyfesolver/general_contact/beam_s3_transient.py +139 -0
  443. anyfesolver/general_contact/beam_shell.py +648 -0
  444. anyfesolver/general_contact/beam_shell_patch.py +395 -0
  445. anyfesolver/general_contact/beam_temporal_bounds.py +230 -0
  446. anyfesolver/general_contact/capability.py +101 -0
  447. anyfesolver/general_contact/capped_cylinder.py +618 -0
  448. anyfesolver/general_contact/contact_search.py +95 -0
  449. anyfesolver/general_contact/core.py +1769 -0
  450. anyfesolver/general_contact/curved_s3_q4_work.py +238 -0
  451. anyfesolver/general_contact/curved_shell_skin.py +520 -0
  452. anyfesolver/general_contact/d1_interior_profile.py +427 -0
  453. anyfesolver/general_contact/deformable_beam_s3_caps.py +143 -0
  454. anyfesolver/general_contact/deformable_beam_s3_line.py +211 -0
  455. anyfesolver/general_contact/deformable_beam_s3_static.py +152 -0
  456. anyfesolver/general_contact/descriptor_rates.py +61 -0
  457. anyfesolver/general_contact/enriched_cold_predictor.py +161 -0
  458. anyfesolver/general_contact/enriched_moment_batch.py +337 -0
  459. anyfesolver/general_contact/enriched_multitarget.py +308 -0
  460. anyfesolver/general_contact/enriched_multitarget_gap.py +156 -0
  461. anyfesolver/general_contact/enriched_multitarget_moments.py +194 -0
  462. anyfesolver/general_contact/enriched_multitarget_topology.py +171 -0
  463. anyfesolver/general_contact/enriched_nonlinear.py +440 -0
  464. anyfesolver/general_contact/enriched_overlap.py +299 -0
  465. anyfesolver/general_contact/enriched_path.py +253 -0
  466. anyfesolver/general_contact/enriched_small_cut.py +310 -0
  467. anyfesolver/general_contact/enriched_static.py +481 -0
  468. anyfesolver/general_contact/enriched_union.py +320 -0
  469. anyfesolver/general_contact/equilibrated_pressure_pivot.py +140 -0
  470. anyfesolver/general_contact/event_subdivision.py +273 -0
  471. anyfesolver/general_contact/finite_mixed_state.py +133 -0
  472. anyfesolver/general_contact/finite_rigid_patch.py +196 -0
  473. anyfesolver/general_contact/finite_rigid_support.py +129 -0
  474. anyfesolver/general_contact/finite_rigid_sweep.py +53 -0
  475. anyfesolver/general_contact/finite_rotation_predictor.py +171 -0
  476. anyfesolver/general_contact/finite_rotation_skin.py +212 -0
  477. anyfesolver/general_contact/finite_rotation_state.py +106 -0
  478. anyfesolver/general_contact/finite_rotation_sweep.py +161 -0
  479. anyfesolver/general_contact/finite_v2d_state.py +104 -0
  480. anyfesolver/general_contact/fixed_projection_graph.py +205 -0
  481. anyfesolver/general_contact/fixed_projection_s3_events.py +235 -0
  482. anyfesolver/general_contact/free_rigid.py +134 -0
  483. anyfesolver/general_contact/friction_point.py +378 -0
  484. anyfesolver/general_contact/gap_extrema.py +119 -0
  485. anyfesolver/general_contact/guided_pair_transient_geometry.py +138 -0
  486. anyfesolver/general_contact/initial_contact_seed.py +368 -0
  487. anyfesolver/general_contact/linearized_pivot.py +595 -0
  488. anyfesolver/general_contact/material_work_diagnostics.py +159 -0
  489. anyfesolver/general_contact/moment_batch_ad.py +129 -0
  490. anyfesolver/general_contact/moment_batch_points.py +75 -0
  491. anyfesolver/general_contact/moving.py +1031 -0
  492. anyfesolver/general_contact/multiple_beam_rigid.py +248 -0
  493. anyfesolver/general_contact/multiple_beam_s3_caps.py +199 -0
  494. anyfesolver/general_contact/multiple_beam_s3_transient.py +223 -0
  495. anyfesolver/general_contact/multiple_projected_pairs.py +196 -0
  496. anyfesolver/general_contact/multiple_shell_planes.py +226 -0
  497. anyfesolver/general_contact/native_activation_static.py +143 -0
  498. anyfesolver/general_contact/native_checkpoint_results.py +213 -0
  499. anyfesolver/general_contact/native_curve_union.py +268 -0
  500. anyfesolver/general_contact/native_curved_geometry.py +374 -0
  501. anyfesolver/general_contact/native_curved_pair_partition.py +590 -0
  502. anyfesolver/general_contact/native_curved_plane_work.py +864 -0
  503. anyfesolver/general_contact/native_curved_s3_work.py +472 -0
  504. anyfesolver/general_contact/native_event_polynomial.py +164 -0
  505. anyfesolver/general_contact/native_fibre_context.py +173 -0
  506. anyfesolver/general_contact/native_fibre_results.py +74 -0
  507. anyfesolver/general_contact/native_initial_guess.py +122 -0
  508. anyfesolver/general_contact/native_material_activation.py +108 -0
  509. anyfesolver/general_contact/native_material_plane.py +111 -0
  510. anyfesolver/general_contact/native_material_s3.py +209 -0
  511. anyfesolver/general_contact/native_multi_s3_static.py +722 -0
  512. anyfesolver/general_contact/native_normal_angles.py +233 -0
  513. anyfesolver/general_contact/native_pair_error_budget.py +49 -0
  514. anyfesolver/general_contact/native_replay_arithmetic.py +103 -0
  515. anyfesolver/general_contact/native_replay_diagnostics.py +90 -0
  516. anyfesolver/general_contact/native_retained_body_junction.py +99 -0
  517. anyfesolver/general_contact/native_retained_body_pair.py +220 -0
  518. anyfesolver/general_contact/native_retained_body_projection.py +160 -0
  519. anyfesolver/general_contact/native_retained_body_static.py +62 -0
  520. anyfesolver/general_contact/native_retained_cell_pair.py +185 -0
  521. anyfesolver/general_contact/native_retained_curve_cell.py +102 -0
  522. anyfesolver/general_contact/native_retained_line_plane.py +210 -0
  523. anyfesolver/general_contact/native_retained_line_s3.py +440 -0
  524. anyfesolver/general_contact/native_retained_line_s3_work.py +91 -0
  525. anyfesolver/general_contact/native_retained_pair_static.py +845 -0
  526. anyfesolver/general_contact/native_retained_s3_static.py +554 -0
  527. anyfesolver/general_contact/native_retained_static.py +478 -0
  528. anyfesolver/general_contact/native_retained_station.py +222 -0
  529. anyfesolver/general_contact/native_retained_whole_pair.py +2120 -0
  530. anyfesolver/general_contact/native_s3_swept.py +149 -0
  531. anyfesolver/general_contact/native_shell_static_driver.py +144 -0
  532. anyfesolver/general_contact/native_surface_activation.py +391 -0
  533. anyfesolver/general_contact/native_surface_bounds.py +198 -0
  534. anyfesolver/general_contact/native_surface_contributor.py +121 -0
  535. anyfesolver/general_contact/native_surface_crossing.py +81 -0
  536. anyfesolver/general_contact/native_surface_event_sequence.py +320 -0
  537. anyfesolver/general_contact/native_surface_extrema.py +72 -0
  538. anyfesolver/general_contact/native_surface_friction_integral.py +1724 -0
  539. anyfesolver/general_contact/native_surface_front.py +742 -0
  540. anyfesolver/general_contact/native_surface_front_boundary.py +160 -0
  541. anyfesolver/general_contact/native_surface_front_precision.py +78 -0
  542. anyfesolver/general_contact/native_surface_front_work.py +118 -0
  543. anyfesolver/general_contact/native_surface_history.py +334 -0
  544. anyfesolver/general_contact/native_surface_normal.py +324 -0
  545. anyfesolver/general_contact/native_surface_patch.py +213 -0
  546. anyfesolver/general_contact/native_surface_path.py +348 -0
  547. anyfesolver/general_contact/native_surface_quadrature.py +212 -0
  548. anyfesolver/general_contact/native_surface_s3_friction.py +340 -0
  549. anyfesolver/general_contact/native_surface_support.py +776 -0
  550. anyfesolver/general_contact/native_surface_transaction.py +198 -0
  551. anyfesolver/general_contact/native_transient_multi_shell.py +108 -0
  552. anyfesolver/general_contact/native_transient_pair.py +61 -0
  553. anyfesolver/general_contact/native_transient_shell.py +44 -0
  554. anyfesolver/general_contact/normal_s3_skin_path.py +163 -0
  555. anyfesolver/general_contact/overlap.py +443 -0
  556. anyfesolver/general_contact/pair.py +466 -0
  557. anyfesolver/general_contact/patch_broadphase.py +111 -0
  558. anyfesolver/general_contact/patch_mortar.py +593 -0
  559. anyfesolver/general_contact/patch_sliding.py +644 -0
  560. anyfesolver/general_contact/plane_newmark_events.py +278 -0
  561. anyfesolver/general_contact/pointwise_patch.py +288 -0
  562. anyfesolver/general_contact/polynomial_roots.py +312 -0
  563. anyfesolver/general_contact/pressure_basis.py +241 -0
  564. anyfesolver/general_contact/pressure_diagnostics.py +155 -0
  565. anyfesolver/general_contact/pressure_overlay.py +182 -0
  566. anyfesolver/general_contact/pressure_space.py +51 -0
  567. anyfesolver/general_contact/q4_material_checkpoint.py +276 -0
  568. anyfesolver/general_contact/q4_overlap_local_bound.py +1111 -0
  569. anyfesolver/general_contact/q4_q4_overlap_derivatives.py +234 -0
  570. anyfesolver/general_contact/q4_q4_swept_bound.py +58 -0
  571. anyfesolver/general_contact/q4_s3_overlap_derivatives.py +1159 -0
  572. anyfesolver/general_contact/q4_s3_tiny_wedge.py +180 -0
  573. anyfesolver/general_contact/q4_skin_work.py +87 -0
  574. anyfesolver/general_contact/q4_source_mortar_energy.py +181 -0
  575. anyfesolver/general_contact/q4_source_overlap.py +401 -0
  576. anyfesolver/general_contact/q4_target_overlap.py +181 -0
  577. anyfesolver/general_contact/request_identity.py +192 -0
  578. anyfesolver/general_contact/rigid_translation.py +199 -0
  579. anyfesolver/general_contact/runtime.py +27 -0
  580. anyfesolver/general_contact/s3_friction_chart_interval.py +104 -0
  581. anyfesolver/general_contact/s3_friction_coverage.py +165 -0
  582. anyfesolver/general_contact/s3_friction_dyadic_interval.py +39 -0
  583. anyfesolver/general_contact/s3_friction_history_path.py +111 -0
  584. anyfesolver/general_contact/s3_friction_mesh_coverage.py +145 -0
  585. anyfesolver/general_contact/s3_friction_mesh_gap.py +74 -0
  586. anyfesolver/general_contact/s3_friction_mesh_monolithic.py +150 -0
  587. anyfesolver/general_contact/s3_friction_monolithic.py +121 -0
  588. anyfesolver/general_contact/s3_friction_moving_coverage.py +114 -0
  589. anyfesolver/general_contact/s3_friction_pair.py +237 -0
  590. anyfesolver/general_contact/s3_friction_point.py +129 -0
  591. anyfesolver/general_contact/s3_friction_prepared_path.py +254 -0
  592. anyfesolver/general_contact/s3_friction_replay.py +1094 -0
  593. anyfesolver/general_contact/s3_friction_rim_path.py +69 -0
  594. anyfesolver/general_contact/s3_friction_rim_transaction.py +23 -0
  595. anyfesolver/general_contact/s3_friction_static.py +267 -0
  596. anyfesolver/general_contact/s3_friction_swept_gap.py +112 -0
  597. anyfesolver/general_contact/s3_friction_touch.py +94 -0
  598. anyfesolver/general_contact/s3_friction_weak_pressure.py +66 -0
  599. anyfesolver/general_contact/s3_overlap_derivatives.py +220 -0
  600. anyfesolver/general_contact/s3_overlap_hermite_gap.py +180 -0
  601. anyfesolver/general_contact/s3_pair_static.py +343 -0
  602. anyfesolver/general_contact/s3_patch_augmented.py +2056 -0
  603. anyfesolver/general_contact/s3_patch_mortar_energy.py +545 -0
  604. anyfesolver/general_contact/s3_patch_static.py +1725 -0
  605. anyfesolver/general_contact/s3_pressure_hermite_bounds.py +395 -0
  606. anyfesolver/general_contact/s3_pressure_polygon_bounds.py +73 -0
  607. anyfesolver/general_contact/s3_q4_gap_resolution.py +191 -0
  608. anyfesolver/general_contact/s3_q4_overlap_derivatives.py +252 -0
  609. anyfesolver/general_contact/s3_q4_swept.py +56 -0
  610. anyfesolver/general_contact/s3_skin_work.py +101 -0
  611. anyfesolver/general_contact/s3_sliding_friction.py +640 -0
  612. anyfesolver/general_contact/s3_stable_plane_gap.py +43 -0
  613. anyfesolver/general_contact/seed_path_vertex_interval.py +323 -0
  614. anyfesolver/general_contact/semismooth.py +133 -0
  615. anyfesolver/general_contact/semismooth_block.py +147 -0
  616. anyfesolver/general_contact/semismooth_path.py +871 -0
  617. anyfesolver/general_contact/shell_pair_hermite_sweep.py +336 -0
  618. anyfesolver/general_contact/shell_sphere_temporal_bounds.py +305 -0
  619. anyfesolver/general_contact/skin_geometry.py +106 -0
  620. anyfesolver/general_contact/sliding.py +349 -0
  621. anyfesolver/general_contact/sphere.py +691 -0
  622. anyfesolver/general_contact/sphere_temporal_bounds.py +241 -0
  623. anyfesolver/general_contact/static_path.py +2608 -0
  624. anyfesolver/general_contact/static_results_archive.py +140 -0
  625. anyfesolver/general_contact/structural_state_bridge.py +196 -0
  626. anyfesolver/general_contact/supported_schur.py +594 -0
  627. anyfesolver/general_contact/swept_patch_broadphase.py +160 -0
  628. anyfesolver/general_contact/transient.py +2921 -0
  629. anyfesolver/general_contact/transient_results_archive.py +491 -0
  630. anyfesolver/general_contact/triangle_mortar_energy.py +280 -0
  631. anyfesolver/general_contact/triangle_pair_energy.py +211 -0
  632. anyfesolver/general_contact/triangulated_rigid.py +94 -0
  633. anyfesolver/general_contact/triangulated_rigid_static.py +399 -0
  634. anyfesolver/general_contact/warped_q4_plane.py +185 -0
  635. anyfesolver/general_contact/warped_q4_skin.py +302 -0
  636. anyfesolver/general_contact/warped_q4_static.py +331 -0
  637. anyfesolver/impact_performance.py +273 -0
  638. anyfesolver/impact_reduced_assembly.py +294 -0
  639. anyfesolver/imperfections.py +397 -0
  640. anyfesolver/inertia_relief.py +676 -0
  641. anyfesolver/initial_field_state.py +54 -0
  642. anyfesolver/jit_compiler.py +135 -0
  643. anyfesolver/kernel_warmup.py +228 -0
  644. anyfesolver/linalg.py +1002 -0
  645. anyfesolver/marine_contributions.py +634 -0
  646. anyfesolver/marine_coupled_equilibrium.py +292 -0
  647. anyfesolver/marine_equilibrium.py +405 -0
  648. anyfesolver/marine_hydro_coordinates.py +346 -0
  649. anyfesolver/marine_linear.py +549 -0
  650. anyfesolver/marine_link.py +306 -0
  651. anyfesolver/marine_mechanical.py +923 -0
  652. anyfesolver/marine_mooring.py +533 -0
  653. anyfesolver/marine_morison.py +331 -0
  654. anyfesolver/marine_potential_flow.py +403 -0
  655. anyfesolver/marine_rigid.py +397 -0
  656. anyfesolver/marine_seabed.py +122 -0
  657. anyfesolver/mass_properties.py +352 -0
  658. anyfesolver/material_curves.py +52 -0
  659. anyfesolver/materials.py +43 -0
  660. anyfesolver/matrix_assembly.py +7330 -0
  661. anyfesolver/mesh_gen.py +904 -0
  662. anyfesolver/mesh_load_bc_verification.py +612 -0
  663. anyfesolver/modal.py +2494 -0
  664. anyfesolver/nonlinear.py +341 -0
  665. anyfesolver/nonlinear_analysis_diagnostics.py +466 -0
  666. anyfesolver/nonlinear_element_evaluation.py +126 -0
  667. anyfesolver/nonlinear_performance.py +2094 -0
  668. anyfesolver/nonlinear_performance_batch_b.py +1173 -0
  669. anyfesolver/nonlinear_performance_batch_c.py +850 -0
  670. anyfesolver/nonlinear_performance_bootstrap.py +164 -0
  671. anyfesolver/nonlinear_reduced_assembly.py +806 -0
  672. anyfesolver/nonlinear_restart.py +1708 -0
  673. anyfesolver/nonlinear_state.py +2800 -0
  674. anyfesolver/nonlinear_static.py +9407 -0
  675. anyfesolver/outcomes.py +694 -0
  676. anyfesolver/plasticity.py +2087 -0
  677. anyfesolver/plasticity_qualification.py +758 -0
  678. anyfesolver/prepared_static.py +1129 -0
  679. anyfesolver/prepared_static_envelopes.py +389 -0
  680. anyfesolver/prepared_static_loads.py +1180 -0
  681. anyfesolver/prepared_static_measures.py +18 -0
  682. anyfesolver/prepared_static_numeric.py +247 -0
  683. anyfesolver/prepared_static_q4.py +297 -0
  684. anyfesolver/prepared_static_recovery.py +2068 -0
  685. anyfesolver/prepared_static_response.py +1013 -0
  686. anyfesolver/prepared_static_shared.py +226 -0
  687. anyfesolver/prepared_static_snapshot.py +323 -0
  688. anyfesolver/prepared_static_vectorized.py +866 -0
  689. anyfesolver/pressure_regions.py +287 -0
  690. anyfesolver/production_readiness.py +343 -0
  691. anyfesolver/quality_control.py +1487 -0
  692. anyfesolver/quantities.py +1289 -0
  693. anyfesolver/recovery.py +4217 -0
  694. anyfesolver/recovery_batches.py +1985 -0
  695. anyfesolver/recovery_policy.py +205 -0
  696. anyfesolver/recovery_s4.py +289 -0
  697. anyfesolver/recovery_shell_frames.py +102 -0
  698. anyfesolver/reference_cases.py +366 -0
  699. anyfesolver/result_capsule.py +2175 -0
  700. anyfesolver/results.py +752 -0
  701. anyfesolver/runtime.py +9819 -0
  702. anyfesolver/s3_reference_batch.py +1338 -0
  703. anyfesolver/s3_v2c_fast_assembly.py +544 -0
  704. anyfesolver/s3_v2d_fast_assembly.py +447 -0
  705. anyfesolver/s4_validity.py +354 -0
  706. anyfesolver/sections.py +67 -0
  707. anyfesolver/sesam_fem/__init__.py +55 -0
  708. anyfesolver/sesam_fem/__main__.py +80 -0
  709. anyfesolver/sesam_fem/diagnostics.py +5 -0
  710. anyfesolver/sesam_fem/document.py +39 -0
  711. anyfesolver/sesam_fem/exporter.py +9 -0
  712. anyfesolver/sesam_fem/importer.py +228 -0
  713. anyfesolver/sesam_fem/records.py +21 -0
  714. anyfesolver/sesam_fem/schema.py +35 -0
  715. anyfesolver/sesam_fem/sif_importer.py +9 -0
  716. anyfesolver/sesam_fem/validation.py +5 -0
  717. anyfesolver/sestra_deep_diagnostics.py +173 -0
  718. anyfesolver/sestra_mixed_verification.py +146 -0
  719. anyfesolver/sestra_structure_benchmarks.py +270 -0
  720. anyfesolver/sestra_three_case.py +215 -0
  721. anyfesolver/sestra_verification.py +523 -0
  722. anyfesolver/shell_benchmarks.py +367 -0
  723. anyfesolver/shell_formulations/director_field.py +799 -0
  724. anyfesolver/shell_formulations/geometry_provenance.py +921 -0
  725. anyfesolver/shell_formulations/mitc4_plus_d_quality.py +318 -0
  726. anyfesolver/shell_formulations/mitc4_plus_d_reference.py +837 -0
  727. anyfesolver/shell_formulations/mitc4_plus_d_scalar.py +209 -0
  728. anyfesolver/shell_formulations/protocol.py +222 -0
  729. anyfesolver/shell_formulations/q4_common.py +271 -0
  730. anyfesolver/shell_formulations/s4_restricted_policy.py +78 -0
  731. anyfesolver/shell_sections.py +394 -0
  732. anyfesolver/solid_submodel.py +930 -0
  733. anyfesolver/static_batch.py +662 -0
  734. anyfesolver/static_envelopes.py +250 -0
  735. anyfesolver/stiffened_shell.py +388 -0
  736. anyfesolver/submodel_transfer.py +1306 -0
  737. anyfesolver/test_cases.py +610 -0
  738. anyfesolver/threading_policy.py +431 -0
  739. anyfesolver/validation.py +719 -0
  740. anyfesolver/vectorized_beam.py +139 -0
  741. anyfesolver/vectorized_generalized_shell.py +448 -0
  742. anyfesolver/vectorized_hill48.py +956 -0
  743. anyfesolver/vectorized_nonlinear.py +360 -0
  744. anyfesolver/vectorized_stiffness.py +694 -0
  745. anyfesolver-0.5.0a1.dist-info/METADATA +374 -0
  746. anyfesolver-0.5.0a1.dist-info/RECORD +751 -0
  747. anyfesolver-0.5.0a1.dist-info/WHEEL +5 -0
  748. anyfesolver-0.5.0a1.dist-info/licenses/COPYRIGHT +1 -0
  749. anyfesolver-0.5.0a1.dist-info/licenses/LICENSE +373 -0
  750. anyfesolver-0.5.0a1.dist-info/licenses/THIRD_PARTY_NOTICES.md +32 -0
  751. anyfesolver-0.5.0a1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,2447 @@
1
+ """Small elastic multi-Q4/multi-B2 panel with existing F10 point joints.
2
+
3
+ This is an experimental reference-linear owner. Shell and beam node identities
4
+ are explicit and separate. Reference modes require explicit physical mass.
5
+ Explicit nodal footprints have a distinct reference-linear law; virgin supported
6
+ modes retain its normal-spin locking and incomplete mechanical qualification.
7
+ No line, geometric stiffness, nonlinear state or qualification is inferred.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from functools import wraps
13
+ from dataclasses import dataclass, field
14
+ from hashlib import sha256
15
+ from math import fsum, isfinite
16
+ from numbers import Real
17
+ from typing import Callable, Mapping, Sequence
18
+
19
+ import numpy as np
20
+ from scipy import sparse
21
+ from scipy.sparse.linalg import LinearOperator, eigs, ArpackError
22
+ from scipy.sparse.csgraph import maximum_bipartite_matching
23
+
24
+ from ._b2_f10_f11_point_attachment import LAW_ID, compile_reference_point
25
+ from ._stiffened_shell_b2_nodal_footprint import (
26
+ LAW_ID as FOOTPRINT_LAW_ID, B2NodalRigidFootprint,
27
+ B2NodalRigidFootprintSelection, B2NodalRigidFootprintWrench,
28
+ compile_footprints, checked_selection, selected_wrenches,
29
+ )
30
+ from ._stiffened_shell_b2_physical_rigid_footprint import (
31
+ LAW_ID as PHYSICAL_FOOTPRINT_LAW_ID, B2PhysicalRigidFootprint,
32
+ B2PhysicalRigidFootprintSelection, B2PhysicalRigidFootprintWrench,
33
+ compile_physical_footprints, checked_selection as checked_physical_selection,
34
+ selected_wrenches as physical_selected_wrenches,
35
+ )
36
+ from ._b2_physical_linear_definition import (
37
+ PROFILE_ID, CompiledPhysicalB2Member, PhysicalB2Member,
38
+ PhysicalB2OperatorCache,
39
+ )
40
+ from ._b2_physical_linear_batch_dev import (
41
+ _load_value_snapshot, _checked_affine_projection as _project_affine_work,
42
+ _accurate_sparse_residual, B2NumericalSolveFailure,
43
+ )
44
+ from .control import CancellationToken, SolveCancelled
45
+ from ._stiffened_shell_physical_q4_mass import physical_mass, POLICY as Q4_MASS_POLICY
46
+ from .e4_pl_s3_v2d_element import (
47
+ MASS_POLICY_ID as S3_V2D_TRANSLATIONAL_LUMPED_SECTION_AREAL_MASS_V1,
48
+ FORMULATION_ID as S3_V2D_FORMULATION_ID,
49
+ )
50
+ from ._b2_physical_flexibility_linear import RepresentabilityError
51
+ from ._b2_loaded_point_trace_dev import LoadedPointMember, checked_sparse_action
52
+ from ._stiffened_shell_b2_affine_resolution import certify_affine
53
+ from ._b2_attachment_elimination_dev import prepare_attachment_elimination
54
+ from .e4_pl_element import _retire_q4_private_mesh_caches
55
+ from ._stiffened_shell_b2_assembly_diagnostics import active_capture, DiagnosticRefusal
56
+ from .elements import create_shell_element
57
+ from .fe_core import FEModel
58
+ from .linalg import MatrixClass, factorize_cached
59
+
60
+ B2_FIXED_END_LOADED_POINT_TRACE_V1_DEV = "B2_FIXED_END_LOADED_POINT_TRACE_V1_DEV"
61
+
62
+ class ExperimentalB2Q4PanelError(RuntimeError):
63
+ """The experimental panel cannot be prepared or used."""
64
+
65
+
66
+ class _NumericalFailure(RuntimeError):
67
+ """A checked sparse panel solution failed numerically."""
68
+
69
+
70
+ _MAX_DENSE_AUDIT_ROWS = 48
71
+ _MAX_DENSE_AUDIT_VALUES = 16_384
72
+
73
+
74
+ def _bounded_accurate_residual(matrix, solution, rhs):
75
+ """Stored-equation residual and outward bound using exact products.
76
+
77
+ Only selected explicitly loaded rows use this exceptional path. Fraction
78
+ products/summation avoid FMA/fsum portability and underflow assumptions.
79
+ Conversion to binary64 is correctly rounded; a full ULP and outward final
80
+ rounding conservatively enclose the absolute rational residual. This does
81
+ not certify element/assembly rounding or forward displacement accuracy.
82
+ """
83
+ if (not np.isfinite(matrix.data).all() or not np.isfinite(solution).all() or
84
+ not np.isfinite(rhs).all()):
85
+ raise B2NumericalSolveFailure("physical B2 residual is not representable")
86
+ defect = _accurate_sparse_residual(matrix, solution, rhs, exact_products=True)
87
+ magnitude = np.abs(defect)
88
+ with np.errstate(over="ignore", under="ignore", invalid="ignore"):
89
+ rounding = np.nextafter(magnitude, np.inf)-magnitude
90
+ upper = np.nextafter(magnitude+rounding, np.inf)
91
+ return defect, upper
92
+
93
+
94
+ def _checked_affine_projection(rows, multi, singles, values: np.ndarray) -> np.ndarray:
95
+ """Keep the panel's failure identity while sharing checked affine work."""
96
+ return _project_affine_work(rows, multi, singles, values,
97
+ failure_type=_NumericalFailure, label="panel")
98
+
99
+
100
+ def _scaled_point_kkt(reduced_k: sparse.spmatrix,
101
+ reduced_c: sparse.spmatrix,
102
+ multiplier_compliance: sparse.spmatrix | None = None) -> tuple[sparse.csc_matrix, np.ndarray]:
103
+ """Representable congruence for the elastic point-constrained KKT.
104
+
105
+ A zero-energy primal coordinate is valid when a constraint anchors it;
106
+ its scale then follows that constraint column. This owner admits no
107
+ negative-energy primal diagonal or unanchored zero-energy coordinate.
108
+ """
109
+ diagonal = reduced_k.diagonal()
110
+ if not np.isfinite(diagonal).all() or np.any(diagonal < 0.):
111
+ raise ExperimentalB2Q4PanelError(
112
+ "panel factorization requires nonnegative finite primal diagonal")
113
+ primal_scale = np.empty(len(diagonal))
114
+ positive = diagonal > 0.
115
+ with np.errstate(over="ignore", under="ignore", divide="ignore",
116
+ invalid="ignore"):
117
+ primal_scale[positive] = 1./np.sqrt(diagonal[positive])
118
+ if np.any(~positive):
119
+ # For a positive-semidefinite elastic K, a zero diagonal has a
120
+ # zero row. An off-diagonal term instead signals invalid data.
121
+ zero_rows = reduced_k.tocsr()[np.flatnonzero(~positive)]
122
+ if np.any(zero_rows.data != 0.):
123
+ raise ExperimentalB2Q4PanelError(
124
+ "zero-energy primal row has nonzero stiffness coupling")
125
+ column_max = np.asarray(
126
+ abs(reduced_c).max(axis=0).toarray()).reshape(-1)
127
+ if (not np.isfinite(column_max[~positive]).all() or
128
+ np.any(column_max[~positive] <= 0.)):
129
+ raise ExperimentalB2Q4PanelError(
130
+ "unanchored zero-energy primal coordinate")
131
+ primal_scale[~positive] = 1./column_max[~positive]
132
+ if (not np.isfinite(primal_scale).all() or
133
+ np.any(primal_scale <= 0.)):
134
+ raise ExperimentalB2Q4PanelError(
135
+ "panel factorization scaling is not representable")
136
+ scaled_rows = reduced_c @ sparse.diags(primal_scale)
137
+ row_max = np.asarray(abs(scaled_rows).max(axis=1).toarray()).reshape(-1)
138
+ if multiplier_compliance is not None:
139
+ diagonal_h = multiplier_compliance.diagonal()
140
+ if (multiplier_compliance.shape != (reduced_c.shape[0],)*2 or
141
+ not np.isfinite(multiplier_compliance.data).all() or
142
+ np.any(diagonal_h < 0.)):
143
+ raise ExperimentalB2Q4PanelError("finite nonnegative point compliance required")
144
+ row_max = np.maximum(row_max, np.sqrt(diagonal_h))
145
+ multiplier_scale = 1./row_max
146
+ factor_scale = np.r_[primal_scale, multiplier_scale]
147
+ kkt = sparse.bmat(
148
+ ((reduced_k, reduced_c.T), (reduced_c, None if multiplier_compliance is None
149
+ else -multiplier_compliance)), format="csc")
150
+ scaling = sparse.diags(factor_scale, format="csc")
151
+ scaled_kkt = (scaling @ kkt @ scaling).tocsc()
152
+ kkt.sort_indices()
153
+ scaled_kkt.sort_indices()
154
+ if (not np.isfinite(factor_scale).all() or
155
+ np.any(factor_scale <= 0.) or
156
+ not np.isfinite(scaled_kkt.data).all() or
157
+ kkt.nnz != scaled_kkt.nnz or
158
+ not np.array_equal(kkt.indptr, scaled_kkt.indptr) or
159
+ not np.array_equal(kkt.indices, scaled_kkt.indices) or
160
+ np.any((kkt.data != 0.) & (scaled_kkt.data == 0.))):
161
+ raise ExperimentalB2Q4PanelError(
162
+ "panel factorization scaling is not representable")
163
+ return scaled_kkt, factor_scale
164
+
165
+
166
+ def _require_independent_supported_point_rows(reduced_c: sparse.spmatrix) -> None:
167
+ """Bound the dense audit; defer large numerical rank to KKT factorization.
168
+
169
+ A matching proves only structural rank. The subsequent sparse KKT
170
+ factorization must still reject numerical row dependence for larger models.
171
+ """
172
+ rows, columns = reduced_c.shape
173
+ if rows > columns or not np.isfinite(reduced_c.data).all():
174
+ raise ValueError("independent supported point rows required")
175
+ if rows <= _MAX_DENSE_AUDIT_ROWS and rows*columns <= _MAX_DENSE_AUDIT_VALUES:
176
+ if np.linalg.matrix_rank(reduced_c.toarray()) != rows:
177
+ raise ValueError("independent supported point rows required")
178
+ return
179
+ pattern = reduced_c.tocsr(copy=True)
180
+ pattern.eliminate_zeros()
181
+ matching = maximum_bipartite_matching(pattern, perm_type="column")
182
+ if np.any(matching < 0):
183
+ raise ValueError("structurally independent supported point rows required")
184
+
185
+
186
+ @dataclass(frozen=True)
187
+ class Q4PanelShell:
188
+ shell_id: str
189
+ node_ids: tuple[int, int, int, int]
190
+ E: float
191
+ nu: float
192
+ thickness_m: float
193
+ reference_normal: tuple[float, float, float] = (0., 0., 1.)
194
+ density_kg_m3: float | None = None
195
+
196
+
197
+ @dataclass(frozen=True)
198
+ class Q4B2Point:
199
+ point_id: str
200
+ shell_id: str
201
+ member_id: str
202
+ station: float
203
+ offset_local_m: tuple[float, float, float] = (0., 0., 0.)
204
+
205
+
206
+ @dataclass(frozen=True)
207
+ class S3V2DPanelShell:
208
+ """Explicit native S3 V2D elastic facet; reference mass requires modal opt-in."""
209
+
210
+ shell_id: str
211
+ node_ids: tuple[int, int, int]
212
+ E: float
213
+ nu: float
214
+ thickness_m: float
215
+ reference_normal: tuple[float, float, float]
216
+ density_kg_m3: float | None = None
217
+
218
+
219
+ @dataclass(frozen=True)
220
+ class ExperimentalB2Q4PanelCapabilities:
221
+ profile_id: str = PROFILE_ID
222
+ point_law_id: str = LAW_ID
223
+ status: str = "experimental"
224
+ production_qualified: bool = False
225
+ shell_domain: str = "multiple flat elastic Q4"
226
+ beam_domain: str = "multiple physical B2 members"
227
+ attachment: str = "F10 reference points only"
228
+ operations: tuple[str, ...] = ("solve_linear", "solve_linear_many", "close")
229
+ spectral_domain: str = "unavailable without explicit physical mass"
230
+ body_load_domain: str = "unavailable without complete physical density/mass"
231
+ point_trace_policy: str | None = None
232
+
233
+
234
+ @dataclass(frozen=True)
235
+ class ExperimentalB2LoadedPointCapabilities(ExperimentalB2Q4PanelCapabilities):
236
+ point_trace_policy: str = B2_FIXED_END_LOADED_POINT_TRACE_V1_DEV
237
+ spectral_domain: str = "unsupported: loaded point trace inertia unverified"
238
+
239
+
240
+ @dataclass(frozen=True, eq=False)
241
+ class B2ReferenceBodyLoad:
242
+ """Owned reference-linear body load; six global coordinates per node.
243
+
244
+ Shell nodes precede beam nodes, each in the returned ID order. Acceleration
245
+ is global in m/s2. This is a reference load, not finite-configuration gravity.
246
+ """
247
+
248
+ acceleration_m_s2: tuple[float, float, float]
249
+ shell_node_ids: tuple[int, ...]
250
+ beam_node_ids: tuple[int, ...]
251
+ load: np.ndarray
252
+ model_identity: str = ""
253
+ policy: str = field(default="B2_E4_PL_REFERENCE_BODY_ACCELERATION_V1", init=False)
254
+ production_qualified: bool = field(default=False, init=False)
255
+ member_recovery: str = field(default="pass typed load for particular-load equilibrated fields; .load alone retains nodal-state constitutive recovery", init=False)
256
+
257
+ def __post_init__(self):
258
+ # Bytes-backed storage cannot be made writable, and never aliases the
259
+ # owner's compiled map or the caller's acceleration.
260
+ values = np.asarray(self.load, dtype=np.float64)
261
+ object.__setattr__(self, "load", np.frombuffer(values.tobytes(), dtype=np.float64))
262
+ object.__setattr__(self, "acceleration_m_s2", tuple(self.acceleration_m_s2))
263
+ object.__setattr__(self, "shell_node_ids", tuple(self.shell_node_ids))
264
+ object.__setattr__(self, "beam_node_ids", tuple(self.beam_node_ids))
265
+
266
+
267
+ @dataclass(frozen=True)
268
+ class B2LoadedMemberSections:
269
+ """Static admissible section fields; not a new loaded kinematic state."""
270
+
271
+ member_id: str
272
+ stations: tuple[float, ...]
273
+ resultants_left: tuple[tuple[float, ...], ...]
274
+ resultants_right: tuple[tuple[float, ...], ...]
275
+ strains_left: tuple[tuple[float, ...], ...]
276
+ strains_right: tuple[tuple[float, ...], ...]
277
+ endpoint_tractions_local: tuple[float, ...]
278
+ policy: str = field(default="B2_REFERENCE_EQUILIBRATED_MEMBER_FIELDS_V1_DEV", init=False)
279
+ production_qualified: bool = field(default=False, init=False)
280
+
281
+
282
+ def _beam_body_map(member: CompiledPhysicalB2Member, frame: np.ndarray) -> np.ndarray:
283
+ """Integrate physical section action before any rotary-mass cancellation."""
284
+ section = member.definition.mass.matrix_at_reference_axis()[:, :3]
285
+ abscissae, weights = np.polynomial.legendre.leggauss(4)
286
+ stations = tuple(float((x+1.)/2.) for x in abscissae)
287
+ trace = member.operator.trace(np.zeros(12), stations=stations)
288
+ local = np.zeros((12, 3))
289
+ with np.errstate(over="ignore", invalid="ignore"):
290
+ for weight, jacobian in zip(weights, trace.jacobian):
291
+ local += member.definition.length_m*(weight/2.)*(jacobian.T@section)
292
+ result = frame.T@local@frame[:3, :3]
293
+ if not np.isfinite(result).all():
294
+ raise RepresentabilityError("physical B2 body-load map is not representable")
295
+ return result
296
+
297
+
298
+ @dataclass(frozen=True)
299
+ class ExperimentalB2Q4FootprintCapabilities(ExperimentalB2Q4PanelCapabilities):
300
+ footprint_law_id: str = FOOTPRINT_LAW_ID
301
+ footprints: tuple[B2NodalRigidFootprint, ...] = ()
302
+
303
+ @dataclass(frozen=True)
304
+ class B2PhysicalRigidFootprintCapabilities(ExperimentalB2Q4PanelCapabilities):
305
+ footprint_law_id: str = PHYSICAL_FOOTPRINT_LAW_ID
306
+ footprints: tuple[B2PhysicalRigidFootprint, ...] = ()
307
+ normal_spin: str = "independent; normal torque through noncollinear translation arms"
308
+
309
+
310
+ @dataclass(frozen=True)
311
+ class ExperimentalB2MixedPanelCapabilities(ExperimentalB2Q4PanelCapabilities):
312
+ shell_domain: str = "explicit flat elastic Q4/native S3 V2D panel"
313
+ point_law_id: tuple[str, ...] = ()
314
+ shell_formulations: tuple[tuple[str, str], ...] = ()
315
+ point_laws: tuple[tuple[str, str], ...] = ()
316
+ footprint_law_id: str | None = None
317
+ footprints: tuple[B2NodalRigidFootprint, ...] = ()
318
+
319
+
320
+ @dataclass(frozen=True)
321
+ class ExperimentalB2MixedReferenceModalCapabilities(ExperimentalB2MixedPanelCapabilities):
322
+ inertia_policy: str = S3_V2D_TRANSLATIONAL_LUMPED_SECTION_AREAL_MASS_V1
323
+ shell_mass_policies: tuple[tuple[str, str], ...] = ()
324
+ s3_rotary_inertia: str = "omitted by S3 facet contributions; shared nodes may retain Q4 director inertia"
325
+ beam_mass_policy: str = "declared physical B2 sectional inertia through the station trace"
326
+ boundary_policy: str | None = None
327
+
328
+
329
+ B2_FREE_FREE_MODAL_ONLY_V1 = "B2_FREE_FREE_MODAL_ONLY_V1"
330
+
331
+
332
+ @dataclass(frozen=True)
333
+ class B2ReferenceModesRequest:
334
+ """Elastic finite reference pairs; boundary policy belongs to the definition."""
335
+
336
+ count: int = 3
337
+ shift_hz: float = 1.
338
+ max_iterations: int = 600
339
+
340
+ def __post_init__(self):
341
+ if (type(self.count) is not int or self.count <= 0 or
342
+ type(self.max_iterations) is not int or self.max_iterations <= 0 or
343
+ isinstance(self.shift_hz, (bool, np.bool_)) or
344
+ not isinstance(self.shift_hz, (int, float, np.floating)) or
345
+ not np.isfinite(self.shift_hz) or self.shift_hz <= 0.):
346
+ raise ValueError("positive finite reference-mode controls required")
347
+
348
+
349
+ @dataclass(frozen=True)
350
+ class B2ReferenceModesResult:
351
+ status: str
352
+ frequencies_hz: tuple[float, ...] = ()
353
+ physical_modes: tuple[tuple[float, ...], ...] = ()
354
+ point_multiplier_modes: tuple[tuple[float, ...], ...] = ()
355
+ equation_residuals: tuple[float, ...] = ()
356
+ constraint_residuals: tuple[float, ...] = ()
357
+ factorization_count: int = 0
358
+ solve_count: int = 0
359
+ failure_reason: str | None = None
360
+ production_qualified: bool = False
361
+
362
+
363
+ @dataclass(frozen=True)
364
+ class B2FreeReferenceModesResult:
365
+ """Six verified physical rigid fields and selected finite flexible pairs.
366
+
367
+ This does not certify complete nullity or an eigenvalue error bound.
368
+ All returned numerical collections own immutable scalar tuples.
369
+ """
370
+
371
+ status: str
372
+ flexible: B2ReferenceModesResult
373
+ rigid_origin_m: tuple[float, ...] = ()
374
+ canonical_rigid_modes: tuple[tuple[float, ...], ...] = ()
375
+ rigid_modes: tuple[tuple[float, ...], ...] = ()
376
+ rigid_multiplier_modes: tuple[tuple[float, ...], ...] = ()
377
+ rigid_kinetic_gram: tuple[tuple[float, ...], ...] = ()
378
+ rigid_equation_residuals: tuple[float, ...] = ()
379
+ rigid_constraint_residuals: tuple[float, ...] = ()
380
+ rigid_flexible_mass_overlap: float = 0.
381
+ spectral_shift_rad2_s2: float = 0.
382
+ failure_reason: str | None = None
383
+ policy: str = B2_FREE_FREE_MODAL_ONLY_V1
384
+ production_qualified: bool = False
385
+
386
+
387
+ @dataclass(frozen=True)
388
+ class B2ModalConstraintRow:
389
+ """Immutable provenance for one actual physical constraint/multiplier row.
390
+
391
+ Rows follow C. Footprint gaps are global; point translation gaps use the
392
+ master shell reference frame and point rotational gaps use the beam
393
+ reference frame. Multipliers are work-conjugate covectors, not display-ready
394
+ global wrenches. Frame columns give the actual basis in global coordinates.
395
+ """
396
+
397
+ row_index: int
398
+ component: str
399
+ point_id: str | None = None
400
+ footprint_id: str | None = None
401
+ shell_node_id: int | None = None
402
+ beam_node_id: int | None = None
403
+ coordinate_frame: str = "global"
404
+ reference_frame_columns: tuple[tuple[float, ...], ...] = (
405
+ (1., 0., 0.), (0., 1., 0.), (0., 0., 1.))
406
+
407
+
408
+ @dataclass(frozen=True)
409
+ class B2FootprintReferenceModesResult(B2ReferenceModesResult):
410
+ """Virgin descriptor modes of the reference-linear, normal-spin locked law.
411
+
412
+ All multipliers follow constraint_rows. Inherited point_multiplier_modes
413
+ contain only the true point prefix, never footprint multipliers.
414
+ """
415
+
416
+ footprint_law_id: str = FOOTPRINT_LAW_ID
417
+ footprint_ids: tuple[str, ...] = ()
418
+ constraint_multiplier_modes: tuple[tuple[float, ...], ...] = ()
419
+ constraint_rows: tuple[B2ModalConstraintRow, ...] = ()
420
+ normal_spin: str = "locked"
421
+ mechanical_qualification: str = "incomplete"
422
+
423
+
424
+ @dataclass(frozen=True)
425
+ class ExperimentalB2Q4PanelResult:
426
+ status: str
427
+ displacements: tuple[float, ...] = ()
428
+ point_multipliers: tuple[tuple[str, tuple[float, ...]], ...] = ()
429
+ affine_reactions: tuple[float, ...] = ()
430
+ point_gaps: tuple[tuple[str, tuple[float, ...]], ...] = ()
431
+ beam_resultants_local: tuple[tuple[str, tuple[tuple[float, ...], ...]], ...] = ()
432
+ internal_strain_energy: float | None = None
433
+ factorization_count: int | None = None
434
+ solve_count: int | None = None
435
+ failure_reason: str | None = None
436
+ production_qualified: bool = False
437
+
438
+
439
+ @dataclass(frozen=True)
440
+ class ExperimentalB2Q4PanelSelectedCase:
441
+ case_id: str
442
+ selected_displacements: tuple[float, ...]
443
+ selected_affine_reactions: tuple[float, ...]
444
+ point_multipliers: tuple[tuple[str, tuple[float, ...]], ...]
445
+ point_gaps: tuple[tuple[str, tuple[float, ...]], ...]
446
+ beam_resultants_local: tuple[tuple[str, tuple[tuple[float, ...], ...]], ...]
447
+ internal_strain_energy: float
448
+
449
+
450
+ @dataclass(frozen=True)
451
+ class ExperimentalB2Q4PanelResponseEnvelope:
452
+ """One selected physical coordinate's signed complete-case extrema.
453
+
454
+ Displacements retain m/rad and affine reactions N/Nm according to the
455
+ selected DOF. Equal extrema are governed by the first submitted case.
456
+ """
457
+
458
+ selected_dof: int
459
+ minimum: float
460
+ minimum_case_id: str
461
+ maximum: float
462
+ maximum_case_id: str
463
+
464
+
465
+ @dataclass(frozen=True)
466
+ class ExperimentalB2Q4PanelMemberResultantEnvelope:
467
+ """Signed local sectional-resultant extrema at one requested station.
468
+
469
+ Components are N, Qy, Qz, Mx, My, Mz in the member's local frame.
470
+ Station indices distinguish repeated normalized station coordinates.
471
+ """
472
+
473
+ member_id: str
474
+ station_index: int
475
+ station_normalized: float
476
+ component: str
477
+ minimum: float
478
+ minimum_case_id: str
479
+ maximum: float
480
+ maximum_case_id: str
481
+
482
+
483
+ @dataclass(frozen=True)
484
+ class ExperimentalB2Q4PanelBatchResult:
485
+ status: str
486
+ selected_dofs: tuple[int, ...]
487
+ selected_points: tuple[str, ...]
488
+ cases: tuple[ExperimentalB2Q4PanelSelectedCase, ...]
489
+ factorization_count: int
490
+ solve_count: int
491
+ maximum_rhs_chunk: int = 1
492
+ failure_reason: str | None = None
493
+ production_qualified: bool = False
494
+ displacement_envelopes: tuple[ExperimentalB2Q4PanelResponseEnvelope, ...] = ()
495
+ affine_reaction_envelopes: tuple[ExperimentalB2Q4PanelResponseEnvelope, ...] = ()
496
+ member_resultant_envelopes: tuple[ExperimentalB2Q4PanelMemberResultantEnvelope, ...] = ()
497
+
498
+
499
+ @dataclass(frozen=True)
500
+ class ExperimentalB2Q4FootprintResult(ExperimentalB2Q4PanelResult):
501
+ footprint_ids: tuple[str, ...] = ()
502
+ footprint_wrenches: tuple[B2NodalRigidFootprintWrench, ...] = ()
503
+ footprint_law_id: str = FOOTPRINT_LAW_ID
504
+
505
+ @dataclass(frozen=True)
506
+ class B2PhysicalRigidFootprintResult(ExperimentalB2Q4PanelResult):
507
+ footprint_ids: tuple[str, ...] = ()
508
+ footprint_wrenches: tuple[B2PhysicalRigidFootprintWrench, ...] = ()
509
+ constraint_multipliers: tuple = ()
510
+ constraint_gaps: tuple = ()
511
+ constraint_rows: tuple = ()
512
+ footprint_law_id: str = PHYSICAL_FOOTPRINT_LAW_ID
513
+
514
+
515
+ @dataclass(frozen=True)
516
+ class B2PhysicalRigidFootprintSelectedCase(ExperimentalB2Q4PanelSelectedCase):
517
+ """Only requested physical outputs from one complete batch case."""
518
+ footprint_wrenches: tuple[B2PhysicalRigidFootprintWrench, ...] = ()
519
+ constraint_multipliers: tuple[float, ...] = ()
520
+ constraint_gaps: tuple[float, ...] = ()
521
+ footprint_law_id: str = PHYSICAL_FOOTPRINT_LAW_ID
522
+
523
+
524
+ @dataclass(frozen=True)
525
+ class B2PhysicalRigidFootprintBatchResult(ExperimentalB2Q4PanelBatchResult):
526
+ selected_footprints: tuple[str, ...] = ()
527
+ constraint_rows: tuple = ()
528
+ footprint_law_id: str = PHYSICAL_FOOTPRINT_LAW_ID
529
+
530
+
531
+ @dataclass(frozen=True)
532
+ class ExperimentalB2Q4FootprintSelectedCase(ExperimentalB2Q4PanelSelectedCase):
533
+ footprint_wrenches: tuple[B2NodalRigidFootprintWrench, ...] = ()
534
+
535
+
536
+ @dataclass(frozen=True)
537
+ class ExperimentalB2Q4FootprintBatchResult(ExperimentalB2Q4PanelBatchResult):
538
+ selected_footprints: tuple[str, ...] = ()
539
+ footprint_law_id: str = FOOTPRINT_LAW_ID
540
+
541
+
542
+ @dataclass(frozen=True)
543
+ class B2BodyLoadedPanelResult(ExperimentalB2Q4PanelResult):
544
+ body_model_identity: str = ""
545
+ body_acceleration_m_s2: tuple[float, ...] = ()
546
+ equilibrated_sections: tuple[B2LoadedMemberSections, ...] = ()
547
+
548
+
549
+ @dataclass(frozen=True)
550
+ class B2BodyLoadedFootprintResult(ExperimentalB2Q4FootprintResult):
551
+ body_model_identity: str = ""
552
+ body_acceleration_m_s2: tuple[float, ...] = ()
553
+ equilibrated_sections: tuple[B2LoadedMemberSections, ...] = ()
554
+
555
+
556
+ @dataclass(frozen=True)
557
+ class B2BodyLoadedPanelSelectedCase(ExperimentalB2Q4PanelSelectedCase):
558
+ body_model_identity: str = ""
559
+ body_acceleration_m_s2: tuple[float, ...] = ()
560
+ equilibrated_sections: tuple[B2LoadedMemberSections, ...] = ()
561
+
562
+
563
+ @dataclass(frozen=True)
564
+ class B2BodyLoadedFootprintSelectedCase(ExperimentalB2Q4FootprintSelectedCase):
565
+ body_model_identity: str = ""
566
+ body_acceleration_m_s2: tuple[float, ...] = ()
567
+ equilibrated_sections: tuple[B2LoadedMemberSections, ...] = ()
568
+
569
+
570
+ @dataclass(frozen=True)
571
+ class B2LoadedMemberPoses:
572
+ member_id: str
573
+ stations: tuple[float, ...]
574
+ poses_local: tuple[tuple[float, ...], ...]
575
+ point_trace_policy: str = field(default=B2_FIXED_END_LOADED_POINT_TRACE_V1_DEV, init=False)
576
+ production_qualified: bool = field(default=False, init=False)
577
+
578
+
579
+ @dataclass(frozen=True)
580
+ class B2LoadedPointPanelResult(B2BodyLoadedPanelResult):
581
+ loaded_member_poses: tuple[B2LoadedMemberPoses, ...] = ()
582
+ loaded_strain_energy: float | None = None
583
+ point_trace_policy: str = field(default=B2_FIXED_END_LOADED_POINT_TRACE_V1_DEV, init=False)
584
+
585
+
586
+ @dataclass(frozen=True)
587
+ class B2LoadedPointPanelSelectedCase(B2BodyLoadedPanelSelectedCase):
588
+ loaded_member_poses: tuple[B2LoadedMemberPoses, ...] = ()
589
+ loaded_strain_energy: float | None = None
590
+ point_trace_policy: str = field(default=B2_FIXED_END_LOADED_POINT_TRACE_V1_DEV, init=False)
591
+
592
+
593
+ def _unique_ids(items, name):
594
+ ids = [getattr(item, name) for item in items]
595
+ if any(not isinstance(value, str) or not value.strip() for value in ids):
596
+ raise ValueError(f"nonempty {name} required")
597
+ if len(set(ids)) != len(ids):
598
+ raise ValueError(f"unique {name} required")
599
+
600
+
601
+ def _checked_recovery(recover_members, available):
602
+ if recover_members is None:
603
+ return None
604
+ if not isinstance(recover_members, Mapping):
605
+ raise ValueError("selected physical B2 recovery mapping required")
606
+ selected = {}
607
+ for member_id, requested in recover_members.items():
608
+ if member_id not in available:
609
+ raise ValueError("selected physical B2 member is not compiled")
610
+ try:
611
+ stations = np.asarray(requested, dtype=float).copy()
612
+ except (TypeError, ValueError, OverflowError) as exc:
613
+ raise ValueError("finite normalized recovery stations required") from exc
614
+ if (stations.ndim != 1 or not len(stations) or
615
+ not np.isfinite(stations).all() or
616
+ np.any((stations < 0.) | (stations > 1.))):
617
+ raise ValueError("finite normalized recovery stations required")
618
+ selected[member_id] = tuple(float(value) for value in stations)
619
+ return selected
620
+
621
+
622
+ def _flat_vertices(vertices):
623
+ base = vertices[0]
624
+ normal = np.cross(vertices[1]-base, vertices[3]-base)
625
+ diameter = max(np.linalg.norm(a-b) for a in vertices for b in vertices)
626
+ magnitude = np.linalg.norm(normal)
627
+ if (not isfinite(diameter) or diameter <= 0. or
628
+ magnitude <= 1e-12*diameter**2 or
629
+ abs((vertices[2]-base) @ normal) > 1e-10*diameter*magnitude):
630
+ raise ValueError("nondegenerate flat Q4 shell required")
631
+
632
+
633
+ def _require_connected_panel(shells, members, points, footprints=()):
634
+ """Check physical incidence without forming a numerical constraint matrix."""
635
+ neighbours = {(0, shell.shell_id): set() for shell in shells}
636
+ neighbours.update({(1, member.member_id): set() for member in members})
637
+
638
+ def join(a, b):
639
+ neighbours[a].add(b)
640
+ neighbours[b].add(a)
641
+
642
+ for kind, entities in ((0, shells), (1, members)):
643
+ owners = {}
644
+ for entity in entities:
645
+ key = (kind, entity.shell_id if kind == 0 else entity.member_id)
646
+ shell_count = 3 if type(entity) is S3V2DPanelShell else 4
647
+ if (kind == 0 and (type(entity.node_ids) is not tuple or
648
+ len(entity.node_ids) != shell_count or
649
+ any(type(node_id) is not int for node_id in entity.node_ids))):
650
+ raise ValueError("three integer S3 V2D nodes required" if shell_count == 3
651
+ else "four integer Q4 nodes required")
652
+ for node_id in entity.node_ids:
653
+ previous = owners.setdefault(node_id, key)
654
+ join(key, previous)
655
+ for point in points:
656
+ shell_key, member_key = (0, point.shell_id), (1, point.member_id)
657
+ if shell_key not in neighbours or member_key not in neighbours:
658
+ raise ValueError("point host IDs must identify declared elements")
659
+ join(shell_key, member_key)
660
+ for group in footprints:
661
+ shell_hosts = [s for s in shells if set(s.node_ids) & set(group.shell_nodes)]
662
+ beam_hosts = [m for m in members if group.beam_node in m.node_ids]
663
+ if (not beam_hosts or set(group.shell_nodes) -
664
+ {n for s in shell_hosts for n in s.node_ids}):
665
+ raise ValueError("known shell nodes and physical B2 endpoint required")
666
+ for shell in shell_hosts:
667
+ for member in beam_hosts:
668
+ join((0, shell.shell_id), (1, member.member_id))
669
+ first = next(iter(neighbours))
670
+ visited = {first}
671
+ pending = [first]
672
+ while pending:
673
+ for other in neighbours[pending.pop()]-visited:
674
+ visited.add(other)
675
+ pending.append(other)
676
+ if len(visited) != len(neighbours):
677
+ raise ValueError("connected shell/beam point panel required")
678
+
679
+
680
+ def _supports_all_panel_rigid_fields(transformation, shell_positions, beam_positions):
681
+ """Detect all six physical rigid fields using only fixed coordinate rows.
682
+
683
+ The constructor requires one connected physical incidence graph, so this
684
+ is its one component's necessary support check, not a stiffness/kernel or
685
+ mechanical qualification claim. u=a+w cross (X-origin), theta=w. Translate
686
+ and scale its origin/length to keep rank independent of coordinate units.
687
+ Stream at most six support rows through a 6x6 QR; no global dense rigid,
688
+ stiffness, constraint or nullspace matrix is assembled.
689
+ """
690
+ selector = transformation.copy()
691
+ selector.eliminate_zeros()
692
+ fixed = np.flatnonzero(np.diff(selector.indptr) == 0)
693
+ if len(fixed) < 6:
694
+ return False
695
+ xyz = tuple(shell_positions[n] for n in sorted(shell_positions))+tuple(
696
+ beam_positions[n] for n in sorted(beam_positions))
697
+ supported_nodes = np.unique(fixed//6)
698
+ origin = np.asarray(xyz[int(supported_nodes[0])], dtype=float)
699
+ length = 0.
700
+ with np.errstate(over="ignore", invalid="ignore"):
701
+ for node in supported_nodes:
702
+ arm = np.asarray(xyz[int(node)], dtype=float)-origin
703
+ if not np.isfinite(arm).all():
704
+ return False
705
+ length = max(length, float(np.max(abs(arm))))
706
+ length = length or 1.
707
+ reduced = np.zeros((6, 6))
708
+ start = 0
709
+ for node in supported_nodes:
710
+ stop = start
711
+ while stop < len(fixed) and fixed[stop]//6 == node:
712
+ stop += 1
713
+ arm = (np.asarray(xyz[int(node)], dtype=float)-origin)/length
714
+ x, y, z = arm
715
+ cross = np.array(((0., -z, y), (z, 0., -x), (-y, x, 0.)))
716
+ rigid = np.block([[np.eye(3), -cross], [np.zeros((3, 3)), np.eye(3)]])
717
+ reduced = np.linalg.qr(np.vstack((reduced, rigid[fixed[start:stop] % 6])),
718
+ mode="r")
719
+ start = stop
720
+ scales = np.linalg.norm(reduced, axis=0)
721
+ if not np.isfinite(reduced).all() or np.any(scales == 0.):
722
+ return False
723
+ singular = np.linalg.svd(reduced/scales, compute_uv=False)
724
+ return bool(singular[-1] > 64*np.finfo(float).eps*max(6, len(fixed))*singular[0])
725
+
726
+
727
+ def _retire_private_q4_mesh(owner):
728
+ mesh = owner._private_q4_mesh
729
+ for element in owner._private_q4_elements:
730
+ _retire_q4_private_mesh_caches(element, mesh)
731
+ owner._private_q4_elements.clear()
732
+ owner._private_q4_mesh = None
733
+
734
+
735
+ def _private_q4_constructor_lifetime(initialize):
736
+ @wraps(initialize)
737
+ def guarded(owner, *args, **kwargs):
738
+ owner._private_q4_mesh = None
739
+ owner._private_q4_elements = []
740
+ try:
741
+ return initialize(owner, *args, **kwargs)
742
+ except BaseException as original:
743
+ try:
744
+ _retire_private_q4_mesh(owner)
745
+ except BaseException as cleanup:
746
+ original.add_note("private Q4 retirement failed: " + repr(cleanup))
747
+ raise
748
+ return guarded
749
+
750
+
751
+ class ExperimentalB2Q4PanelAnalysis:
752
+ """Own one connected reference-linear panel and its sparse factorization.
753
+
754
+ The coordinate order is sorted shell node IDs, then sorted beam node IDs,
755
+ six global translations/rotations for each. Affine ``T/u0`` is supplied
756
+ in that order. Reactions are affine reactions; they are support reactions
757
+ only when the affine plan fixes coordinates without other MPCs.
758
+ """
759
+
760
+ _mixed_shells = False
761
+
762
+ @_private_q4_constructor_lifetime
763
+ def __init__(self, shell_nodes_m: Mapping[int, Sequence[float]],
764
+ shells: Sequence[Q4PanelShell],
765
+ members: Sequence[PhysicalB2Member | CompiledPhysicalB2Member],
766
+ points: Sequence[Q4B2Point], *,
767
+ transformation: sparse.spmatrix, prescribed_offset,
768
+ footprints: Sequence[B2NodalRigidFootprint] = (),
769
+ reference_inertia_policy: str | None = None,
770
+ reference_boundary_policy: str | None = None,
771
+ point_trace_policy: str | None = None,
772
+ physical_footprints: Sequence[B2PhysicalRigidFootprint] = ()):
773
+ diagnostic = active_capture()
774
+ if diagnostic is not None:
775
+ diagnostic.begin()
776
+ if not physical_footprints or points or footprints or reference_inertia_policy is not None or reference_boundary_policy is not None or point_trace_policy is not None:
777
+ raise DiagnosticRefusal("capture admits physical static footprint compilation only")
778
+ shells = tuple(shells)
779
+ supplied_members = tuple(members)
780
+ points = tuple(points)
781
+ footprints = tuple(footprints)
782
+ physical_footprints = tuple(physical_footprints)
783
+ self._physical_footprint_law = bool(physical_footprints)
784
+ if physical_footprints:
785
+ if points or footprints or reference_inertia_policy is not None or reference_boundary_policy is not None or point_trace_policy is not None:
786
+ raise ValueError("separate physical footprint static family required")
787
+ footprints = physical_footprints
788
+ modal_mass = reference_inertia_policy is not None
789
+ if reference_boundary_policy is not None and (
790
+ type(reference_boundary_policy) is not str or
791
+ reference_boundary_policy != B2_FREE_FREE_MODAL_ONLY_V1 or
792
+ not modal_mass):
793
+ raise ValueError("explicit mixed free-free modal-only boundary policy required")
794
+ self._modal_only = reference_boundary_policy is not None
795
+ self._reference_boundary_policy = reference_boundary_policy
796
+ if modal_mass and (not self._mixed_shells or
797
+ type(reference_inertia_policy) is not str or
798
+ reference_inertia_policy != S3_V2D_TRANSLATIONAL_LUMPED_SECTION_AREAL_MASS_V1 or
799
+ footprints):
800
+ raise ValueError("explicit native-S3 reference inertia policy without footprints required")
801
+ has_s3 = any(type(shell) is S3V2DPanelShell for shell in shells)
802
+ if point_trace_policy is not None and (
803
+ type(point_trace_policy) is not str or
804
+ point_trace_policy != B2_FIXED_END_LOADED_POINT_TRACE_V1_DEV or
805
+ self._mixed_shells or has_s3 or footprints or modal_mass):
806
+ raise ValueError("explicit Q4-only fixed-end loaded point trace without footprints or modes required")
807
+ self._point_trace_policy = point_trace_policy
808
+ if has_s3 and footprints and not self._physical_footprint_law:
809
+ raise ValueError("S3 mixed-panel nodal footprints are unsupported")
810
+ admitted_shells = ((Q4PanelShell, S3V2DPanelShell) if self._mixed_shells
811
+ else (Q4PanelShell,))
812
+ footprint_type = B2PhysicalRigidFootprint if self._physical_footprint_law else B2NodalRigidFootprint
813
+ if any(type(g) is not footprint_type for g in footprints):
814
+ raise ValueError("typed experimental B2 nodal footprints required")
815
+ if (len(shells) < 2 or len(supplied_members) < 2 or not (points or footprints) or
816
+ any((type(shell) not in admitted_shells if self._mixed_shells else
817
+ not isinstance(shell, Q4PanelShell)) for shell in shells) or
818
+ any(not isinstance(member, (PhysicalB2Member,
819
+ CompiledPhysicalB2Member))
820
+ for member in supplied_members) or
821
+ any(not isinstance(point, Q4B2Point) for point in points)):
822
+ raise ValueError("multi-Q4/S3/multi-B2 physical point panel required" if
823
+ self._mixed_shells else
824
+ "multi-Q4/multi-B2 physical point panel required")
825
+ _unique_ids(shells, "shell_id")
826
+ _unique_ids(points, "point_id")
827
+ definitions = tuple(member.definition if isinstance(
828
+ member, CompiledPhysicalB2Member) else member
829
+ for member in supplied_members)
830
+ if diagnostic is not None and (any(shell.density_kg_m3 is not None for shell in shells) or
831
+ any(member.mass is not None for member in definitions)):
832
+ raise DiagnosticRefusal("capture of physical mass/body-map storage is not admitted")
833
+ if modal_mass:
834
+ for shell in shells:
835
+ density = shell.density_kg_m3
836
+ if isinstance(density, (bool, np.bool_)) or not isinstance(density, Real):
837
+ raise ValueError("mixed reference modes require complete positive finite shell density")
838
+ try:
839
+ density_value = float(density)
840
+ except OverflowError as exc:
841
+ raise ValueError("mixed reference modes require representable shell density") from exc
842
+ if not isfinite(density_value) or density_value <= 0.:
843
+ raise ValueError("mixed reference modes require complete positive finite shell density")
844
+ if any(member.mass is None for member in definitions):
845
+ raise ValueError("mixed reference modes require complete declared physical B2 mass")
846
+ if any(member.geometric is not None for member in definitions):
847
+ raise ValueError("mixed reference modes are virgin elastic; prestress/geometric metadata unsupported")
848
+ _unique_ids(definitions, "member_id")
849
+ _require_connected_panel(shells, definitions, points, footprints)
850
+ cache = PhysicalB2OperatorCache()
851
+ if diagnostic is not None: diagnostic.mark("member_compilation")
852
+ compiled = tuple(
853
+ member if isinstance(member, CompiledPhysicalB2Member)
854
+ else member.compile_local(operator_cache=cache)
855
+ for member in supplied_members)
856
+ if diagnostic is not None: diagnostic.mark("topology_mapping")
857
+ shell_ids = tuple(sorted(shell_nodes_m))
858
+ if any(type(node_id) is not int for node_id in shell_ids):
859
+ raise ValueError("integer shell node IDs required")
860
+ positions = {}
861
+ for node_id in shell_ids:
862
+ vector = np.array(shell_nodes_m[node_id], dtype=float, copy=True)
863
+ if vector.shape != (3,) or not np.isfinite(vector).all():
864
+ raise ValueError("finite shell node coordinates required")
865
+ positions[node_id] = vector
866
+ used_shell_nodes = {node_id for shell in shells for node_id in shell.node_ids}
867
+ if used_shell_nodes != set(shell_ids):
868
+ raise ValueError("every declared shell node must belong to a Q4/S3" if self._mixed_shells
869
+ else "every declared shell node must belong to a Q4")
870
+ shell_index = {node_id: i for i, node_id in enumerate(shell_ids)}
871
+ beam_positions = {}
872
+ for member in definitions:
873
+ for node_id, point in zip(member.node_ids, member.reference_points_m):
874
+ old = beam_positions.setdefault(node_id, point)
875
+ if old != point:
876
+ raise ValueError("shared B2 node has inconsistent position")
877
+ beam_ids = tuple(sorted(beam_positions))
878
+ beam_index = {node_id: i+len(shell_ids)
879
+ for i, node_id in enumerate(beam_ids)}
880
+ footprint_compiler = compile_physical_footprints if self._physical_footprint_law else compile_footprints
881
+ footprints, footprint_incidence = footprint_compiler(
882
+ footprints, positions, shells, beam_positions,
883
+ shell_index, beam_index, points)
884
+ total = 6*(len(shell_ids)+len(beam_ids))
885
+ for shell in shells:
886
+ density = shell.density_kg_m3
887
+ if density is not None:
888
+ if isinstance(density, (bool, np.bool_)) or not isinstance(density, Real):
889
+ raise ValueError("positive finite shell density required when supplied")
890
+ try:
891
+ density_value = float(density)
892
+ except (ValueError, TypeError, OverflowError) as exc:
893
+ raise ValueError("representable positive shell density required") from exc
894
+ if not isfinite(density_value) or density_value <= 0.:
895
+ raise ValueError("positive finite shell density required when supplied")
896
+ complete_body_mass = (all(shell.density_kg_m3 is not None for shell in shells)
897
+ and all(member.definition.mass is not None for member in compiled))
898
+ body_map = np.zeros((total, 3)) if complete_body_mass else None
899
+ body_masses = []
900
+ if diagnostic is not None: diagnostic.mark("shell_model_setup")
901
+ model = FEModel("experimental-multi-Q4-B2-point")
902
+ self._private_q4_mesh = model.mesh
903
+ for node_id in shell_ids:
904
+ model.add_node(node_id, *positions[node_id])
905
+ rows, columns, values = [], [], []
906
+ mass_rows, mass_columns, mass_values = [], [], []
907
+ complete_mass = (not has_s3 or modal_mass) and all(shell.density_kg_m3 is not None for shell in shells)
908
+ complete_mass = complete_mass and all(member.definition.mass is not None
909
+ for member in compiled)
910
+ shell_lookup = {}
911
+ pieces = []
912
+ for index, shell in enumerate(shells, 1):
913
+ count = 3 if type(shell) is S3V2DPanelShell else 4
914
+ if (len(shell.node_ids) != count or len(set(shell.node_ids)) != count or
915
+ any(node_id not in shell_index for node_id in shell.node_ids)):
916
+ raise ValueError("distinct declared Q4/S3 nodes required" if self._mixed_shells
917
+ else "four declared Q4 nodes required")
918
+ vertices = np.array([positions[node_id] for node_id in shell.node_ids])
919
+ if count == 4:
920
+ _flat_vertices(vertices)
921
+ material_id = f"panel-material-{index}"
922
+ if modal_mass:
923
+ model.add_material(material_id, shell.E, shell.nu, density=shell.density_kg_m3)
924
+ else:
925
+ model.add_material(material_id, shell.E, shell.nu)
926
+ if self._mixed_shells:
927
+ element = create_shell_element(
928
+ index, list(shell.node_ids), material_id,
929
+ formulation="e4-pl-s3-v2d" if count == 3 else "e4-pl",
930
+ thickness=shell.thickness_m,
931
+ reference_normal=np.asarray(shell.reference_normal, dtype=float))
932
+ else:
933
+ element = create_shell_element(
934
+ index, shell.node_ids, material_id, formulation="e4-pl", thickness=shell.thickness_m,
935
+ reference_normal=np.asarray(shell.reference_normal, dtype=float))
936
+ if count == 4:
937
+ self._private_q4_elements.append(element)
938
+ model.add_element(index, element)
939
+ pieces.append((shell, vertices, element, material_id, count))
940
+ # Native S3 scope validation must see the complete shell registry.
941
+ if diagnostic is not None: diagnostic.mark("shell_response_scatter")
942
+ for shell, vertices, element, material_id, count in pieces:
943
+ local = element.compute_stiffness_matrix(
944
+ model.mesh, model.get_material(material_id))
945
+ if local.shape != (6*count, 6*count) or not np.isfinite(local).all():
946
+ raise ExperimentalB2Q4PanelError("finite Q4/S3 stiffness required" if self._mixed_shells
947
+ else "finite Q4 stiffness required")
948
+ slots = np.array([6*shell_index[node_id]+component
949
+ for node_id in shell.node_ids
950
+ for component in range(6)])
951
+ _scatter(rows, columns, values, slots, local)
952
+ if count == 4 and shell.density_kg_m3 is not None:
953
+ local_mass = physical_mass(
954
+ vertices, density_kg_m3=shell.density_kg_m3,
955
+ thickness_m=shell.thickness_m,
956
+ reference_normal=shell.reference_normal)
957
+ if complete_mass:
958
+ _scatter(mass_rows, mass_columns, mass_values, slots, local_mass)
959
+ if body_map is not None:
960
+ translations = np.zeros((24, 3))
961
+ for i in range(4):
962
+ translations[6*i:6*i+3] = np.eye(3)
963
+ local_body = local_mass@translations
964
+ body_map[slots] += local_body
965
+ body_masses.append(fsum(float(v) for v in local_body[::6, 0]))
966
+ elif count == 3 and modal_mass:
967
+ local_mass = element.compute_mass_matrix(model.mesh, model.get_material(material_id))
968
+ angular = np.array([6*i+j for i in range(3) for j in (3, 4, 5)])
969
+ translation = np.array([6*i+j for i in range(3) for j in (0, 1, 2)])
970
+ if (local_mass.shape != (18, 18) or not np.isfinite(local_mass).all()
971
+ or np.any(np.diag(local_mass)[translation] <= 0.)
972
+ or np.any(local_mass[angular, :] != 0.)
973
+ or np.any(local_mass[:, angular] != 0.)):
974
+ raise ValueError("native S3 translational mass is not representable with zero angular inertia")
975
+ _scatter(mass_rows, mass_columns, mass_values, slots, local_mass)
976
+ if count == 3 and body_map is not None:
977
+ density = shell.density_kg_m3
978
+ area = .5*float(np.linalg.norm(np.cross(vertices[1]-vertices[0], vertices[2]-vertices[0])))
979
+ weight = float(density)*shell.thickness_m*area/3.
980
+ if not isfinite(weight) or weight <= 0.:
981
+ raise RepresentabilityError("S3 reference body load is not representable")
982
+ for i in range(3):
983
+ body_map[slots[6*i:6*i+3]] += weight*np.eye(3)
984
+ body_masses.append(3.*weight)
985
+ shell_lookup[shell.shell_id] = (vertices, slots)
986
+ if diagnostic is not None: diagnostic.mark("beam_response_scatter")
987
+ member_lookup = {}
988
+ for member in compiled:
989
+ frame = member.local_dof_map()
990
+ local = frame.T @ member.operator.evaluate(np.zeros(12)).tangent @ frame
991
+ if local.shape != (12, 12) or not np.isfinite(local).all():
992
+ raise ExperimentalB2Q4PanelError("finite physical B2 stiffness required")
993
+ slots = np.array([6*beam_index[node_id]+component
994
+ for node_id in member.definition.node_ids
995
+ for component in range(6)])
996
+ _scatter(rows, columns, values, slots, local)
997
+ if complete_mass:
998
+ local_mass = frame.T@member.local_candidate_mass_matrix()@frame
999
+ if not np.isfinite(local_mass).all():
1000
+ raise ValueError("physical B2 mass assembly is not representable")
1001
+ if modal_mass and np.any(np.diag(local_mass)[[0, 1, 2, 6, 7, 8]] <= 0.):
1002
+ raise ValueError("physical B2 translational mass is not representable")
1003
+ _scatter(mass_rows, mass_columns, mass_values, slots, local_mass)
1004
+ if body_map is not None:
1005
+ body_map[slots] += _beam_body_map(member, frame)
1006
+ body_masses.append(member.definition.length_m*member.definition.mass.matrix_per_m[0][0])
1007
+ member_lookup[member.definition.member_id] = (member, slots)
1008
+ if diagnostic is not None: diagnostic.mark("K_CSR")
1009
+ K = sparse.coo_matrix((values, (rows, columns)),
1010
+ shape=(total, total)).tocsr()
1011
+ _release_coo_scratch(rows, columns, values, diagnostic, "K_CSR")
1012
+ if diagnostic is not None: diagnostic.mark("attachment_rows")
1013
+ constraint_rows, constraint_cols, constraint_values = [], [], []
1014
+ point_reference_frames = []
1015
+ point_laws = []
1016
+ buckling_points = []
1017
+ section_point_actions = {}
1018
+ loaded_point_entries = {}
1019
+ for point_index, point in enumerate(points):
1020
+ if (point.shell_id not in shell_lookup or
1021
+ point.member_id not in member_lookup):
1022
+ raise ValueError("point host IDs must identify declared elements")
1023
+ vertices, shell_slots = shell_lookup[point.shell_id]
1024
+ member, beam_slots = member_lookup[point.member_id]
1025
+ law = compile_reference_point(
1026
+ member.definition, vertices, station=point.station,
1027
+ offset_local_m=point.offset_local_m)
1028
+ point_laws.append((point.point_id, law.law_id))
1029
+ buckling_points.append((point, law, vertices.copy(), member,
1030
+ np.r_[shell_slots, beam_slots]))
1031
+ if point_trace_policy is not None:
1032
+ port_constraint = (law.beam_constraint_differential @
1033
+ np.kron(np.eye(2), member.definition.local_frame_rows().T))
1034
+ loaded_point_entries.setdefault(point.member_id, []).append(
1035
+ (point_index, float(point.station), tuple(point.offset_local_m), port_constraint))
1036
+ if 0. < point.station < 1.:
1037
+ d = np.array(point.offset_local_m, dtype=float, copy=True)
1038
+ shift = np.eye(6)
1039
+ shift[3:, :3] = ((0., -d[2], d[1]), (d[2], 0., -d[0]), (-d[1], d[0], 0.))
1040
+ pullback = shift@np.kron(np.eye(2), member.definition.local_frame_rows())@(-law.beam_constraint_differential.T)
1041
+ pullback.setflags(write=False)
1042
+ section_point_actions.setdefault(point.member_id, []).append(
1043
+ (point_index, float(point.station), pullback))
1044
+ if footprints:
1045
+ point_reference_frames.append(tuple(
1046
+ tuple(tuple(map(float, row)) for row in frame)
1047
+ for frame in law.reference_frames))
1048
+ slots = np.r_[shell_slots, beam_slots]
1049
+ for local_row in range(6):
1050
+ for local_col, slot in enumerate(slots):
1051
+ constraint_rows.append(6*point_index+local_row)
1052
+ constraint_cols.append(int(slot))
1053
+ constraint_values.append(law.constraint[local_row, local_col])
1054
+ for item in footprint_incidence:
1055
+ slots = item.shell_slots+item.beam_slots
1056
+ width = getattr(item, "row_count", 6)
1057
+ local = np.column_stack((item.shell_matrix, getattr(item, "beam_matrix", np.eye(6))))
1058
+ for local_row in range(width):
1059
+ for local_col, slot in enumerate(slots):
1060
+ constraint_rows.append(item.row_start+local_row)
1061
+ constraint_cols.append(slot)
1062
+ constraint_values.append(local[local_row, local_col])
1063
+ if diagnostic is not None: diagnostic.mark("C_CSR")
1064
+ C = sparse.coo_matrix(
1065
+ (constraint_values, (constraint_rows, constraint_cols)),
1066
+ shape=(6*len(points)+sum(getattr(item,"row_count",6) for item in footprint_incidence), total)).tocsr()
1067
+ _release_coo_scratch(constraint_rows, constraint_cols, constraint_values, diagnostic, "C_CSR")
1068
+ if diagnostic is not None: diagnostic.mark("support_reduction_scaling_elimination")
1069
+ loaded_members = {}
1070
+ H = None
1071
+ if point_trace_policy is not None:
1072
+ h_rows, h_columns, h_values = [], [], []
1073
+ for member_id, (member, _) in member_lookup.items():
1074
+ compiled_load = LoadedPointMember(member, loaded_point_entries.get(member_id, ()),
1075
+ body_enabled=body_map is not None)
1076
+ loaded_members[member_id] = compiled_load
1077
+ slots = np.array([6*index+channel for index in compiled_load.point_indices
1078
+ for channel in range(6)])
1079
+ _scatter(h_rows, h_columns, h_values, slots, compiled_load.constraint_compliance)
1080
+ H = sparse.coo_matrix((h_values, (h_rows, h_columns)), shape=(C.shape[0],)*2).tocsr()
1081
+ # Stored analytical zeros carry no numerical information. Remove
1082
+ # them before congruence so underflow checks compare nonzero patterns.
1083
+ H.eliminate_zeros()
1084
+ try:
1085
+ T = transformation.tocsr(copy=True)
1086
+ u0 = np.array(prescribed_offset, dtype=float, copy=True)
1087
+ except (AttributeError, TypeError, ValueError) as exc:
1088
+ raise ValueError("finite sparse panel affine plan required") from exc
1089
+ minimum_columns = C.shape[0] if point_trace_policy is None else 0
1090
+ if (T.shape[0] != total or T.shape[1] <= minimum_columns or
1091
+ u0.shape != (total,) or not np.isfinite(T.data).all() or
1092
+ not np.isfinite(u0).all()):
1093
+ raise ValueError("finite sparse panel affine plan required")
1094
+ if modal_mass:
1095
+ if np.any(u0 != 0.):
1096
+ raise ValueError("virgin mixed reference modes require zero prescribed offset")
1097
+ selector = T.copy()
1098
+ selector.eliminate_zeros()
1099
+ if (np.any(np.diff(selector.tocsc().indptr) != 1) or
1100
+ np.any(np.diff(selector.indptr) > 1)):
1101
+ raise ValueError("mixed reference modes require coordinate supports without MPCs")
1102
+ if self._modal_only:
1103
+ delta = (T-sparse.eye(total, format="csr")) if T.shape == (total, total) else None
1104
+ if delta is None or np.any(delta.data != 0.):
1105
+ raise ValueError("free-free modal-only policy requires identity transformation without supports")
1106
+ elif not _supports_all_panel_rigid_fields(T, positions, beam_positions):
1107
+ raise ValueError("mixed reference modes require supports detecting all six rigid fields; free-free unsupported")
1108
+ reduced_c = (C @ T).tocsc()
1109
+ if point_trace_policy is None:
1110
+ _require_independent_supported_point_rows(reduced_c)
1111
+ elif not np.isfinite(reduced_c.data).all():
1112
+ raise ValueError("finite supported loaded-point rows required")
1113
+ # Loaded fixed-end compliance can anchor a multiplier row even when
1114
+ # its free-coordinate trace vanishes. Independence is therefore a
1115
+ # property of the complete [K, C.T; C, -H] factorization on this route.
1116
+ reduced_k = (T.T @ K @ T).tocsc()
1117
+ # Congruence scaling changes factorization coordinates only. The
1118
+ # physical displacement and multiplier are both restored after solve.
1119
+ # Without this, a valid slender B2 section can leave point gaps above
1120
+ # the physical acceptance tolerance despite a sound local operator.
1121
+ handle = factor_scale = None
1122
+ attachment_maps = None
1123
+ attachment_reason = "not_physical_static_family"
1124
+ if not self._modal_only:
1125
+ scaled_kkt, factor_scale = _scaled_point_kkt(reduced_k, reduced_c, H)
1126
+ if self._physical_footprint_law and not points and point_trace_policy is None and H is None:
1127
+ attachment_maps, attachment_reason = prepare_attachment_elimination(
1128
+ scaled_kkt, reduced_k.shape[0], T, footprint_incidence)
1129
+ # Ineligible attachment layouts use the exact existing factor path.
1130
+ # An eligible path factors once and restores the full scaled answer
1131
+ # before original physical checks/recovery; spectral operators are
1132
+ # still prepared from original K/C/T, not these static embeddings.
1133
+ if diagnostic is not None:
1134
+ diagnostic.prefactor(
1135
+ dict(K=K,C=C,T=T,u0=u0,H=H,reduced_k=reduced_k,reduced_c=reduced_c,
1136
+ scaled_kkt=scaled_kkt,factor_scale=factor_scale,attachment_maps=attachment_maps,
1137
+ model=model,positions=positions,beam_positions=beam_positions,shell_index=shell_index,
1138
+ beam_index=beam_index,shell_lookup=shell_lookup,member_lookup=member_lookup,
1139
+ footprint_incidence=footprint_incidence,compiled=compiled,cache=cache,
1140
+ private_q4_elements=self._private_q4_elements),
1141
+ dict(rows=rows,columns=columns,values=values,constraint_rows=constraint_rows,
1142
+ constraint_cols=constraint_cols,constraint_values=constraint_values,
1143
+ pieces=pieces,local=local,slots=slots,frame=frame),
1144
+ (*shells,*definitions,*footprints,*(m.section for m in definitions),
1145
+ *(m for m in supplied_members if isinstance(m,CompiledPhysicalB2Member))))
1146
+ handle = (attachment_maps.factorize() if attachment_maps is not None else
1147
+ factorize_cached(scaled_kkt, MatrixClass.SYMMETRIC_INDEFINITE))
1148
+ if handle.status != "ok":
1149
+ raise ExperimentalB2Q4PanelError(
1150
+ "panel factorization failed: " + str(handle.failure_reason))
1151
+ if diagnostic is not None:
1152
+ try: diagnostic.mark("owner_publication")
1153
+ except BaseException:
1154
+ if handle is not None:
1155
+ try: handle.close()
1156
+ except BaseException: pass
1157
+ raise
1158
+ self._modal_positions = (np.array(
1159
+ [positions[i] for i in shell_ids]+[beam_positions[i] for i in beam_ids],
1160
+ dtype=float, copy=True) if self._modal_only else None)
1161
+ self.shell_node_ids = shell_ids
1162
+ self.beam_node_ids = beam_ids
1163
+ self.point_ids = tuple(point.point_id for point in points)
1164
+ self._has_s3 = has_s3
1165
+ self._reference_inertia_policy = reference_inertia_policy
1166
+ self._shell_formulations = tuple((shell.shell_id, element.formulation_id)
1167
+ for shell, _, element, _, _ in pieces)
1168
+ self._point_laws = tuple(point_laws)
1169
+ self._point_reference_frames = tuple(point_reference_frames)
1170
+ self.footprints = footprints
1171
+ self.footprint_ids = tuple(g.footprint_id for g in footprints)
1172
+ self._footprint_incidence = footprint_incidence
1173
+ self._members = tuple(member_lookup.items())
1174
+ self._section_point_actions = {key: tuple(value) for key, value in section_point_actions.items()}
1175
+ self._loaded_members = loaded_members
1176
+ self._H = H
1177
+ self._abs_H = None if H is None else abs(H)
1178
+ self._K = K
1179
+ # Spectral-only captures reuse the exact prepared reference operators;
1180
+ # no accepted state or nonlinear material history is manufactured.
1181
+ self._buckling_model = model
1182
+ self._buckling_shells = tuple((shell, element, material_id,
1183
+ shell_lookup[shell.shell_id][1]) for shell, _, element, material_id, _ in pieces)
1184
+ self._buckling_points = tuple(buckling_points)
1185
+ if body_map is not None and not np.isfinite(body_map).all():
1186
+ raise RepresentabilityError("reference body-load map is not representable")
1187
+ self._body_map = body_map
1188
+ self._body_mass = None
1189
+ if self._body_map is not None:
1190
+ try:
1191
+ self._body_mass = fsum(body_masses)
1192
+ except OverflowError as exc:
1193
+ raise RepresentabilityError("total reference body mass is not representable") from exc
1194
+ if not isfinite(self._body_mass) or self._body_mass <= 0.:
1195
+ raise RepresentabilityError("total reference body mass is not representable")
1196
+ self._body_map.setflags(write=False)
1197
+ self._M = (sparse.coo_matrix((mass_values, (mass_rows, mass_columns)),
1198
+ shape=(total, total)).tocsr()
1199
+ if complete_mass else None)
1200
+ if self._M is not None and not np.isfinite(self._M.data).all():
1201
+ raise ValueError("physical panel mass assembly is not representable")
1202
+ self._C = C
1203
+ # Freeze a translation scale from the physical reference geometry;
1204
+ # rotation constraints remain in radians. Origin shifts do not alter it.
1205
+ self._compatibility_length = None
1206
+ if footprints:
1207
+ reference = np.asarray(tuple(positions.values())+tuple(beam_positions.values()))
1208
+ relative = reference-reference[0]
1209
+ radius = np.linalg.norm(relative-np.mean(relative, axis=0), axis=1)
1210
+ self._compatibility_length = 2.*float(np.max(radius))
1211
+ if not isfinite(self._compatibility_length) or self._compatibility_length <= 0.:
1212
+ raise ValueError("representable physical reference length required")
1213
+ self._T = T
1214
+ self._Tt = T.T.tocsr()
1215
+ self._projection_multi = np.flatnonzero(
1216
+ np.diff(self._Tt.indptr) > 1)
1217
+ self._projection_single = np.flatnonzero(
1218
+ np.diff(self._Tt.indptr) == 1)
1219
+ # Reuse the immutable CSR patterns; only the absolute values need
1220
+ # separate storage for repeated RHS validation.
1221
+ self._abs_K = sparse.csr_matrix(
1222
+ (np.abs(K.data), K.indices, K.indptr), shape=K.shape, copy=False)
1223
+ self._abs_C = sparse.csr_matrix(
1224
+ (np.abs(C.data), C.indices, C.indptr), shape=C.shape, copy=False)
1225
+ self._abs_Ct = abs(C.T.tocsr())
1226
+ self._abs_Tt = abs(T.T.tocsr())
1227
+ self._u0 = u0
1228
+ # One compilation-time binding covers the numerical model and physical
1229
+ # member data used by body action/recovery. Never hash the model per case.
1230
+ identity = sha256(b"B2_REFERENCE_BODY_LOAD_BINDING_V1_DEV")
1231
+ if self._modal_only:
1232
+ identity.update(reference_boundary_policy.encode("ascii"))
1233
+ identity.update(repr((shell_ids, beam_ids, tuple((i, tuple(positions[i])) for i in shell_ids), self._shell_formulations,
1234
+ tuple(point_laws), reference_inertia_policy)).encode("utf-8"))
1235
+ for matrix in (K, C, T):
1236
+ data = matrix.tocsr()
1237
+ identity.update(repr(data.shape).encode("ascii"))
1238
+ for array in (data.indptr, data.indices):
1239
+ identity.update(np.asarray(array, dtype="<i8").tobytes())
1240
+ identity.update(np.asarray(data.data, dtype="<f8").tobytes())
1241
+ identity.update(np.asarray(u0, dtype="<f8").tobytes())
1242
+ if body_map is not None:
1243
+ identity.update(np.asarray(body_map, dtype="<f8").tobytes())
1244
+ for member_id, (member, _) in self._members:
1245
+ identity.update(repr((member_id, member.definition.reference_points_m,
1246
+ member.definition.local_y_global, member.definition.section,
1247
+ member.definition.mass)).encode("utf-8"))
1248
+ for key, actions in sorted(self._section_point_actions.items()):
1249
+ identity.update(repr((key, tuple((i, s) for i, s, _ in actions))).encode("utf-8"))
1250
+ for _, _, pullback in actions:
1251
+ identity.update(np.asarray(pullback, dtype="<f8").tobytes())
1252
+ if point_trace_policy is not None:
1253
+ identity.update(point_trace_policy.encode("ascii"))
1254
+ for matrix in (H,):
1255
+ identity.update(np.asarray(matrix.indptr, dtype="<i8").tobytes())
1256
+ identity.update(np.asarray(matrix.indices, dtype="<i8").tobytes())
1257
+ identity.update(np.asarray(matrix.data, dtype="<f8").tobytes())
1258
+ for member_id, compiled_load in sorted(loaded_members.items()):
1259
+ identity.update(repr((member_id, compiled_load.point_indices,
1260
+ compiled_load.stations, compiled_load.offsets,
1261
+ compiled_load.quadrature_stations)).encode("utf-8"))
1262
+ for value in (compiled_load.maps.uniform_pose_map, compiled_load.maps.point_pose_map,
1263
+ compiled_load.maps.point_compliance, compiled_load.body_wrench_map,
1264
+ compiled_load.body_constraint_map, compiled_load.quadrature_weights,
1265
+ *compiled_load.port_constraints):
1266
+ identity.update(repr(value.shape).encode("ascii"))
1267
+ identity.update(np.asarray(value, dtype="<f8").tobytes())
1268
+ self._body_model_identity = identity.hexdigest()
1269
+ self._footprint_rigid_supported = (not footprints or
1270
+ (self._coordinate_modal_supports() and
1271
+ _supports_all_panel_rigid_fields(T, positions, beam_positions)))
1272
+ self._ku0 = np.asarray(K @ u0).reshape(-1)
1273
+ self._cu0 = np.asarray(C @ u0).reshape(-1)
1274
+ # Exact homogeneous singleton constraints prescribe a reduced
1275
+ # coordinate to zero. Store those substitutions once, rather than
1276
+ # allowing factor roundoff to turn a mathematically zero channel into
1277
+ # its own nonzero relative compatibility scale.
1278
+ self._physical_singletons = ()
1279
+ if self._physical_footprint_law and not self._modal_only:
1280
+ self._physical_singletons = self._structural_physical_singletons(C, T, u0)
1281
+ self._factor_scale = factor_scale
1282
+ self._handle = handle
1283
+ self._attachment_elimination = attachment_maps
1284
+ self._attachment_elimination_info = ({"status": "eligible", **attachment_maps.info}
1285
+ if attachment_maps is not None else
1286
+ {"status": "legacy_fallback", "reason": attachment_reason})
1287
+ # Retain the full original scaled operator on the physical route.
1288
+ # Eligible handles factorize its attachment reduction and recover full
1289
+ # answers; original checks/correction still use the same reused factors.
1290
+ self._physical_scaled_kkt = (scaled_kkt if self._physical_footprint_law
1291
+ and not self._modal_only else None)
1292
+ self._resolution_kkt = None
1293
+ self._affine_resolution_cache = None
1294
+ self._closed = False
1295
+ if diagnostic is not None:
1296
+ try: diagnostic.finish()
1297
+ except BaseException:
1298
+ try: self.close()
1299
+ except BaseException: pass
1300
+ raise
1301
+
1302
+ def _physical_equilibrium_ratio(self, answer, applied):
1303
+ """Measure the existing per-row equilibrium check before recovery."""
1304
+ q, lam = answer[:-len(self._cu0)], answer[-len(self._cu0):]
1305
+ u = np.asarray(self._T @ q).reshape(-1)+self._u0
1306
+ force = np.asarray(self._K @ u).reshape(-1)
1307
+ constraint_force = np.asarray(self._C.T @ lam).reshape(-1)
1308
+ projected = self._project(force+constraint_force-applied)
1309
+ physical_force_scale = np.asarray(
1310
+ self._abs_K @ np.abs(u)+self._abs_Ct @ np.abs(lam)+
1311
+ np.abs(applied)).reshape(-1)
1312
+ equilibrium_scale = np.asarray(self._abs_Tt @ physical_force_scale).reshape(-1)
1313
+ by_node = physical_force_scale.reshape(-1, 6)
1314
+ physical_roundoff = np.tile(
1315
+ (float(np.max(by_node[:, :3])),)*3+
1316
+ (float(np.max(by_node[:, 3:])),)*3, len(by_node))
1317
+ projected_roundoff = np.asarray(self._abs_Tt @ physical_roundoff).reshape(-1)
1318
+ tolerance = (1e-9*equilibrium_scale+
1319
+ len(answer)*np.finfo(float).eps*projected_roundoff)
1320
+ if (not np.isfinite(projected).all() or
1321
+ not np.isfinite(tolerance).all() or np.any(tolerance < 0.)):
1322
+ raise _NumericalFailure("panel equilibrium correction is not representable")
1323
+ with np.errstate(divide="ignore", invalid="ignore"):
1324
+ ratios = np.divide(np.abs(projected), tolerance,
1325
+ out=np.full_like(projected, np.inf),
1326
+ where=tolerance > 0.)
1327
+ ratios[(tolerance == 0.) & (projected == 0.)] = 0.
1328
+ return float(np.max(ratios))
1329
+
1330
+ def _physical_work_ratio(self, answer, applied):
1331
+ """Check one explicit physical load against homogeneous-constraint work."""
1332
+ if np.any(self._u0 != 0.):
1333
+ return 0.
1334
+ q = answer[:-len(self._cu0)]
1335
+ u = np.asarray(self._T @ q).reshape(-1)
1336
+ with np.errstate(over="ignore", under="ignore", invalid="ignore"):
1337
+ internal = float(u @ (self._K @ u))
1338
+ external = float(u @ applied)
1339
+ if not isfinite(internal) or not isfinite(external):
1340
+ raise _NumericalFailure("panel physical footprint work is not representable")
1341
+ defect = abs(internal-external)
1342
+ # The physical-footprint family admits no points, body-load driver or
1343
+ # nonzero constraint RHS. Keep the registered work absolute allowance.
1344
+ tolerance = 1e-12 + 1e-8*abs(external)
1345
+ return defect/tolerance
1346
+
1347
+ @staticmethod
1348
+ def _structural_physical_singletons(C, T, u0):
1349
+ """Admit only a single product with individually zero offset terms.
1350
+
1351
+ Rounded cancellation in C@T or C@u0 never establishes eligibility.
1352
+ Coupled rows and even cancellation within one reduced column remain
1353
+ on the ordinary factor/guard route.
1354
+ """
1355
+ constraints = C.tocsr(copy=True)
1356
+ transform = T.tocsr(copy=True)
1357
+ constraints.eliminate_zeros()
1358
+ transform.eliminate_zeros()
1359
+ singletons = []
1360
+ for row in range(constraints.shape[0]):
1361
+ start, end = constraints.indptr[row:row+2]
1362
+ physical = constraints.indices[start:end]
1363
+ if np.any(u0[physical] != 0.):
1364
+ continue
1365
+ products = []
1366
+ for index, coefficient in zip(physical, constraints.data[start:end]):
1367
+ first, last = transform.indptr[index:index+2]
1368
+ for column, value in zip(transform.indices[first:last], transform.data[first:last]):
1369
+ products.append((int(column), float(coefficient*value)))
1370
+ if len(products) > 1:
1371
+ break
1372
+ if len(products) > 1:
1373
+ break
1374
+ if len(products) == 1 and np.isfinite(products[0][1]) and products[0][1] != 0.:
1375
+ singletons.append((row, *products[0]))
1376
+ return tuple(singletons)
1377
+
1378
+ def _restore_physical_singletons(self, answer):
1379
+ """Enforce exact zero coordinates only for roundoff-sized residues.
1380
+
1381
+ This changes the candidate, never a compatibility tolerance. Material
1382
+ errors remain for the existing equilibrium/correction/refusal checks.
1383
+ Translation and rotation amplitudes retain their separate units.
1384
+ """
1385
+ if not self._physical_singletons:
1386
+ return answer
1387
+ q = answer[:-len(self._cu0)]
1388
+ u = np.asarray(self._T @ q).reshape(-1)+self._u0
1389
+ poses = np.abs(u).reshape(-1, 6)
1390
+ amplitude = np.tile((float(np.max(poses[:, :3])),)*3+
1391
+ (float(np.max(poses[:, 3:])),)*3, len(poses))
1392
+ bounds = len(answer)*np.finfo(float).eps*np.asarray(
1393
+ self._abs_C @ amplitude).reshape(-1)
1394
+ if not np.isfinite(bounds).all():
1395
+ raise _NumericalFailure("physical singleton substitution is not representable")
1396
+ restored = answer.copy()
1397
+ for row, column, coefficient in self._physical_singletons:
1398
+ if abs(coefficient*q[column]) <= bounds[row]:
1399
+ restored[column] = 0.
1400
+ return restored
1401
+
1402
+ def _physical_original_residual(self, answer, applied):
1403
+ """Original affine/constraint covectors at the actual scored candidate.
1404
+
1405
+ Use the same original K/C/T and ordinary arithmetic as recovery; no
1406
+ rounded reduced/congruence matrix supplies this target. This private
1407
+ residual is used only by the physical footprint reference-static route.
1408
+ """
1409
+ # Constructor admission excludes points and point_trace_policy, so H
1410
+ # and body/fixed-end constraint RHS d are absent on this route. Typed
1411
+ # body loads are refused before the sole solve_linear caller; batches
1412
+ # delegate to that caller. Refuse a corrupted/foreign private owner
1413
+ # rather than silently evaluating Cu=0 for [C,-H] or nonzero d.
1414
+ if (not self._physical_footprint_law or self.point_ids or
1415
+ self._point_trace_policy is not None or
1416
+ (self._H is not None and self._H.nnz != 0)):
1417
+ raise _NumericalFailure("original correction requires the separate physical footprint static family")
1418
+ if (answer.shape != self._factor_scale.shape or
1419
+ applied.shape != self._u0.shape or
1420
+ not np.isfinite(answer).all() or not np.isfinite(applied).all()):
1421
+ raise _NumericalFailure("finite original physical correction inputs required")
1422
+ nq = self._T.shape[1]
1423
+ q, lam = answer[:nq], answer[nq:]
1424
+ u = np.asarray(self._T @ q).reshape(-1)+self._u0
1425
+ reaction = np.asarray(self._K @ u).reshape(-1)+np.asarray(self._C.T @ lam).reshape(-1)-applied
1426
+ target = np.r_[self._project(reaction), np.asarray(self._C @ u).reshape(-1)]
1427
+ if not np.isfinite(u).all() or not np.isfinite(target).all():
1428
+ raise _NumericalFailure("original physical correction target is not representable")
1429
+ return target
1430
+
1431
+ def _correct_physical_equilibrium(self, scaled_answer, scaled_rhs, applied):
1432
+ """One original-operator correction using the existing factorization.
1433
+
1434
+ ``scaled_rhs`` remains in this private calling convention, but cannot
1435
+ define the original target. Restoration precedes both scoring and RHS
1436
+ formation; the correction is added to that same restored candidate.
1437
+ Physical equation/work/compatibility acceptance remains unchanged.
1438
+ """
1439
+ answer = self._restore_physical_singletons(self._factor_scale*scaled_answer)
1440
+ original_ratio = self._physical_equilibrium_ratio(answer, applied)
1441
+ original_work_ratio = self._physical_work_ratio(answer, applied)
1442
+ original_score = max(original_ratio, original_work_ratio)
1443
+ if original_score <= 1.:
1444
+ return answer
1445
+ original_target = self._physical_original_residual(answer, applied)
1446
+ with np.errstate(over="ignore", under="ignore", invalid="ignore"):
1447
+ correction_rhs = -self._factor_scale*original_target
1448
+ if (not np.isfinite(correction_rhs).all() or
1449
+ np.any((original_target != 0.) & (correction_rhs == 0.))):
1450
+ raise _NumericalFailure("panel equilibrium correction is not representable")
1451
+ try:
1452
+ delta = np.asarray(self._handle.solve(correction_rhs), dtype=float)
1453
+ except RuntimeError as exc:
1454
+ raise _NumericalFailure("panel equilibrium correction failed: " + str(exc)) from exc
1455
+ with np.errstate(over="ignore", under="ignore", invalid="ignore"):
1456
+ candidate = answer+self._factor_scale*delta
1457
+ if candidate.shape != answer.shape or not np.isfinite(candidate).all():
1458
+ raise _NumericalFailure("finite panel equilibrium correction required")
1459
+ candidate = self._restore_physical_singletons(candidate)
1460
+ candidate_score = max(self._physical_equilibrium_ratio(candidate, applied),
1461
+ self._physical_work_ratio(candidate, applied))
1462
+ if candidate_score >= original_score:
1463
+ raise _NumericalFailure("panel equilibrium correction did not improve")
1464
+ return candidate
1465
+
1466
+ def _explicit_load_resolved(self, answer, rhs, explicit_rhs, *, applied=None, reconstructed=None) -> bool:
1467
+ """Certify active stored KKT rows without a model-wide force floor.
1468
+
1469
+ Pure unit selectors retain the stored KKT certificate. Zero-offset
1470
+ footprint affine maps certify exact physical virtual equilibrium and
1471
+ the actual reconstruction error from original sparse operators/load.
1472
+ Nonzero offsets and loaded-point affine maps remain ineligible. This
1473
+ resolves net projected loads only, not cancelling individual actions.
1474
+ All existing physical equilibrium/compatibility checks still apply.
1475
+ """
1476
+ if (np.any(self._u0 != 0.) or not np.isfinite(answer).all() or
1477
+ not np.isfinite(rhs).all()):
1478
+ return False
1479
+ selector = self._T.tocsc(copy=True)
1480
+ selector.eliminate_zeros()
1481
+ if (np.any(np.diff(selector.indptr) != 1) or
1482
+ np.any(selector.data != 1.) or
1483
+ len(np.unique(selector.indices)) != selector.shape[1]):
1484
+ if not self.footprints or self._point_trace_policy is not None or applied is None or reconstructed is None:
1485
+ return False
1486
+ if self._affine_resolution_cache is None:self._affine_resolution_cache = {}
1487
+ try:
1488
+ return certify_affine(self._K,self._C,self._T,answer,applied,reconstructed,self._affine_resolution_cache)
1489
+ except B2NumericalSolveFailure:
1490
+ return False
1491
+ active = np.flatnonzero(explicit_rhs > 0.)
1492
+ with np.errstate(over="ignore", under="ignore", invalid="ignore"):
1493
+ cap = np.nextafter(1e-9*explicit_rhs[active], 0.)
1494
+ if not len(active) or np.any(cap <= 0.) or not np.isfinite(cap).all():
1495
+ return False
1496
+ if self._resolution_kkt is None:
1497
+ columns = selector.indices
1498
+ rk = self._K.tocsr()[columns][:, columns]
1499
+ rc = self._C.tocsr()[:, columns]
1500
+ self._resolution_kkt = sparse.bmat(
1501
+ ((rk, rc.T), (rc, None if self._point_trace_policy is None else -self._H)), format="csr")
1502
+ try:
1503
+ _, upper = _bounded_accurate_residual(
1504
+ self._resolution_kkt[active], answer[:, None], rhs[active, None])
1505
+ except B2NumericalSolveFailure:
1506
+ return False
1507
+ return bool(np.isfinite(upper).all() and np.all(upper[:, 0] <= cap))
1508
+
1509
+ def _project(self, values: np.ndarray) -> np.ndarray:
1510
+ return _checked_affine_projection(
1511
+ self._Tt, self._projection_multi, self._projection_single, values)
1512
+
1513
+ def _footprint_gap_tolerance(self, u, gap_scale, roundoff_fraction):
1514
+ """Quiet-channel solve admission, bounded by reference compatibility.
1515
+
1516
+ Called only after explicit-load certification for unit selectors or
1517
+ proved general-affine footprint maps, with zero offsets. This is a
1518
+ solve/compatibility convention, not a bound on
1519
+ rounding in C@u. Translation and rotation amplitudes stay separate;
1520
+ actual row coefficients include eccentric lever arms. Historical point
1521
+ rows retain their original check. Every footprint row also stays below
1522
+ the independent reference-geometry 1e-9 compatibility cap.
1523
+ """
1524
+ tolerance = 1e-9*gap_scale
1525
+ poses = np.abs(u).reshape(-1, 6)
1526
+ amplitude = np.tile((float(np.max(poses[:, :3])),)*3+
1527
+ (float(np.max(poses[:, 3:])),)*3, len(poses))
1528
+ allowance = roundoff_fraction*np.asarray(self._abs_C@amplitude).reshape(-1)
1529
+ if not np.isfinite(allowance).all():
1530
+ raise _NumericalFailure("footprint compatibility allowance is not representable")
1531
+ for item in self._footprint_incidence:
1532
+ width = getattr(item,"row_count",6)
1533
+ rows = slice(item.row_start, item.row_start+width)
1534
+ cap = 1e-9*np.array((self._compatibility_length,)*3+(1.,)*(width-3))
1535
+ tolerance[rows] = np.minimum(tolerance[rows]+allowance[rows], cap)
1536
+ return tolerance
1537
+
1538
+ @property
1539
+ def capabilities(self) -> ExperimentalB2Q4PanelCapabilities:
1540
+ if self._physical_footprint_law:
1541
+ return B2PhysicalRigidFootprintCapabilities(
1542
+ attachment="physical rigid-region translations and director tangents; five rows per selected node",
1543
+ operations=(("solve_linear","solve_linear_many","close") if self._has_s3 else ("solve_linear","solve_linear_many","buckling","close")),
1544
+ spectral_domain=("S3/mixed physical footprint modes and buckling unsupported" if self._has_s3 else "explicit physical-footprint conservative reference buckling request; modes unavailable without mass contract"),
1545
+ footprints=self.footprints)
1546
+ if self._modal_only:
1547
+ return ExperimentalB2Q4PanelCapabilities(
1548
+ operations=("modes", "close"),
1549
+ body_load_domain="unsupported by free-free modal-only policy",
1550
+ spectral_domain="experimental connected free-free reference descriptor; six verified physical rigid fields and selected finite flexible pairs; completeness unqualified")
1551
+ body_operations = (("body_acceleration_load",) if self._body_map is not None else ())
1552
+ body_domain = ("experimental reference-linear uniform global acceleration; typed loads give separate equilibrated section fields including interior point actions; raw vectors retain nodal constitutive recovery; loaded interior pose and finite gravity unsupported"
1553
+ if self._body_map is not None else "unavailable without complete physical density/mass")
1554
+ if self._point_trace_policy is not None:
1555
+ return ExperimentalB2LoadedPointCapabilities(
1556
+ attachment="explicit fixed-end loaded finite-vertex point trace",
1557
+ operations=("solve_linear", "solve_linear_many", "close") + body_operations,
1558
+ body_load_domain=("typed reference body acceleration and multiplier-dependent fixed-end bubbles; "
1559
+ "raw arrays are nodal loads; finite gravity unsupported" if self._body_map is not None
1560
+ else body_domain))
1561
+ if self.footprints:
1562
+ admitted = (self._M is not None and self._coordinate_modal_supports()
1563
+ and not np.any(self._u0 != 0.) and self._footprint_rigid_supported)
1564
+ return ExperimentalB2Q4FootprintCapabilities(
1565
+ attachment="explicit reference-linear selected-node rigid footprints and finite-vertex points",
1566
+ operations=(("solve_linear", "solve_linear_many", "modes", "close")
1567
+ if admitted else ("solve_linear", "solve_linear_many", "close")) + body_operations,
1568
+ body_load_domain=body_domain,
1569
+ spectral_domain=("experimental virgin supported reference modes; normal-spin locked; "
1570
+ "mechanical qualification incomplete; prestress unsupported"
1571
+ if admitted else
1572
+ "reference modes require complete physical mass, coordinate supports detecting all six rigid fields and zero offset; "
1573
+ "normal-spin locked; mechanical qualification incomplete; prestress unsupported"),
1574
+ footprints=self.footprints)
1575
+ if self._M is None:
1576
+ return ExperimentalB2Q4PanelCapabilities(
1577
+ operations=("solve_linear", "solve_linear_many", "close") + body_operations,
1578
+ body_load_domain=body_domain)
1579
+ return ExperimentalB2Q4PanelCapabilities(
1580
+ operations=("solve_linear", "solve_linear_many", "modes", "close") + body_operations,
1581
+ body_load_domain=body_domain,
1582
+ spectral_domain="experimental reference B2/Q4 finite pairs with coordinate supports; completeness unqualified")
1583
+
1584
+ def body_acceleration_load(self, acceleration_m_s2) -> B2ReferenceBodyLoad:
1585
+ """Prepare an owned dead-load vector without assembling or factoring.
1586
+
1587
+ f = integral D.T rho a uses the compiled physical trace and mass origin.
1588
+ Pass ``result.load`` to the existing static or compatible batch route.
1589
+ """
1590
+ if self._closed:
1591
+ raise ExperimentalB2Q4PanelError("experimental panel is closed")
1592
+ if self._modal_only:
1593
+ raise ValueError("free-free modal-only policy does not admit body loads")
1594
+ if self._body_map is None:
1595
+ raise ValueError("complete physical density/mass required for reference body load")
1596
+ raw = np.asarray(acceleration_m_s2, dtype=object)
1597
+ if (raw.shape != (3,) or any(isinstance(v, (bool, np.bool_)) or
1598
+ not isinstance(v, Real) for v in raw.flat)):
1599
+ raise ValueError("three finite real global acceleration components required")
1600
+ try:
1601
+ acceleration = np.array(raw, dtype=float, copy=True)
1602
+ except (ValueError, TypeError, OverflowError) as exc:
1603
+ raise ValueError("three finite real global acceleration components required") from exc
1604
+ if not np.isfinite(acceleration).all():
1605
+ raise ValueError("three finite real global acceleration components required")
1606
+ with np.errstate(over="ignore", invalid="ignore", under="ignore"):
1607
+ applied = self._body_map@acceleration
1608
+ if not np.isfinite(applied).all():
1609
+ raise RepresentabilityError("reference body acceleration load is not representable")
1610
+ # A finite vector can still lose a required force through underflow or
1611
+ # cancellation. Test rigid-translation work against integrated physical
1612
+ # mass without adding an absolute load floor or changing solver criteria.
1613
+ for axis, component in enumerate(acceleration):
1614
+ if component == 0.:
1615
+ continue
1616
+ expected = self._body_mass*float(component)
1617
+ try:
1618
+ actual = fsum(float(v) for v in applied[axis::6])
1619
+ except OverflowError as exc:
1620
+ raise RepresentabilityError("reference body force is not representable") from exc
1621
+ if (not isfinite(expected) or expected == 0. or not isfinite(actual)
1622
+ or abs(actual/expected-1.) > 1e-9):
1623
+ raise RepresentabilityError("required reference body force is not representable")
1624
+ return B2ReferenceBodyLoad(tuple(float(v) for v in acceleration),
1625
+ self.shell_node_ids, self.beam_node_ids, applied,
1626
+ self._body_model_identity)
1627
+
1628
+ def _body_load_snapshot(self, load):
1629
+ if isinstance(load, B2ReferenceBodyLoad):
1630
+ if (load.model_identity != self._body_model_identity or
1631
+ load.shell_node_ids != self.shell_node_ids or load.beam_node_ids != self.beam_node_ids):
1632
+ raise ValueError("body load belongs to a different compiled model")
1633
+ expected = self.body_acceleration_load(load.acceleration_m_s2)
1634
+ if not np.array_equal(load.load, expected.load):
1635
+ raise ValueError("body load values disagree with its physical acceleration")
1636
+ snapshot, fingerprint = _load_value_snapshot(load.load, self._u0.shape)
1637
+ metadata = sha256(fingerprint)
1638
+ metadata.update(load.model_identity.encode("utf-8"))
1639
+ metadata.update(repr((load.shell_node_ids, load.beam_node_ids)).encode("utf-8"))
1640
+ metadata.update(np.asarray(load.acceleration_m_s2, dtype="<f8").tobytes())
1641
+ return snapshot, metadata.digest()
1642
+ return _load_value_snapshot(load, self._u0.shape)
1643
+
1644
+ def _coordinate_modal_supports(self) -> bool:
1645
+ selector = self._T.copy()
1646
+ selector.eliminate_zeros()
1647
+ return not (np.any(np.diff(selector.tocsc().indptr) != 1) or
1648
+ np.any(np.diff(selector.indptr) > 1))
1649
+
1650
+ def _reference_modes_result(self, status, frequencies=(), physical=(),
1651
+ multipliers=(), equations=(), constraints=(),
1652
+ factorization_count=0, solve_count=0,
1653
+ failure_reason=None):
1654
+ args = (status, frequencies, physical, multipliers, equations, constraints,
1655
+ factorization_count, solve_count, failure_reason)
1656
+ if not self.footprints:
1657
+ return B2ReferenceModesResult(*args)
1658
+ components = ("ux", "uy", "uz", "rx", "ry", "rz")
1659
+ provenance = [B2ModalConstraintRow(
1660
+ 6*i+j, component, point_id=point_id,
1661
+ coordinate_frame=("point_master_shell_reference" if j < 3
1662
+ else "point_beam_reference"),
1663
+ reference_frame_columns=self._point_reference_frames[i][0 if j < 3 else 1])
1664
+ for i, point_id in enumerate(self.point_ids)
1665
+ for j, component in enumerate(components)]
1666
+ for item in self._footprint_incidence:
1667
+ provenance.extend(B2ModalConstraintRow(
1668
+ item.row_start+j, component, footprint_id=item.footprint_id,
1669
+ shell_node_id=item.shell_node, beam_node_id=item.beam_node)
1670
+ for j, component in enumerate(components))
1671
+ point_modes = (tuple(mode[:6*len(self.point_ids)] for mode in multipliers)
1672
+ if self.point_ids else ())
1673
+ return B2FootprintReferenceModesResult(
1674
+ status, frequencies, physical, point_modes, equations, constraints,
1675
+ factorization_count, solve_count, failure_reason,
1676
+ footprint_ids=self.footprint_ids,
1677
+ constraint_multiplier_modes=multipliers, constraint_rows=tuple(provenance))
1678
+
1679
+ @property
1680
+ def physical_footprint_buckling_capabilities(self):
1681
+ from ._stiffened_shell_b2_reference_buckling import physical_footprint_capabilities
1682
+ return physical_footprint_capabilities(self)
1683
+
1684
+ @property
1685
+ def reference_buckling_capabilities(self):
1686
+ from ._stiffened_shell_b2_reference_buckling import capabilities
1687
+ return capabilities(self)
1688
+
1689
+ def buckling(self, request, *, cancellation=None):
1690
+ """Explicit experimental reference-preload conservative spectral route."""
1691
+ from ._stiffened_shell_b2_reference_buckling import solve
1692
+ return solve(self, request, cancellation=cancellation)
1693
+
1694
+ def modes(self, request: B2ReferenceModesRequest, *,
1695
+ cancellation: CancellationToken | None = None
1696
+ ) -> B2ReferenceModesResult | B2FreeReferenceModesResult:
1697
+ """Sparse descriptor modes with physical inertia and massless multipliers.
1698
+
1699
+ Reuse compiled K/C/T and mass, never a global nullspace or dense inverse.
1700
+ A shifted general inverse-product is not Euclidean symmetric, so use
1701
+ eigs rather than eigsh. Explicit free-free owners project verified rigid
1702
+ fields in the physical mass metric; loaded/prestressed modes remain out.
1703
+ """
1704
+ if self._physical_footprint_law:
1705
+ raise ValueError("physical footprint reference modes require a separately adopted mass contract")
1706
+ if self._closed:
1707
+ raise ExperimentalB2Q4PanelError("experimental panel is closed")
1708
+ if self._point_trace_policy is not None:
1709
+ raise ValueError("loaded point trace reference modes unsupported: shape inertia unverified")
1710
+ if self._has_s3 and self._reference_inertia_policy is None:
1711
+ raise ValueError("native S3 V2D physical mass and reference modes are deferred")
1712
+ if type(request) is not B2ReferenceModesRequest:
1713
+ raise ValueError("typed reference-mode request required")
1714
+ if self._M is None:
1715
+ raise ValueError("explicit shell density and beam physical mass required")
1716
+ if not self._coordinate_modal_supports():
1717
+ raise ValueError("reference modes currently require coordinate supports without MPCs")
1718
+ if self.footprints and np.any(self._u0 != 0.):
1719
+ raise ValueError("virgin nodal-footprint reference modes require zero prescribed offset")
1720
+ if self.footprints and not self._footprint_rigid_supported:
1721
+ raise ValueError("nodal-footprint reference modes require coordinate supports detecting all six physical rigid fields; free-free and under-supported modes unsupported")
1722
+ if cancellation is not None and not isinstance(cancellation, CancellationToken):
1723
+ raise ValueError("CancellationToken required")
1724
+ n = self._T.shape[1]
1725
+ rows = self._C.shape[0]
1726
+ if request.count >= min(n-rows-(6 if self._modal_only else 0), n+rows-1):
1727
+ raise ValueError("reference mode count exceeds supported sparse spectrum")
1728
+ with np.errstate(over="ignore", under="ignore", invalid="ignore"):
1729
+ sigma = -float(np.square(2.*np.pi*np.float64(request.shift_hz)))
1730
+ if not np.isfinite(sigma) or sigma == 0.:
1731
+ raise ValueError("reference mode shift is not representable")
1732
+
1733
+ def safe_point(stage):
1734
+ if cancellation is not None:
1735
+ cancellation.checkpoint(stage)
1736
+
1737
+ handle = None
1738
+ rigid = None
1739
+ overlap = 0.
1740
+
1741
+ def finish(*args, **kwargs):
1742
+ flexible = self._reference_modes_result(*args, **kwargs)
1743
+ if not self._modal_only:
1744
+ return flexible
1745
+ data = {}
1746
+ if flexible.status == "ok":
1747
+ data = dict(rigid_origin_m=rigid["origin"],
1748
+ canonical_rigid_modes=tuple(tuple(map(float, x)) for x in rigid["canonical"].T),
1749
+ rigid_modes=tuple(tuple(map(float, x)) for x in rigid["normalized"].T),
1750
+ rigid_multiplier_modes=tuple((0.,)*rows for _ in range(6)),
1751
+ rigid_kinetic_gram=tuple(tuple(map(float, x)) for x in rigid["gram"]),
1752
+ rigid_equation_residuals=rigid["equations"],
1753
+ rigid_constraint_residuals=rigid["gaps"],
1754
+ rigid_flexible_mass_overlap=overlap)
1755
+ return B2FreeReferenceModesResult(flexible.status, flexible,
1756
+ spectral_shift_rad2_s2=sigma, failure_reason=flexible.failure_reason, **data)
1757
+
1758
+ try:
1759
+ safe_point("reference modes before factorization")
1760
+ k = (self._T.T@self._K@self._T).tocsc()
1761
+ m = (self._T.T@self._M@self._T).tocsc()
1762
+ c = (self._C@self._T).tocsc()
1763
+ a = sparse.bmat(((k, c.T), (c, None)), format="csc")
1764
+ b = sparse.block_diag((m, sparse.csc_matrix((rows, rows))), format="csc")
1765
+ try:
1766
+ shifted, scale = _scaled_point_kkt(k-sigma*m, c)
1767
+ except ExperimentalB2Q4PanelError as exc:
1768
+ raise _NumericalFailure("reference modal scaling failed: "+str(exc)) from exc
1769
+ scaling = sparse.diags(scale, format="csc")
1770
+ bs = (scaling@b@scaling).tocsc()
1771
+ b.eliminate_zeros(); b.sort_indices()
1772
+ bs.eliminate_zeros(); bs.sort_indices()
1773
+ if (not np.isfinite(bs.data).all() or b.nnz != bs.nnz or
1774
+ not np.array_equal(b.indptr, bs.indptr) or
1775
+ not np.array_equal(b.indices, bs.indices) or
1776
+ np.any((b.data != 0.) & (bs.data == 0.))):
1777
+ raise _NumericalFailure("reference modal mass scaling is not representable")
1778
+ if self._modal_only:
1779
+ rigid = _free_b2_rigid_space(self, scale, bs, safe_point)
1780
+
1781
+ def project(vector):
1782
+ if not self._modal_only:
1783
+ return vector
1784
+ return vector-rigid["scaled"]@(rigid["scaled"].T@(bs@vector))
1785
+
1786
+ handle = factorize_cached(shifted, MatrixClass.GENERAL)
1787
+ safe_point("reference modes after factorization")
1788
+ if handle.status != "ok":
1789
+ raise _NumericalFailure("reference modal factorization failed: "+str(handle.failure_reason))
1790
+
1791
+ def action(vector):
1792
+ safe_point("reference modal iteration")
1793
+ try:
1794
+ answer = np.asarray(handle.solve(bs@project(vector)), dtype=float)
1795
+ except RuntimeError as exc:
1796
+ # These are explicit numerical failures of the shared
1797
+ # backend. Unclassified/programming errors still propagate.
1798
+ if (str(exc) == "Sparse solve produced NaN/Inf values" or
1799
+ str(exc).startswith("Cannot solve with failed factorization:")):
1800
+ raise _NumericalFailure("reference modal sparse solve failed: "+str(exc)) from exc
1801
+ raise
1802
+ if not np.isfinite(answer).all():
1803
+ raise _NumericalFailure("finite reference modal inverse action required")
1804
+ safe_point("reference modal iteration completed")
1805
+ return project(answer)
1806
+
1807
+ operator = LinearOperator(a.shape, matvec=action, dtype=float)
1808
+ values, vectors = eigs(operator, k=request.count, which="LM",
1809
+ v0=np.sin(np.arange(a.shape[0])+.23),
1810
+ tol=1e-11, maxiter=request.max_iterations)
1811
+ safe_point("reference modes after eigensolve")
1812
+ abs_a, abs_b, abs_c = abs(a), abs(b), abs(c)
1813
+ recovered = []
1814
+ for mu, y in zip(values, vectors.T):
1815
+ if (not np.isfinite(mu) or abs(mu.imag) > 1e-9*abs(mu.real) or
1816
+ mu.real <= 0. or not np.isfinite(y).all() or
1817
+ np.linalg.norm(y.imag) > 1e-9*np.linalg.norm(y.real)):
1818
+ raise _NumericalFailure("nonphysical or unresolved finite modal pair")
1819
+ lam = sigma+1./mu.real
1820
+ if self._modal_only:
1821
+ # Resolution screen, not a certified eigenvalue error bound.
1822
+ screen = 64*np.finfo(float).eps*a.shape[0]*(abs(sigma)+abs(1./mu.real))
1823
+ if not np.isfinite(screen) or lam <= screen:
1824
+ raise _NumericalFailure("extra mechanism or unresolved near-zero flexible pair")
1825
+ x = scale*y.real
1826
+ mass_norm = float(x@b@x)
1827
+ if not np.isfinite(lam) or lam <= 0. or not np.isfinite(mass_norm) or mass_norm <= 0.:
1828
+ raise _NumericalFailure("positive finite supported mode and physical inertia required")
1829
+ x /= np.sqrt(mass_norm)
1830
+ physical = np.asarray(self._T@x[:n])
1831
+ if not np.isfinite(x).all() or not np.isfinite(physical).all():
1832
+ raise _NumericalFailure("physical mode normalization is not representable")
1833
+ residual = a@x-lam*(b@x)
1834
+ bound = abs_a@abs(x)+abs(lam)*(abs_b@abs(x))
1835
+ equation = _modal_scaled_residual(residual, bound, scale)
1836
+ constraint = _modal_scaled_residual(c@x[:n], abs_c@abs(x[:n]), scale[n:])
1837
+ physical_gap = _modal_scaled_residual(
1838
+ self._C@physical, abs(self._C)@abs(physical), scale[n:])
1839
+ physical_balance = self._K@physical+self._C.T@x[n:]-lam*(self._M@physical)
1840
+ physical_bound = (abs(self._K)@abs(physical)+abs(self._C.T)@abs(x[n:])+
1841
+ abs(lam)*(abs(self._M)@abs(physical)))
1842
+ physical_equation = _modal_scaled_residual(
1843
+ self._project(physical_balance), abs(self._T.T)@physical_bound, scale[:n])
1844
+ equation = max(equation, physical_equation)
1845
+ constraint = max(constraint, physical_gap)
1846
+ if equation > 1e-8 or constraint > 1e-9:
1847
+ raise _NumericalFailure("reference modal equation or constraint residual failed")
1848
+ recovered.append((float(np.sqrt(lam)/(2.*np.pi)),
1849
+ tuple(physical), tuple(x[n:]),
1850
+ equation, constraint))
1851
+ recovered.sort(key=lambda mode: mode[0])
1852
+ primal = np.array([mode[1] for mode in recovered]).T
1853
+ modal_gram = primal.T@self._M@primal
1854
+ if (not np.isfinite(modal_gram).all() or
1855
+ np.max(abs(modal_gram-np.eye(request.count))) > 1e-7):
1856
+ raise _NumericalFailure("modal physical-mass orthogonality failed")
1857
+ if self._modal_only:
1858
+ overlap = float(np.max(abs(rigid["normalized"].T@self._M@primal)))
1859
+ if not np.isfinite(overlap) or overlap > 1e-7:
1860
+ raise _NumericalFailure("rigid/flexible physical-mass overlap failed")
1861
+ safe_point("reference modes before publication")
1862
+ return finish(
1863
+ "ok", tuple(mode[0] for mode in recovered),
1864
+ tuple(mode[1] for mode in recovered), tuple(mode[2] for mode in recovered),
1865
+ tuple(mode[3] for mode in recovered), tuple(mode[4] for mode in recovered),
1866
+ handle.factorization_count, handle.solve_count)
1867
+ except SolveCancelled as exc:
1868
+ return finish(
1869
+ "cancelled", factorization_count=handle.factorization_count if handle else 0,
1870
+ solve_count=handle.solve_count if handle else 0, failure_reason=str(exc))
1871
+ except (ArpackError, _NumericalFailure) as exc:
1872
+ return finish(
1873
+ "failed", factorization_count=handle.factorization_count if handle else 0,
1874
+ solve_count=handle.solve_count if handle else 0, failure_reason=str(exc))
1875
+ finally:
1876
+ # Do not retain a numerical factor through an exception traceback.
1877
+ handle = None
1878
+
1879
+ def _panel_result(self, *args, **kwargs):
1880
+ if self._physical_footprint_law:
1881
+ return B2PhysicalRigidFootprintResult(*args,footprint_ids=self.footprint_ids,**kwargs)
1882
+ if self._point_trace_policy is not None:
1883
+ return B2LoadedPointPanelResult(*args, **kwargs)
1884
+ if "body_model_identity" in kwargs:
1885
+ result_type = B2BodyLoadedFootprintResult if self.footprints else B2BodyLoadedPanelResult
1886
+ if self.footprints:
1887
+ kwargs["footprint_ids"] = self.footprint_ids
1888
+ return result_type(*args, **kwargs)
1889
+ if self.footprints:
1890
+ return ExperimentalB2Q4FootprintResult(
1891
+ *args, footprint_ids=self.footprint_ids, **kwargs)
1892
+ return ExperimentalB2Q4PanelResult(*args, **kwargs)
1893
+
1894
+ def solve_linear(self, load, *, recover_members=None,
1895
+ selected_footprints: B2NodalRigidFootprintSelection | B2PhysicalRigidFootprintSelection | None = None
1896
+ ) -> ExperimentalB2Q4PanelResult:
1897
+ if self._closed:
1898
+ raise ExperimentalB2Q4PanelError("experimental panel is closed")
1899
+ if self._modal_only:
1900
+ raise ValueError("free-free modal-only policy does not admit static solves")
1901
+ selector = checked_physical_selection if self._physical_footprint_law else checked_selection
1902
+ chosen_footprints = selector(selected_footprints, self.footprints)
1903
+ if self._physical_footprint_law and isinstance(load,B2ReferenceBodyLoad):
1904
+ raise ValueError("physical footprint body-load driver not admitted")
1905
+ recovery = _checked_recovery(
1906
+ recover_members, {member_id for member_id, _ in self._members})
1907
+ try:
1908
+ applied, _ = self._body_load_snapshot(load)
1909
+ except (TypeError, ValueError, OverflowError) as exc:
1910
+ if isinstance(load, B2ReferenceBodyLoad):
1911
+ raise
1912
+ raise ValueError("finite panel load vector required") from exc
1913
+ if applied.shape != self._u0.shape or not np.isfinite(applied).all():
1914
+ raise ValueError("finite panel load vector required")
1915
+ acceleration = load.acceleration_m_s2 if isinstance(load, B2ReferenceBodyLoad) else None
1916
+ body_gap = body_gap_scale = None
1917
+ try:
1918
+ rhs = np.r_[self._project(applied-self._ku0), -self._cu0]
1919
+ if self._point_trace_policy is not None:
1920
+ body_gap = np.zeros(len(self._cu0))
1921
+ body_gap_scale = np.zeros(len(self._cu0))
1922
+ for compiled_load in self._loaded_members.values():
1923
+ values, scales = compiled_load.body_constraint(acceleration)
1924
+ slots = [6*index+channel for index in compiled_load.point_indices for channel in range(6)]
1925
+ body_gap[slots] = values
1926
+ body_gap_scale[slots] = scales
1927
+ rhs[-len(self._cu0):] -= body_gap
1928
+ except (_NumericalFailure, RepresentabilityError) as exc:
1929
+ return self._panel_result(
1930
+ "failed", factorization_count=self._handle.factorization_count,
1931
+ solve_count=self._handle.solve_count, failure_reason=str(exc))
1932
+ with np.errstate(over="ignore", under="ignore", invalid="ignore"):
1933
+ scaled_rhs = self._factor_scale*rhs
1934
+ if (not np.isfinite(scaled_rhs).all() or
1935
+ np.any((rhs != 0.) & (scaled_rhs == 0.))):
1936
+ return self._panel_result(
1937
+ "failed", factorization_count=self._handle.factorization_count,
1938
+ solve_count=self._handle.solve_count,
1939
+ failure_reason="panel right-hand side scaling is not representable")
1940
+ try:
1941
+ scaled_answer = np.asarray(self._handle.solve(scaled_rhs), dtype=float)
1942
+ answer = self._factor_scale*scaled_answer
1943
+ if self._physical_footprint_law and answer.shape == rhs.shape and np.isfinite(answer).all():
1944
+ answer = self._correct_physical_equilibrium(
1945
+ scaled_answer, scaled_rhs, applied)
1946
+ if self._physical_work_ratio(answer, applied) > 1.:
1947
+ raise _NumericalFailure("panel physical footprint work consistency failed")
1948
+ except (RuntimeError, _NumericalFailure) as exc:
1949
+ return self._panel_result(
1950
+ "failed", factorization_count=self._handle.factorization_count,
1951
+ solve_count=self._handle.solve_count, failure_reason=str(exc))
1952
+ try:
1953
+ if answer.shape != rhs.shape or not np.isfinite(answer).all():
1954
+ raise _NumericalFailure("finite panel solution required")
1955
+ q, lam = answer[:-len(self._cu0)], answer[-len(self._cu0):]
1956
+ u = np.asarray(self._T @ q).reshape(-1)+self._u0
1957
+ if not np.isfinite(u).all():
1958
+ raise _NumericalFailure("finite panel solution required")
1959
+ gap = np.asarray(self._C @ u).reshape(-1)
1960
+ compliance_gap_scale = None
1961
+ if self._point_trace_policy is not None:
1962
+ compliance_gap, compliance_gap_scale = checked_sparse_action(self._H, lam)
1963
+ gap = gap+body_gap-compliance_gap
1964
+ force = np.asarray(self._K @ u).reshape(-1)
1965
+ constraint_force = np.asarray(self._C.T @ lam).reshape(-1)
1966
+ reaction = force+constraint_force-applied
1967
+ projected = self._project(reaction)
1968
+ # Each row uses the absolute contribution sum in its own physical
1969
+ # work unit. A 1 N floor would mask a failed micro-/nano-load;
1970
+ # a single mixed force/moment norm would depend on chosen units.
1971
+ gap_scale = np.asarray(self._abs_C @ np.abs(u)).reshape(-1)
1972
+ if self._point_trace_policy is not None:
1973
+ gap_scale += body_gap_scale+compliance_gap_scale
1974
+ physical_force_scale = np.asarray(
1975
+ self._abs_K @ np.abs(u)+self._abs_Ct @ np.abs(lam)+
1976
+ np.abs(applied)).reshape(-1)
1977
+ equilibrium_scale = np.asarray(
1978
+ self._abs_Tt @ physical_force_scale).reshape(-1)
1979
+ # Sparse factorization can leave roundoff in a locally quiet DOF.
1980
+ # Its allowance follows the current force and moment magnitudes,
1981
+ # separately, then maps through the affine transpose; it has no
1982
+ # fixed N, Nm, m or rad floor.
1983
+ by_node = physical_force_scale.reshape(-1, 6)
1984
+ force_roundoff = float(np.max(by_node[:, :3]))
1985
+ moment_roundoff = float(np.max(by_node[:, 3:]))
1986
+ physical_roundoff = np.tile(
1987
+ (force_roundoff,)*3+(moment_roundoff,)*3,
1988
+ len(by_node))
1989
+ projected_roundoff = np.asarray(
1990
+ self._abs_Tt @ physical_roundoff).reshape(-1)
1991
+ roundoff_fraction = len(rhs)*np.finfo(float).eps
1992
+ if roundoff_fraction >= 1e-9:
1993
+ raise _NumericalFailure("panel equilibrium resolution insufficient")
1994
+ explicit_rhs = np.abs(self._project(applied))
1995
+ resolution_floor = roundoff_fraction*projected_roundoff
1996
+ # Prescribed offsets can induce reduced loads much smaller than
1997
+ # the model-wide reaction scale. Their physical effect is checked
1998
+ # by the full equilibrium/compatibility test below. This extra
1999
+ # small-load resolution guard applies to explicit applied work,
2000
+ # so a superposed tiny external load cannot disappear silently.
2001
+ active = explicit_rhs > 0.
2002
+ certified = False
2003
+ if np.any(resolution_floor[active] >
2004
+ 1e-9*explicit_rhs[active]):
2005
+ certified = (bool(self.footprints) or self._point_trace_policy is not None) and self._explicit_load_resolved(
2006
+ answer, rhs, explicit_rhs, applied=applied, reconstructed=u)
2007
+ if not certified:
2008
+ raise _NumericalFailure("panel mixed-load resolution insufficient")
2009
+ equilibrium_tolerance = (1e-9*equilibrium_scale+
2010
+ roundoff_fraction*projected_roundoff)
2011
+ gap_tolerance = 1e-9*gap_scale
2012
+ if (self.footprints and np.any(np.abs(gap) > gap_tolerance) and
2013
+ (certified or self._explicit_load_resolved(answer, rhs, explicit_rhs, applied=applied, reconstructed=u))):
2014
+ gap_tolerance = self._footprint_gap_tolerance(u, gap_scale, roundoff_fraction)
2015
+ for item in self._footprint_incidence:
2016
+ width=getattr(item,"row_count",6)
2017
+ rows = slice(item.row_start, item.row_start+width)
2018
+ cap = 1e-9*np.array((self._compatibility_length,)*3+(1.,)*(width-3))
2019
+ gap_tolerance[rows] = np.minimum(gap_tolerance[rows], cap)
2020
+ if (not np.isfinite(reaction).all() or
2021
+ not np.isfinite(gap).all() or not np.isfinite(projected).all() or
2022
+ not np.isfinite(gap_scale).all() or
2023
+ not np.isfinite(equilibrium_tolerance).all() or
2024
+ not np.isfinite(gap_tolerance).all() or
2025
+ np.any(np.abs(gap) > gap_tolerance) or
2026
+ np.any(np.abs(projected) > equilibrium_tolerance)):
2027
+ raise _NumericalFailure("panel point gap or equilibrium failed")
2028
+ except (_NumericalFailure, RepresentabilityError) as exc:
2029
+ return self._panel_result(
2030
+ "failed", factorization_count=self._handle.factorization_count,
2031
+ solve_count=self._handle.solve_count, failure_reason=str(exc))
2032
+ recovered = []
2033
+ equilibrated = []
2034
+ loaded_poses = []
2035
+ loaded_states = {}
2036
+ if self._point_trace_policy is not None:
2037
+ try:
2038
+ loaded_states = {member_id: compiled_load.bubble_state(acceleration, lam)
2039
+ for member_id, compiled_load in self._loaded_members.items()}
2040
+ except RepresentabilityError as exc:
2041
+ return self._panel_result("failed", factorization_count=self._handle.factorization_count,
2042
+ solve_count=self._handle.solve_count, failure_reason=str(exc))
2043
+ for member_id, (member, slots) in self._members:
2044
+ if recovery is not None and member_id not in recovery:
2045
+ continue
2046
+ local = member.local_dof_map() @ u[slots]
2047
+ if not np.isfinite(local).all():
2048
+ return self._panel_result(
2049
+ "failed", factorization_count=self._handle.factorization_count,
2050
+ solve_count=self._handle.solve_count,
2051
+ failure_reason="finite physical B2 recovery coordinates required")
2052
+ result = member.operator.recover_sections(
2053
+ local, stations=(0., .5, 1.) if recovery is None
2054
+ else recovery[member_id])
2055
+ if (not np.isfinite(result.resultants).all() or
2056
+ not np.isfinite(result.strains).all()):
2057
+ return self._panel_result(
2058
+ "failed", factorization_count=self._handle.factorization_count,
2059
+ solve_count=self._handle.solve_count,
2060
+ failure_reason="finite physical B2 recovery required")
2061
+ recovered.append((member_id, tuple(
2062
+ tuple(map(float, row)) for row in result.resultants)))
2063
+ if isinstance(load, B2ReferenceBodyLoad) or self._point_trace_policy is not None:
2064
+ try:
2065
+ if self._point_trace_policy is not None:
2066
+ compiled_load = self._loaded_members[member_id]
2067
+ uniform, point_wrenches, _ = loaded_states[member_id]
2068
+ actions = compiled_load.section_actions(point_wrenches)
2069
+ poses = compiled_load.recover_pose(local, result.stations, uniform, point_wrenches)
2070
+ loaded_poses.append(B2LoadedMemberPoses(member_id, tuple(map(float, result.stations)),
2071
+ tuple(tuple(map(float, row)) for row in poses)))
2072
+ else:
2073
+ with np.errstate(over="ignore", invalid="ignore"):
2074
+ uniform = (member.definition.mass.matrix_at_reference_axis()[:, :3]@
2075
+ member.definition.local_frame_rows()@np.asarray(load.acceleration_m_s2))
2076
+ actions = tuple((station, pullback@lam[6*i:6*i+6]) for i, station, pullback in
2077
+ self._section_point_actions.get(member_id, ()))
2078
+ if (not np.isfinite(uniform).all() or
2079
+ any(not np.isfinite(wrench).all() for _, wrench in actions)):
2080
+ raise RepresentabilityError("physical section actions are not representable")
2081
+ fields = member.operator.recover_equilibrated_sections(local,
2082
+ uniform_wrench=uniform, point_actions=actions, stations=result.stations)
2083
+ except RepresentabilityError as exc:
2084
+ return self._panel_result("failed", factorization_count=self._handle.factorization_count,
2085
+ solve_count=self._handle.solve_count, failure_reason=str(exc))
2086
+ rows = lambda values: tuple(tuple(map(float, row)) for row in values)
2087
+ equilibrated.append(B2LoadedMemberSections(member_id, tuple(map(float, fields.stations)),
2088
+ rows(fields.resultants_left), rows(fields.resultants_right),
2089
+ rows(fields.strains_left), rows(fields.strains_right),
2090
+ tuple(map(float, fields.endpoint_tractions))))
2091
+ energy = .5*float(u @ force)
2092
+ if not isfinite(energy):
2093
+ return self._panel_result(
2094
+ "failed", factorization_count=self._handle.factorization_count,
2095
+ solve_count=self._handle.solve_count,
2096
+ failure_reason="finite panel strain energy required")
2097
+ extra = {}
2098
+ if self._physical_footprint_law:
2099
+ extra.update(constraint_multipliers=tuple(map(float,lam)),constraint_gaps=tuple(map(float,gap)),
2100
+ constraint_rows=tuple((item.row_start+j,item.footprint_id,item.shell_node,item.beam_node,component)
2101
+ for item in self._footprint_incidence for j,component in enumerate(("translation_x","translation_y","translation_z","director_1","director_2"))))
2102
+ if isinstance(load, B2ReferenceBodyLoad):
2103
+ extra.update(body_model_identity=load.model_identity,
2104
+ body_acceleration_m_s2=load.acceleration_m_s2,
2105
+ equilibrated_sections=tuple(equilibrated))
2106
+ if self._point_trace_policy is not None:
2107
+ try:
2108
+ loaded_energy = fsum((energy, *(state[2] for state in loaded_states.values())))
2109
+ except OverflowError:
2110
+ loaded_energy = float("inf")
2111
+ if not isfinite(loaded_energy) or loaded_energy < 0.:
2112
+ return self._panel_result("failed", factorization_count=self._handle.factorization_count,
2113
+ solve_count=self._handle.solve_count, failure_reason="loaded strain energy is not representable")
2114
+ extra.update(equilibrated_sections=tuple(equilibrated), loaded_member_poses=tuple(loaded_poses),
2115
+ loaded_strain_energy=loaded_energy)
2116
+ if chosen_footprints:
2117
+ try:
2118
+ wrench_recovery = physical_selected_wrenches if self._physical_footprint_law else selected_wrenches
2119
+ extra["footprint_wrenches"] = wrench_recovery(
2120
+ self._footprint_incidence, chosen_footprints, lam, gap)
2121
+ except (ValueError, OverflowError) as exc:
2122
+ return self._panel_result(
2123
+ "failed", factorization_count=self._handle.factorization_count,
2124
+ solve_count=self._handle.solve_count, failure_reason=str(exc))
2125
+ return self._panel_result(
2126
+ "completed", tuple(map(float, u)),
2127
+ tuple((point_id, tuple(map(float, lam[6*i:6*i+6])))
2128
+ for i, point_id in enumerate(self.point_ids)),
2129
+ tuple(map(float, reaction)),
2130
+ tuple((point_id, tuple(map(float, gap[6*i:6*i+6])))
2131
+ for i, point_id in enumerate(self.point_ids)),
2132
+ tuple(recovered), energy, self._handle.factorization_count,
2133
+ self._handle.solve_count, **extra)
2134
+
2135
+ def solve_linear_many(
2136
+ self, cases: Sequence[tuple[str, object]], *,
2137
+ selected_dofs: Sequence[int],
2138
+ selected_points: Sequence[str] = (),
2139
+ selected_footprints: B2NodalRigidFootprintSelection | B2PhysicalRigidFootprintSelection | None = None,
2140
+ recover_members: Mapping[str, Sequence[float]] | None = None,
2141
+ cancellation_token: CancellationToken | None = None,
2142
+ on_case: Callable[[str], None] | None = None,
2143
+ ) -> ExperimentalB2Q4PanelBatchResult:
2144
+ """Solve ordered cases with one factorization and bounded case storage.
2145
+
2146
+ Each result is complete before it joins the returned prefix. Numerical
2147
+ failure, cooperative cancellation or owner closure stops further cases.
2148
+ Closing from a completion callback preserves the complete prefix; a
2149
+ final-case closure still returns completed. Callback and programming
2150
+ errors propagate. This development route uses one RHS at a
2151
+ time, avoiding full displacement/reaction batches for large panels.
2152
+ Selected displacement, reaction and local member-resultant envelopes
2153
+ describe only complete cases in the returned prefix, with the first
2154
+ submitted case governing ties.
2155
+ """
2156
+ if self._closed:
2157
+ raise ExperimentalB2Q4PanelError("experimental panel is closed")
2158
+ if self._modal_only:
2159
+ raise ValueError("free-free modal-only policy does not admit static solves")
2160
+ selector = checked_physical_selection if self._physical_footprint_law else checked_selection
2161
+ chosen_footprints = selector(selected_footprints, self.footprints)
2162
+ try:
2163
+ chosen = tuple(selected_dofs)
2164
+ chosen_points = tuple(selected_points)
2165
+ submitted = tuple(cases)
2166
+ except TypeError as exc:
2167
+ raise ValueError("ordered B2 panel cases and selection required") from exc
2168
+ if (not chosen or any(type(value) is not int or
2169
+ not 0 <= value < len(self._u0)
2170
+ for value in chosen) or
2171
+ len(set(chosen)) != len(chosen)):
2172
+ raise ValueError("unique selected panel coordinate indices required")
2173
+ if (any(type(point_id) is not str or
2174
+ point_id not in self.point_ids for point_id in chosen_points) or
2175
+ len(set(chosen_points)) != len(chosen_points)):
2176
+ raise ValueError("unique selected panel point IDs required")
2177
+ if (not submitted or
2178
+ any(type(item) not in (tuple, list) or len(item) != 2 or
2179
+ type(item[0]) is not str or not item[0].strip()
2180
+ for item in submitted) or
2181
+ len({item[0] for item in submitted}) != len(submitted)):
2182
+ raise ValueError("unique nonempty ordered B2 panel case IDs required")
2183
+ # Caller-owned list pairs may be changed by a completion callback.
2184
+ # Snapshot their inventory/references now; later load-value mutation
2185
+ # remains checked by the existing bounded fingerprints below.
2186
+ submitted = tuple((case_id, load) for case_id, load in submitted)
2187
+ if self._physical_footprint_law and any(isinstance(load,B2ReferenceBodyLoad) for _,load in submitted):
2188
+ raise ValueError("physical footprint body-load driver not admitted")
2189
+ selected_rows = tuple((item.row_start+j,item.footprint_id,item.shell_node,item.beam_node,component)
2190
+ for item in self._footprint_incidence if item.footprint_id in chosen_footprints
2191
+ for j,component in enumerate(("translation_x","translation_y","translation_z","director_1","director_2"))) if self._physical_footprint_law else ()
2192
+ if cancellation_token is not None and not isinstance(
2193
+ cancellation_token, CancellationToken):
2194
+ raise TypeError("B2 panel cancellation token required")
2195
+ if on_case is not None and not callable(on_case):
2196
+ raise TypeError("B2 panel case callback required")
2197
+ recovery = _checked_recovery(
2198
+ {} if recover_members is None else recover_members,
2199
+ {member_id for member_id, _ in self._members})
2200
+ # Counts outlive a callback closing the owner, without retaining its
2201
+ # factor in this operation or a propagated callback-error traceback.
2202
+ factorization_count = self._handle.factorization_count
2203
+ solve_count = self._handle.solve_count
2204
+ fingerprints = []
2205
+ for _case_id, load in submitted:
2206
+ if cancellation_token is not None and cancellation_token.is_cancelled:
2207
+ break
2208
+ snapshot, fingerprint = self._body_load_snapshot(load)
2209
+ fingerprints.append(fingerprint)
2210
+ del snapshot
2211
+ completed = []
2212
+
2213
+ def envelopes(attribute):
2214
+ if not completed:
2215
+ return ()
2216
+ extrema = []
2217
+ for index, dof in enumerate(chosen):
2218
+ # min/max preserve the first case on exact ties. No rounding,
2219
+ # absolute envelope or physical-unit conversion is introduced.
2220
+ minimum = min(completed, key=lambda case: getattr(case, attribute)[index])
2221
+ maximum = max(completed, key=lambda case: getattr(case, attribute)[index])
2222
+ extrema.append(ExperimentalB2Q4PanelResponseEnvelope(
2223
+ dof, getattr(minimum, attribute)[index], minimum.case_id,
2224
+ getattr(maximum, attribute)[index], maximum.case_id))
2225
+ return tuple(extrema)
2226
+
2227
+ def member_envelopes():
2228
+ if not completed or not recovery:
2229
+ return ()
2230
+ sections = tuple((case.case_id, dict(case.beam_resultants_local))
2231
+ for case in completed)
2232
+ extrema = []
2233
+ for member_id, stations in recovery.items():
2234
+ if any(member_id not in rows or len(rows[member_id]) != len(stations)
2235
+ for _, rows in sections):
2236
+ raise RuntimeError("incomplete selected B2 member recovery")
2237
+ for station_index, station in enumerate(stations):
2238
+ for component_index, component in enumerate(
2239
+ ("N", "Qy", "Qz", "Mx", "My", "Mz")):
2240
+ values = tuple((case_id, rows[member_id][station_index][component_index])
2241
+ for case_id, rows in sections)
2242
+ minimum = min(values, key=lambda item: item[1])
2243
+ maximum = max(values, key=lambda item: item[1])
2244
+ extrema.append(ExperimentalB2Q4PanelMemberResultantEnvelope(
2245
+ member_id, station_index, station, component,
2246
+ minimum[1], minimum[0], maximum[1], maximum[0]))
2247
+ return tuple(extrema)
2248
+
2249
+ def finish(status, failure_reason=None):
2250
+ result_type = (ExperimentalB2Q4FootprintBatchResult if self.footprints
2251
+ else ExperimentalB2Q4PanelBatchResult)
2252
+ extra = {"selected_footprints": chosen_footprints} if self.footprints else {}
2253
+ if self._physical_footprint_law:
2254
+ result_type = B2PhysicalRigidFootprintBatchResult
2255
+ extra["constraint_rows"] = selected_rows
2256
+ return result_type(
2257
+ status, chosen, chosen_points, tuple(completed),
2258
+ factorization_count, solve_count,
2259
+ failure_reason=failure_reason,
2260
+ displacement_envelopes=envelopes("selected_displacements"),
2261
+ affine_reaction_envelopes=envelopes("selected_affine_reactions"),
2262
+ member_resultant_envelopes=member_envelopes(), **extra)
2263
+
2264
+ for index, (case_id, load) in enumerate(submitted):
2265
+ if self._closed or (cancellation_token is not None and
2266
+ cancellation_token.is_cancelled):
2267
+ break
2268
+ snapshot, fingerprint = self._body_load_snapshot(load)
2269
+ if fingerprint != fingerprints[index]:
2270
+ raise ValueError("B2 panel load case changed after submission")
2271
+ result = self.solve_linear(load if isinstance(load, B2ReferenceBodyLoad) else snapshot, recover_members=recovery,
2272
+ selected_footprints=selected_footprints)
2273
+ factorization_count = result.factorization_count
2274
+ solve_count = result.solve_count
2275
+ if result.status != "completed":
2276
+ return finish("failed", f"{case_id}: {result.failure_reason}")
2277
+ point_multipliers = dict(result.point_multipliers)
2278
+ point_gaps = dict(result.point_gaps)
2279
+ case_type = (ExperimentalB2Q4FootprintSelectedCase if self.footprints
2280
+ else ExperimentalB2Q4PanelSelectedCase)
2281
+ extra = {"footprint_wrenches": result.footprint_wrenches} if self.footprints else {}
2282
+ if self._physical_footprint_law:
2283
+ case_type = B2PhysicalRigidFootprintSelectedCase
2284
+ row_indices = tuple(row[0] for row in selected_rows)
2285
+ extra.update(constraint_multipliers=tuple(result.constraint_multipliers[i] for i in row_indices),
2286
+ constraint_gaps=tuple(result.constraint_gaps[i] for i in row_indices))
2287
+ if isinstance(load, B2ReferenceBodyLoad):
2288
+ case_type = B2BodyLoadedFootprintSelectedCase if self.footprints else B2BodyLoadedPanelSelectedCase
2289
+ extra.update(body_model_identity=result.body_model_identity,
2290
+ body_acceleration_m_s2=result.body_acceleration_m_s2,
2291
+ equilibrated_sections=result.equilibrated_sections)
2292
+ if self._point_trace_policy is not None:
2293
+ case_type = B2LoadedPointPanelSelectedCase
2294
+ extra.update(equilibrated_sections=result.equilibrated_sections,
2295
+ loaded_member_poses=result.loaded_member_poses,
2296
+ loaded_strain_energy=result.loaded_strain_energy)
2297
+ completed.append(case_type(
2298
+ case_id,
2299
+ tuple(result.displacements[index] for index in chosen),
2300
+ tuple(result.affine_reactions[index] for index in chosen),
2301
+ tuple((point_id, point_multipliers[point_id])
2302
+ for point_id in chosen_points),
2303
+ tuple((point_id, point_gaps[point_id])
2304
+ for point_id in chosen_points),
2305
+ result.beam_resultants_local,
2306
+ result.internal_strain_energy, **extra))
2307
+ if on_case is not None:
2308
+ on_case(case_id)
2309
+ return finish(
2310
+ "cancelled" if len(completed) != len(submitted) else "completed",
2311
+ failure_reason=("experimental panel closed" +
2312
+ (" after " + completed[-1].case_id if completed else "")
2313
+ if self._closed and len(completed) != len(submitted)
2314
+ else None))
2315
+
2316
+ def close(self) -> None:
2317
+ self._closed = True
2318
+ try:
2319
+ _retire_private_q4_mesh(self)
2320
+ finally:
2321
+ self._handle = None
2322
+ self._physical_scaled_kkt = None
2323
+ self._attachment_elimination = None
2324
+ if self._point_trace_policy is not None:
2325
+ for compiled_load in self._loaded_members.values():
2326
+ compiled_load._recovery_maps.clear()
2327
+ self._loaded_members.clear()
2328
+ self._H = self._abs_H = self._resolution_kkt = None
2329
+ self._resolution_kkt = None
2330
+ self._affine_resolution_cache = None
2331
+ self._buckling_model = None
2332
+ self._buckling_shells = ()
2333
+
2334
+
2335
+ def _release_coo_scratch(rows, columns, values, diagnostic, phase):
2336
+ # CSR owns its arrays; these compiler lists have no later mechanics use.
2337
+ counts = tuple(map(len, (rows, columns, values))) if diagnostic is not None else None
2338
+ rows.clear()
2339
+ columns.clear()
2340
+ values.clear()
2341
+ if diagnostic is not None:
2342
+ diagnostic.record_coo_release(phase, counts)
2343
+
2344
+
2345
+ def _scatter(rows, columns, values, slots, local):
2346
+ width = len(slots)
2347
+ rows.extend(np.repeat(slots, width))
2348
+ columns.extend(np.tile(slots, width))
2349
+ values.extend(np.asarray(local).ravel())
2350
+
2351
+
2352
+ def _modal_scaled_residual(residual, bound, row_scale):
2353
+ """Infinity backward error in the fixed congruence row representation.
2354
+
2355
+ Near-zero components of an uncoupled mode are not separate relative-error
2356
+ denominators. Scaling the rows before the norm keeps their units consistent
2357
+ with the factorization coordinates without an arbitrary physical floor.
2358
+ """
2359
+ residual, bound = np.asarray(residual), np.asarray(bound)
2360
+ if not np.isfinite(residual).all() or not np.isfinite(bound).all():
2361
+ return float("inf")
2362
+ numerator = np.max(abs(residual)*row_scale, initial=0.)
2363
+ denominator = np.max(bound*row_scale, initial=0.)
2364
+ if not np.isfinite(numerator) or not np.isfinite(denominator):
2365
+ return float("inf")
2366
+ if denominator == 0.:
2367
+ return 0. if numerator == 0. else float("inf")
2368
+ return float(numerator/denominator)
2369
+
2370
+
2371
+ def _free_b2_rigid_space(owner, scale, scaled_mass, safe_point):
2372
+ """Common physical velocities, with only a bounded six-by-six Gram solve."""
2373
+ origin = owner._modal_positions[:len(owner.shell_node_ids)].mean(axis=0)
2374
+ canonical = np.zeros((owner._K.shape[0], 6))
2375
+ for i, position in enumerate(owner._modal_positions):
2376
+ slots = slice(6*i, 6*i+3)
2377
+ canonical[slots, :3] = np.eye(3)
2378
+ for j in range(3):
2379
+ canonical[slots, 3+j] = np.cross(np.eye(3)[j], position-origin)
2380
+ canonical[6*i+3:6*i+6, 3:] = np.eye(3)
2381
+ n = owner._K.shape[0]
2382
+ equations, gaps = [], []
2383
+ for column in canonical.T:
2384
+ safe_point("reference modes rigid field")
2385
+ equation = _modal_scaled_residual(owner._K@column,
2386
+ abs(owner._K)@abs(column), scale[:n])
2387
+ gap = _modal_scaled_residual(owner._C@column,
2388
+ abs(owner._C)@abs(column), scale[n:])
2389
+ if equation > 1e-8 or gap > 1e-9:
2390
+ raise _NumericalFailure("canonical free-free rigid field violates physical K or point C")
2391
+ equations.append(equation); gaps.append(gap)
2392
+ gram = canonical.T@owner._M@canonical
2393
+ if (not np.isfinite(gram).all() or
2394
+ np.max(abs(gram-gram.T)) > 1e-10*np.max(abs(gram))):
2395
+ raise _NumericalFailure("canonical rigid kinetic Gram symmetry unresolved")
2396
+ try:
2397
+ lower = np.linalg.cholesky(gram)
2398
+ normalized = canonical@np.linalg.solve(lower.T, np.eye(6))
2399
+ except np.linalg.LinAlgError as exc:
2400
+ raise _NumericalFailure("six physical rigid fields require positive kinetic Gram") from exc
2401
+ if (not np.isfinite(normalized).all() or
2402
+ np.max(abs(normalized.T@owner._M@normalized-np.eye(6))) > 1e-7):
2403
+ raise _NumericalFailure("rigid kinetic normalization unresolved")
2404
+ scaled = np.vstack((normalized, np.zeros((owner._C.shape[0], 6))))/scale[:, None]
2405
+ if (not np.isfinite(scaled).all() or
2406
+ np.max(abs(scaled.T@scaled_mass@scaled-np.eye(6))) > 1e-7):
2407
+ raise _NumericalFailure("rigid congruence reconstruction unresolved")
2408
+ return dict(origin=tuple(map(float, origin)), canonical=canonical,
2409
+ normalized=normalized, scaled=scaled, gram=gram,
2410
+ equations=tuple(equations), gaps=tuple(gaps))
2411
+
2412
+
2413
+ class ExperimentalB2MixedPanelAnalysis(ExperimentalB2Q4PanelAnalysis):
2414
+ """Explicit mixed-shell admission using the existing static sparse owner."""
2415
+
2416
+ _mixed_shells = True
2417
+
2418
+ @property
2419
+ def capabilities(self) -> ExperimentalB2MixedPanelCapabilities:
2420
+ existing = super().capabilities
2421
+ capability_type = (ExperimentalB2MixedReferenceModalCapabilities if
2422
+ self._reference_inertia_policy is not None else ExperimentalB2MixedPanelCapabilities)
2423
+ extra = ({"shell_mass_policies": tuple((shell_id,
2424
+ S3_V2D_TRANSLATIONAL_LUMPED_SECTION_AREAL_MASS_V1 if formulation ==
2425
+ S3_V2D_FORMULATION_ID else Q4_MASS_POLICY)
2426
+ for shell_id, formulation in self._shell_formulations)}
2427
+ if self._reference_inertia_policy is not None else {})
2428
+ if self._reference_inertia_policy is not None:
2429
+ extra["boundary_policy"] = self._reference_boundary_policy
2430
+ return capability_type(
2431
+ point_law_id=tuple(dict.fromkeys(law for _, law in self._point_laws)),
2432
+ attachment=existing.attachment,
2433
+ operations=existing.operations,
2434
+ body_load_domain=existing.body_load_domain,
2435
+ spectral_domain=(existing.spectral_domain if self._modal_only or self._physical_footprint_law else
2436
+ "experimental virgin supported finite descriptor pairs; heterogeneous Q4 director and native S3 lumped translation inertia; S3 rotary inertia omitted; completeness unqualified; prestress unsupported"
2437
+ if self._reference_inertia_policy is not None else
2438
+ "native S3 V2D physical mass and modes deferred" if self._has_s3
2439
+ else existing.spectral_domain),
2440
+ shell_formulations=self._shell_formulations,
2441
+ point_laws=self._point_laws,
2442
+ footprint_law_id=existing.footprint_law_id if self._physical_footprint_law else FOOTPRINT_LAW_ID if self.footprints else None,
2443
+ footprints=self.footprints, **extra)
2444
+
2445
+
2446
+ def create_experimental_b2_q4_panel_analysis(*args, **kwargs):
2447
+ return ExperimentalB2Q4PanelAnalysis(*args, **kwargs)