mechdsl-core 0.2.1__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 (123) hide show
  1. mechdsl/__init__.py +263 -0
  2. mechdsl/codegen/__init__.py +90 -0
  3. mechdsl/codegen/_experimental.py +71 -0
  4. mechdsl/codegen/anisotropic_emitter.py +233 -0
  5. mechdsl/codegen/artifact.py +319 -0
  6. mechdsl/codegen/boundary_codegen.py +277 -0
  7. mechdsl/codegen/common.py +1 -0
  8. mechdsl/codegen/einsum_optimizer.py +507 -0
  9. mechdsl/codegen/energy_emitter.py +316 -0
  10. mechdsl/codegen/family_registry.py +226 -0
  11. mechdsl/codegen/hex20_tables.py +314 -0
  12. mechdsl/codegen/hex8_reduced_tables.py +91 -0
  13. mechdsl/codegen/hex8_tables.py +214 -0
  14. mechdsl/codegen/hourglass.py +359 -0
  15. mechdsl/codegen/mfem_printer.py +819 -0
  16. mechdsl/codegen/mfem_template/CMakeLists.txt +51 -0
  17. mechdsl/codegen/moose_printer.py +603 -0
  18. mechdsl/codegen/moose_template/__init__.py +6 -0
  19. mechdsl/codegen/moose_template/input_template.i +112 -0
  20. mechdsl/codegen/spectral_emitter.py +284 -0
  21. mechdsl/codegen/taichi_printer.py +3597 -0
  22. mechdsl/codegen/templates/__init__.py +1 -0
  23. mechdsl/codegen/tet10_tables.py +267 -0
  24. mechdsl/codegen/tet4_tables.py +178 -0
  25. mechdsl/frontend/ARCHITECTURE.md +55 -0
  26. mechdsl/frontend/__init__.py +406 -0
  27. mechdsl/frontend/directives.py +748 -0
  28. mechdsl/frontend/math_parser.py +541 -0
  29. mechdsl/frontend/parser.py +294 -0
  30. mechdsl/frontend/two_point.py +12 -0
  31. mechdsl/integration/__init__.py +1100 -0
  32. mechdsl/ir/ARCHITECTURE.md +40 -0
  33. mechdsl/ir/__init__.py +39 -0
  34. mechdsl/ir/element_factory.py +192 -0
  35. mechdsl/ir/element_ir.py +822 -0
  36. mechdsl/ir/mechanics_ir.py +1438 -0
  37. mechdsl/lawgen/README.md +32 -0
  38. mechdsl/lawgen/REUSE.md +120 -0
  39. mechdsl/lawgen/__init__.py +40 -0
  40. mechdsl/lawgen/budgets.py +404 -0
  41. mechdsl/lawgen/carrier_emitter.py +447 -0
  42. mechdsl/lawgen/cli.py +588 -0
  43. mechdsl/lawgen/contracts.py +276 -0
  44. mechdsl/lawgen/diagnostics.py +263 -0
  45. mechdsl/lawgen/guard_transforms.py +234 -0
  46. mechdsl/lawgen/manifest.py +461 -0
  47. mechdsl/lawgen/sympy_to_taichi.py +738 -0
  48. mechdsl/lawgen/test_emitter.py +503 -0
  49. mechdsl/lib/__init__.py +25 -0
  50. mechdsl/lib/plasticity.py +214 -0
  51. mechdsl/lib/plasticity_kinematic.py +316 -0
  52. mechdsl/lib/plasticity_mixed.py +368 -0
  53. mechdsl/lib/tensor_ops.py +141 -0
  54. mechdsl/lowering/__init__.py +29 -0
  55. mechdsl/lowering/boundary.py +152 -0
  56. mechdsl/lowering/einsum_extract.py +255 -0
  57. mechdsl/lowering/fe_localise.py +411 -0
  58. mechdsl/solver/__init__.py +76 -0
  59. mechdsl/solver/_seam_runtime.py +67 -0
  60. mechdsl/solver/critical_timestep.py +300 -0
  61. mechdsl/solver/history_fields.py +116 -0
  62. mechdsl/solver/import_adapter.py +551 -0
  63. mechdsl/solver/integration.py +60 -0
  64. mechdsl/solver/jacobi_preconditioner.py +90 -0
  65. mechdsl/solver/load_stepping.py +143 -0
  66. mechdsl/solver/lumped_mass.py +139 -0
  67. mechdsl/solver/mesh_io.py +449 -0
  68. mechdsl/solver/newton.py +230 -0
  69. mechdsl/solver/seam_integrate.py +129 -0
  70. mechdsl/solver/seam_solve.py +173 -0
  71. mechdsl/symbolic/__init__.py +11 -0
  72. mechdsl/symbolic/anisotropic_energy.py +344 -0
  73. mechdsl/symbolic/bridge.py +234 -0
  74. mechdsl/symbolic/constitutive.py +97 -0
  75. mechdsl/symbolic/convected.py +586 -0
  76. mechdsl/symbolic/energy.py +492 -0
  77. mechdsl/symbolic/invariants.py +186 -0
  78. mechdsl/symbolic/kinematics.py +177 -0
  79. mechdsl/symbolic/models/__init__.py +12 -0
  80. mechdsl/symbolic/models/hgo.py +238 -0
  81. mechdsl/symbolic/models/j2_power_law.py +441 -0
  82. mechdsl/symbolic/models/johnson_cook.py +598 -0
  83. mechdsl/symbolic/models/lemaitre.py +375 -0
  84. mechdsl/symbolic/models/mooney_rivlin.py +214 -0
  85. mechdsl/symbolic/models/neo_hookean.py +204 -0
  86. mechdsl/symbolic/models/ogden.py +227 -0
  87. mechdsl/symbolic/models/perzyna.py +443 -0
  88. mechdsl/symbolic/models/svk.py +142 -0
  89. mechdsl/symbolic/objective_rates.py +370 -0
  90. mechdsl/symbolic/spectral_energy.py +291 -0
  91. mechdsl/symbolic/voigt.py +166 -0
  92. mechdsl/verify/__init__.py +1 -0
  93. mechdsl/verify/_assembly.py +278 -0
  94. mechdsl/verify/_patch_test_kernels.py +185 -0
  95. mechdsl/verify/ad_oracle.py +481 -0
  96. mechdsl/verify/analytical.py +284 -0
  97. mechdsl/verify/benchmarks/__init__.py +101 -0
  98. mechdsl/verify/benchmarks/_core.py +143 -0
  99. mechdsl/verify/benchmarks/_elastic_solver.py +226 -0
  100. mechdsl/verify/benchmarks/_j2_solver.py +185 -0
  101. mechdsl/verify/benchmarks/_meshes.py +398 -0
  102. mechdsl/verify/benchmarks/_taylor_runtime.py +961 -0
  103. mechdsl/verify/benchmarks/cantilever.py +164 -0
  104. mechdsl/verify/benchmarks/cook_membrane.py +266 -0
  105. mechdsl/verify/benchmarks/hgo_strip.py +448 -0
  106. mechdsl/verify/benchmarks/necking_bar.py +402 -0
  107. mechdsl/verify/benchmarks/notched_bar.py +589 -0
  108. mechdsl/verify/benchmarks/plate_with_hole.py +587 -0
  109. mechdsl/verify/benchmarks/taylor_impact.py +399 -0
  110. mechdsl/verify/benchmarks/thick_cylinder.py +629 -0
  111. mechdsl/verify/convergence.py +587 -0
  112. mechdsl/verify/mms_matrix.py +319 -0
  113. mechdsl/verify/patch_test.py +523 -0
  114. mechdsl/verify/perf/__init__.py +47 -0
  115. mechdsl/verify/perf/baseline.py +267 -0
  116. mechdsl/verify/perf/regenerate_baseline.py +62 -0
  117. mechdsl/verify/perf/registry.py +577 -0
  118. mechdsl/verify/perf/run_compare.py +119 -0
  119. mechdsl_core-0.2.1.dist-info/METADATA +41 -0
  120. mechdsl_core-0.2.1.dist-info/RECORD +123 -0
  121. mechdsl_core-0.2.1.dist-info/WHEEL +4 -0
  122. mechdsl_core-0.2.1.dist-info/entry_points.txt +2 -0
  123. mechdsl_core-0.2.1.dist-info/licenses/LICENSE +21 -0
mechdsl/__init__.py ADDED
@@ -0,0 +1,263 @@
1
+ """MechDSL core — LaTeX tensor expressions to FEM solver code (Taichi).
2
+
3
+ The canonical MVP-stable entry point is :func:`compile_latex`, which accepts
4
+ a LaTeX source string and produces an :class:`~mechdsl.codegen.artifact.ArtifactBundle`.
5
+ The lower-level :func:`compile` is preserved as a programmatic API that
6
+ takes a pre-built :class:`~mechdsl.ir.mechanics_ir.ProblemIR`.
7
+
8
+ See ``README.md`` Support tiers and
9
+ ``dev/plans/recovery_plan_latex_contract.md`` Phase 2 (R1) for the recovery
10
+ context.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import TYPE_CHECKING
16
+
17
+ from mechdsl import integration as integration
18
+ from mechdsl.codegen import compile
19
+
20
+ if TYPE_CHECKING:
21
+ from os import PathLike
22
+
23
+ from mechdsl.codegen.artifact import ArtifactBundle
24
+
25
+ __version__ = "0.2.1"
26
+
27
+ # Allowed values for the `profile` argument of :func:`compile_latex`. Module-
28
+ # level so callers can introspect the supported set instead of guessing from
29
+ # the docstring. Add new profiles by extending this set explicitly — never
30
+ # silently relax the gate.
31
+ ALLOWED_PROFILES: frozenset[str] = frozenset({"mvp"})
32
+
33
+ __all__ = ["ALLOWED_PROFILES", "compile", "compile_latex", "integration"]
34
+
35
+ # Authored-invariant markers that select a non-default constitutive derivation
36
+ # path. Principal stretches (lbar1/2/3) belong to the spectral (Ogden) engine;
37
+ # the fiber pseudo-invariant (Ibar4) belongs to the anisotropic (HGO) engine.
38
+ _SPECTRAL_STRETCH = ("lbar1", "lbar2", "lbar3")
39
+ _FIBER_INVARIANT = "Ibar4"
40
+
41
+
42
+ def _derive_constitutive_energy(energy_source: str):
43
+ """Derive the constitutive model, routing by the authored invariants.
44
+
45
+ The free symbols of the parsed energy decide the path (robust to comments,
46
+ unlike a raw substring scan): a fiber pseudo-invariant ``Ibar4`` -> the
47
+ anisotropic (HGO) engine; principal stretches ``lbar1/2/3`` -> the spectral
48
+ (Ogden) engine; otherwise the named-invariant engine. Each engine
49
+ re-validates and rejects symbols outside its own contract.
50
+ """
51
+ from mechdsl.symbolic.energy import parse_energy_scalar_sympy
52
+
53
+ psi, _ = parse_energy_scalar_sympy(energy_source)
54
+ names = {s.name for s in psi.free_symbols}
55
+ if _FIBER_INVARIANT in names:
56
+ from mechdsl.symbolic.anisotropic_energy import derive_from_anisotropic_energy
57
+
58
+ return derive_from_anisotropic_energy(energy_source)
59
+ if any(s in names for s in _SPECTRAL_STRETCH):
60
+ from mechdsl.symbolic.spectral_energy import derive_from_spectral_energy
61
+
62
+ return derive_from_spectral_energy(energy_source)
63
+ from mechdsl.symbolic.energy import derive_from_energy
64
+
65
+ return derive_from_energy(energy_source)
66
+
67
+
68
+ def compile_latex(
69
+ source: str,
70
+ profile: str = "mvp",
71
+ *,
72
+ energy_source: str | None = None,
73
+ energy_file: str | PathLike[str] | None = None,
74
+ ) -> ArtifactBundle:
75
+ """Compile a LaTeX-source mechanics problem to an artifact bundle.
76
+
77
+ This is the canonical MVP-stable entry point. The function:
78
+
79
+ 1. Parses ``source`` through
80
+ :func:`mechdsl.frontend.parse_compile_context`, the math-aware
81
+ frontend entry point that preserves directive-only behaviour while
82
+ rejecting unsupported math-bearing sources before IR construction.
83
+ 2. Adapts the resulting context dict to a :class:`ProblemIR` via
84
+ :meth:`ProblemIR.from_latex_semantics` (fgram P6-1). This is the
85
+ LaTeX-semantic constructor: its MVP-stable *core* (dim, formulation,
86
+ element type, material, boundaries) is identical to
87
+ :meth:`ProblemIR.from_context`, so the emitted Taichi code is
88
+ byte-for-byte unchanged. The constructor additionally attaches a
89
+ serializable :class:`~mechdsl.ir.mechanics_ir.LatexSemantics` record
90
+ to the IR (declared fields, directive-tagged constitutive
91
+ ``(symbol, role)`` pairs, the weak-form label, and any ``$...$``
92
+ equation roles). That record rides into the artifact bundle's
93
+ ``problem_ir_dict["latex_semantics"]`` so emitted code sections are
94
+ traceable back to their source equation roles for review and golden
95
+ diffing — the codegen contract itself stays the typed IR fields
96
+ (``material`` / ``residual_contract``), not the advisory record.
97
+ 3. Forwards to :func:`compile` (the existing programmatic façade)
98
+ to run localisation, einsum planning, and Taichi emission.
99
+
100
+ **Backend stability:** Taichi is the only MVP-stable backend on the
101
+ canonical LaTeX compile path. All code produced by this function
102
+ targets the Taichi runtime. MFEM and MOOSE are experimental backends
103
+ preserved in the tree (``mechdsl.codegen.mfem_printer`` and
104
+ ``mechdsl.codegen.moose_printer``) but they are not reachable through
105
+ this façade and are not part of the stable contract. Reaching those
106
+ surfaces requires calling the experimental printers directly; see
107
+ ``README.md`` Support tiers and
108
+ ``dev/plans/recovery_plan_latex_contract.md`` Phase 5 (R4).
109
+
110
+ Parameters
111
+ ----------
112
+ source:
113
+ LaTeX source text containing ``% mechanics`` directives.
114
+ profile:
115
+ Compile profile selector. Only ``"mvp"`` is currently accepted;
116
+ other profiles (e.g. extended LaTeX math grammar via NRPyLaTeX,
117
+ non-canonical backends) are deferred to later recovery phases.
118
+ energy_source:
119
+ Optional self-contained nrpylatex strain-energy block (``% declare``
120
+ metric + ``EDD`` strain, then the scalar energy assignment). When
121
+ provided, it is run
122
+ through :func:`mechdsl.symbolic.energy.derive_from_energy` and the
123
+ resulting symbolic PK2 stress + material tangent are attached to the
124
+ :class:`ProblemIR` as ``derived_energy``. The generated solver then
125
+ emits its constitutive law and consistent tangent from the derived
126
+ energy (parameterised on the energy's own material-parameter names),
127
+ instead of the named-model ``(lam, mu)`` dispatch. The problem
128
+ ``source``'s ``MaterialSpec`` params must supply a value for every
129
+ parameter the energy uses.
130
+ energy_file:
131
+ Path to a ``.tex`` file holding the same energy block as
132
+ ``energy_source``. Mutually exclusive with ``energy_source``; the file
133
+ is read as UTF-8.
134
+
135
+ Boundary conditions
136
+ -------------------
137
+ LaTeX ``% mechanics boundary`` directives populate
138
+ :class:`mechdsl.ir.mechanics_ir.BoundaryCondition` slots on the
139
+ resulting :class:`ProblemIR`. Two boundary kinds are supported:
140
+
141
+ - **Dirichlet** — fixed displacement components, applied at solve
142
+ time by the runtime adapter.
143
+ - **Neumann** — surface tractions. When the directive carries a
144
+ numeric traction triple, post_recovery_plan P1-5 emits an
145
+ ``f_ext`` initialisation kernel (see ``f_ext_kernel`` in
146
+ *Returns* below) and the bundle is self-contained for that BC.
147
+ Neumann BCs with a symbolic-string traction leave the kernel
148
+ slot empty.
149
+
150
+ Numeric ``f_ext`` provisioning is the **caller's responsibility**
151
+ when no kernel is emitted (Dirichlet-only problems or
152
+ symbolic-traction Neumann): the caller supplies the global
153
+ external-force vector to the solver. When ``f_ext_kernel`` is
154
+ present, the emitted kernel overrides any manual ``f_ext``
155
+ provisioning for the directive-driven contribution; the caller
156
+ is still responsible for compositing it with any additional body
157
+ forces. ``BoundaryCondition`` objects therefore document intent
158
+ at the IR level, while the artifact bundle decides whether the
159
+ contract is satisfied entirely by emitted code or jointly by
160
+ caller-provided ``f_ext``.
161
+
162
+ Returns
163
+ -------
164
+ ArtifactBundle
165
+ The same bundle :func:`compile` produces, ready for golden-file
166
+ comparison or Taichi emission. When the parsed problem contains
167
+ one or more Neumann BCs with numeric traction (post_recovery_plan
168
+ P1-5), the bundle's optional ``f_ext_kernel`` field carries the
169
+ emitted ``init_f_ext_from_neumann_<bc>`` kernel source. The kernel
170
+ signature is parametric in ``f_factor`` (= face_area /
171
+ n_face_nodes) so the runtime mesh adapter supplies the per-node
172
+ weighting at call time. Pure Dirichlet problems and Neumann BCs
173
+ with symbolic-string traction leave ``f_ext_kernel`` at ``None``;
174
+ existing residual / tangent fields and ``emitted_source`` shape
175
+ are unchanged.
176
+
177
+ Raises
178
+ ------
179
+ ValueError
180
+ When ``profile`` is anything other than ``"mvp"``.
181
+ mechdsl.frontend.parser.ParseError
182
+ When the LaTeX source has a malformed ``% mechanics`` directive.
183
+ mechdsl.frontend.FrontendSemanticError
184
+ When a math-bearing LaTeX source cannot produce supported frontend
185
+ math semantics before IR construction.
186
+ mechdsl.symbolic.convected.UnsupportedError
187
+ When the parsed context falls outside the MVP-supported subset.
188
+
189
+ """
190
+ if profile not in ALLOWED_PROFILES:
191
+ raise ValueError(
192
+ f"compile_latex profile={profile!r} is not supported. "
193
+ f"Allowed profiles: {sorted(ALLOWED_PROFILES)}. "
194
+ "Broader profile support is planned for later recovery-plan "
195
+ "phases (see dev/plans/recovery_plan_latex_contract.md)."
196
+ )
197
+
198
+ if energy_source is not None and energy_file is not None:
199
+ raise ValueError(
200
+ "compile_latex accepts at most one of energy_source / energy_file, not both."
201
+ )
202
+ if energy_file is not None:
203
+ from pathlib import Path
204
+
205
+ energy_source = Path(energy_file).read_text(encoding="utf-8")
206
+
207
+ from mechdsl.frontend import parse_compile_context
208
+
209
+ ctx = parse_compile_context(source)
210
+
211
+ import dataclasses
212
+
213
+ from mechdsl.codegen.taichi_printer import (
214
+ EmissionContext,
215
+ emit_neumann_f_ext_kernel_for_ir,
216
+ )
217
+ from mechdsl.ir.mechanics_ir import BCType, ProblemIR
218
+
219
+ # Build the IR through the LaTeX-semantic constructor so the emitted
220
+ # bundle carries the source-role metadata (latex_semantics) that links
221
+ # generated code sections back to source equation roles. The MVP-stable
222
+ # core is identical to from_context, so emitted Taichi is unchanged;
223
+ # only the additive latex_semantics record is new.
224
+ problem_ir = ProblemIR.from_latex_semantics(ctx)
225
+ # When a strain-energy block was supplied, derive its symbolic stress and
226
+ # attach it to the IR. The codegen layer then emits the constitutive law and
227
+ # consistent tangent from the derived energy (taichi_printer + the matching
228
+ # emitter), parameterised on the energy's own material-parameter names rather
229
+ # than the named-model (lam, mu) dispatch. The derivation path is selected
230
+ # from the authored invariants: principal stretches (Ogden) -> spectral,
231
+ # a fiber pseudo-invariant (HGO) -> anisotropic, else the named-invariant path.
232
+ if energy_source is not None:
233
+ problem_ir = dataclasses.replace(
234
+ problem_ir, derived_energy=_derive_constitutive_energy(energy_source)
235
+ )
236
+ # Enforce the MVP-stable contract at the canonical compile-path
237
+ # boundary so users hitting an experimental combination or a missing
238
+ # required-param see a clean IR-level rejection instead of a deep
239
+ # codegen / runtime failure.
240
+ problem_ir.assert_mvp_stable()
241
+ bundle = compile(problem_ir)
242
+
243
+ # Surface a Neumann ``f_ext`` initialisation kernel for every Neumann
244
+ # BC carrying numeric traction. Symbolic (string) traction stays
245
+ # handled by the legacy imported numeric injection path so existing
246
+ # callers keep working unchanged. Pure Dirichlet problems leave
247
+ # ``f_ext_kernel`` at ``None``.
248
+ neumann_bcs = [
249
+ bc
250
+ for bc in problem_ir.boundaries
251
+ if bc.bc_type == BCType.NEUMANN and isinstance(bc.traction, tuple)
252
+ ]
253
+ if not neumann_bcs:
254
+ return bundle
255
+ em_ctx = EmissionContext()
256
+ for bc in neumann_bcs:
257
+ emit_neumann_f_ext_kernel_for_ir(
258
+ em_ctx,
259
+ bc_name=bc.name,
260
+ surface_tag=bc.effective_surface_tag,
261
+ traction=bc.traction, # type: ignore[arg-type]
262
+ )
263
+ return dataclasses.replace(bundle, f_ext_kernel=em_ctx.get_source())
@@ -0,0 +1,90 @@
1
+ """Einsum optimizer, Taichi code generation, boundary condition codegen.
2
+
3
+ Backend support tiers
4
+ ---------------------
5
+ The MVP-stable canonical backend is :mod:`mechdsl.codegen.taichi_printer`.
6
+ :mod:`mechdsl.codegen.mfem_printer` and :mod:`mechdsl.codegen.moose_printer`
7
+ are preserved in-tree as **experimental** surfaces (see
8
+ ``dev/plans/recovery_plan_latex_contract.md`` Phase 5 (R4)).
9
+
10
+ The ``__experimental__`` convention
11
+ -----------------------------------
12
+ Every experimental printer module exposes a module-level constant::
13
+
14
+ __experimental__: bool = True
15
+
16
+ so callers and tests can detect experimental status programmatically::
17
+
18
+ from mechdsl.codegen import mfem_printer
19
+ assert mfem_printer.__experimental__ is True
20
+
21
+ Stable backends do **not** set this flag. On the first call to its public
22
+ ``emit`` function, each experimental printer additionally raises a single
23
+ :class:`mechdsl.codegen._experimental.ExperimentalBackendWarning` that can
24
+ be filtered with the standard :mod:`warnings` machinery.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ from typing import TYPE_CHECKING
30
+
31
+ from mechdsl.codegen._experimental import ExperimentalBackendWarning
32
+ from mechdsl.codegen.taichi_printer import TaichiCodegenFacade
33
+
34
+ if TYPE_CHECKING:
35
+ from mechdsl.codegen.artifact import ArtifactBundle
36
+ from mechdsl.ir.mechanics_ir import ProblemIR
37
+
38
+ __all__ = ["ExperimentalBackendWarning", "TaichiCodegenFacade", "compile"]
39
+
40
+
41
+ def compile(problem_ir: ProblemIR) -> ArtifactBundle:
42
+ """Compile a ProblemIR into a self-contained Taichi solver.
43
+
44
+ Runs the full pipeline: localise → optimise → emit.
45
+
46
+ **Backend stability:** Taichi is the only MVP-stable backend on the
47
+ canonical LaTeX compile path. MFEM and MOOSE printers exist in the
48
+ tree as experimental surfaces (see ``mechdsl.codegen.mfem_printer``
49
+ and ``mechdsl.codegen.moose_printer``) but they are not part of the
50
+ stable contract and may not be supported in future recovery phases
51
+ without explicit roadmap work (Plan B §B8).
52
+
53
+ Parameters
54
+ ----------
55
+ problem_ir : ProblemIR
56
+ The semantic problem specification.
57
+
58
+ Returns
59
+ -------
60
+ ArtifactBundle
61
+ Bundle with ``emitted_source`` populated. Contains all
62
+ intermediate products (IRs, contraction plans).
63
+
64
+ Raises
65
+ ------
66
+ ValueError
67
+ Propagated when the IR contains an unsupported formulation,
68
+ element type, or material model for the MVP pipeline.
69
+ BudgetExceededError
70
+ Propagated when einsum optimisation exceeds the absolute JIT
71
+ budget ceiling.
72
+ """
73
+ from mechdsl.codegen.artifact import ArtifactBundle as _AB
74
+ from mechdsl.codegen.taichi_printer import emit
75
+ from mechdsl.lowering.fe_localise import localise_and_optimize
76
+
77
+ loc_result, plans = localise_and_optimize(problem_ir)
78
+ bundle = _AB.from_pipeline(problem_ir, loc_result, plans)
79
+ source = emit(bundle)
80
+
81
+ return _AB(
82
+ problem_ir_dict=bundle.problem_ir_dict,
83
+ element_ir_summary=bundle.element_ir_summary,
84
+ contraction_plans=bundle.contraction_plans,
85
+ emitted_source=source,
86
+ metadata=bundle.metadata,
87
+ # Preserve the derived-energy Python-object channel through the
88
+ # bundle rebuild so a LaTeX-derived constitutive law reaches codegen.
89
+ derived_energy=bundle.derived_energy,
90
+ )
@@ -0,0 +1,71 @@
1
+ """Experimental-backend infrastructure for mechdsl.codegen.
2
+
3
+ Defines the shared :class:`ExperimentalBackendWarning` that each non-stable
4
+ printer emits on first use, and documents the ``__experimental__`` flag
5
+ convention used across the codegen package.
6
+
7
+ Convention
8
+ ----------
9
+ Every experimental backend module exposes a module-level constant::
10
+
11
+ __experimental__: bool = True
12
+
13
+ This allows tooling and tests to detect experimental status
14
+ programmatically (e.g. ``import mechdsl.codegen.mfem_printer as m;
15
+ assert m.__experimental__``) rather than parsing docstrings. Stable
16
+ backends (currently only :mod:`mechdsl.codegen.taichi_printer`) do **not**
17
+ set this flag.
18
+
19
+ At runtime, each experimental printer emits one
20
+ :class:`ExperimentalBackendWarning` per Python session on the first call to
21
+ its public ``emit`` function. The warning is filterable with the standard
22
+ :mod:`warnings` machinery::
23
+
24
+ import warnings
25
+ warnings.filterwarnings("ignore", category=ExperimentalBackendWarning)
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import warnings
31
+
32
+ __all__ = ["ExperimentalBackendWarning", "warn_experimental_backend_once"]
33
+
34
+
35
+ class ExperimentalBackendWarning(UserWarning):
36
+ """Warning raised on the first call to an experimental codegen backend.
37
+
38
+ Experimental backends (MFEM, MOOSE) are preserved in-tree but are not
39
+ part of the MVP stable contract. This warning signals that the caller
40
+ is using a surface outside the stable Taichi compile path.
41
+
42
+ Filter with::
43
+
44
+ import warnings
45
+ warnings.filterwarnings("ignore", category=ExperimentalBackendWarning)
46
+ """
47
+
48
+
49
+ def warn_experimental_backend_once(state: dict, backend_name: str) -> None:
50
+ """Emit an ExperimentalBackendWarning the first time this is called for the given state.
51
+
52
+ Idempotent: subsequent calls with the same state dict are no-ops.
53
+
54
+ Parameters
55
+ ----------
56
+ state : dict
57
+ Module-level dict tracking warning state. Caller passes a single shared
58
+ dict (e.g. ``_warn_state``); this function flips ``state["warned"]`` to True.
59
+ backend_name : str
60
+ Human-readable backend name (e.g. "MFEM", "MOOSE") for the warning message.
61
+ """
62
+ if state.get("warned"):
63
+ return
64
+ warnings.warn(
65
+ f"{backend_name} backend is experimental — its API may change without "
66
+ f"deprecation notice. Suppress with "
67
+ f"warnings.filterwarnings('ignore', category=ExperimentalBackendWarning).",
68
+ ExperimentalBackendWarning,
69
+ stacklevel=3,
70
+ )
71
+ state["warned"] = True
@@ -0,0 +1,233 @@
1
+ """Emit a Taichi constitutive ``@ti.func`` from a LaTeX-derived *anisotropic*
2
+ (fiber-reinforced, HGO) energy.
3
+
4
+ The straight-line emitter in :mod:`mechdsl.codegen.energy_emitter` prints one
5
+ unconditional assignment per stress component. The Holzapfel-Gasser-Ogden (HGO)
6
+ family needs two things it structurally cannot do (see :issue:`288`):
7
+
8
+ 1. a **data-dependent branch** — the fiber term is gated to tension by the
9
+ Macaulay bracket ``<Ibar4 - 1>`` (active only when ``Ibar4 > 1``); and
10
+ 2. a **per-element fiber-direction gather** — the fiber stress depends on a
11
+ direction ``a`` that is per-element field data (P5-1
12
+ ``ProblemIR.fiber_field`` -> ``ElementIR.fiber_field``), not authored in the
13
+ LaTeX, so the constitutive function gains a fiber-direction argument that the
14
+ call site reads from the gathered field.
15
+
16
+ This module supplies :func:`emit_anisotropic_constitutive_func`, which emits a
17
+ ``constitutive_update(F, a, *params)`` that:
18
+
19
+ * computes the always-on isotropic + volumetric stress ``S_iso(E)`` (identical
20
+ in form to the proven neo-Hookean emission);
21
+ * normalises the fiber direction ``a`` (as the oracle does), computes the fiber
22
+ pseudo-invariant ``Ibar4(E, a)``; and
23
+ * adds the active-branch fiber stress ``S_fib(E, a)`` only when ``Ibar4 > 1``.
24
+
25
+ It reproduces :meth:`AnisotropicEnergyModel.pk2_stress` for a single fiber family
26
+ (the ``fiber_dispersion = 0`` perfectly-aligned case), so the emitted Taichi and
27
+ the NumPy oracle agree to FP noise.
28
+
29
+ Conventions (07-CONVENTIONS.md): tension-positive stress; float64. Material
30
+ parameters keep their sanitised LaTeX names as ``ti.f64`` arguments, matching
31
+ :mod:`mechdsl.codegen.energy_emitter`.
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ from typing import TYPE_CHECKING
37
+
38
+ from sympy import Symbol, pycode
39
+
40
+ from mechdsl.codegen.energy_emitter import _to_numpy_math, _to_taichi_math
41
+
42
+ if TYPE_CHECKING:
43
+ from mechdsl.symbolic.anisotropic_energy import AnisotropicEnergyModel
44
+
45
+ __all__ = [
46
+ "ANISOTROPIC_PK2_NUMPY_NAME",
47
+ "anisotropic_param_names",
48
+ "emit_anisotropic_constitutive_func",
49
+ "emit_anisotropic_pk2_numpy_source",
50
+ "emit_anisotropic_tangent_matvec_body",
51
+ ]
52
+
53
+ _STRAIN = "EDD"
54
+
55
+ # Name of the emitted host-NumPy PK2 helper the FD tangent calls.
56
+ ANISOTROPIC_PK2_NUMPY_NAME = "_pk2_anisotropic"
57
+
58
+
59
+ def anisotropic_param_names(model: AnisotropicEnergyModel) -> list[str]:
60
+ """Sorted union of isotropic and fiber material-parameter names.
61
+
62
+ These name the ``ti.f64`` arguments of the emitted ``constitutive_update``
63
+ (in the same sorted order the body forwards them), mirroring
64
+ :func:`mechdsl.codegen.energy_emitter.derived_param_names`. The union (not
65
+ just the isotropic params) guarantees a fiber-only parameter (e.g. ``k1`` /
66
+ ``k2``) is still plumbed into the signature.
67
+ """
68
+ names = {s.name for s in model.iso_param_symbols}
69
+ names |= {s.name for s in model.fiber_param_symbols}
70
+ return sorted(names)
71
+
72
+
73
+ def emit_anisotropic_constitutive_func(
74
+ model: AnisotropicEnergyModel,
75
+ *,
76
+ func_name: str = "constitutive_update",
77
+ param_names: list[str] | None = None,
78
+ fiber_arg: str = "a",
79
+ ) -> str:
80
+ """Return a Taichi ``@ti.func`` computing PK2 stress ``S(F, a)`` from a
81
+ derived *anisotropic* (HGO) energy model.
82
+
83
+ The emitted function takes the deformation gradient ``F``, a per-element
84
+ fiber-direction vector ``a`` (``ti.Vector(3)``, gathered from the fiber
85
+ field at the call site), and one ``ti.f64`` argument per material parameter.
86
+ It builds the Green-Lagrange strain, assigns the always-on isotropic stress,
87
+ then conditionally adds the fiber stress when the tension gate
88
+ ``Ibar4(E, a) > 1`` is open.
89
+
90
+ ``param_names`` overrides the argument list (default:
91
+ :func:`anisotropic_param_names`); pass an explicit list to keep the
92
+ signature aligned with the unified solver parameter vocabulary, as
93
+ :mod:`mechdsl.codegen.energy_emitter` does. ``fiber_arg`` names the fiber
94
+ vector argument (default ``a``, matching the ``a0``/``a1``/``a2`` symbols the
95
+ derivation uses).
96
+ """
97
+ # Strain symbols EDDij -> the per-component locals Eij the body declares.
98
+ strain_subs = {
99
+ model.strain_symbols[i][j]: Symbol(f"E{i}{j}") for i in range(3) for j in range(3)
100
+ }
101
+
102
+ args = param_names if param_names is not None else anisotropic_param_names(model)
103
+ # @ti.func arguments are untyped: Taichi infers their types from the call site.
104
+ arglist = ", ".join(["F", fiber_arg, *args])
105
+ head = [
106
+ "@ti.func",
107
+ f"def {func_name}({arglist}):",
108
+ ]
109
+ body = [
110
+ '"""PK2 stress from a fiber-reinforced (HGO) strain energy; fiber term'
111
+ ' tension-gated by the Macaulay bracket <Ibar4 - 1>."""',
112
+ "C = F.transpose() @ F",
113
+ "I3 = ti.Matrix.identity(ti.f64, 3)",
114
+ "E = 0.5 * (C - I3)",
115
+ ]
116
+ for i in range(3):
117
+ for j in range(3):
118
+ body.append(f"E{i}{j} = E[{i}, {j}]")
119
+ # Normalise the gathered fiber direction (the oracle normalises before
120
+ # evaluating). The derivation's a0/a1/a2 symbols bind to these locals, so
121
+ # pycode references resolve directly.
122
+ body.append(
123
+ f"_anorm = ti.sqrt({fiber_arg}[0] * {fiber_arg}[0] + {fiber_arg}[1] * {fiber_arg}[1]"
124
+ f" + {fiber_arg}[2] * {fiber_arg}[2])"
125
+ )
126
+ for k in range(3):
127
+ body.append(f"a{k} = {fiber_arg}[{k}] / _anorm")
128
+ # Always-on isotropic + volumetric stress.
129
+ body.append("S = ti.Matrix.zero(ti.f64, 3, 3)")
130
+ for i in range(3):
131
+ for j in range(3):
132
+ expr = model.iso_pk2[i, j].xreplace(strain_subs)
133
+ body.append(f"S[{i}, {j}] = {_to_taichi_math(pycode(expr))}")
134
+ # Fiber gate: the Macaulay bracket makes the fiber term active in tension
135
+ # only (Ibar4 > 1) -- the data-dependent branch the straight-line emitter
136
+ # structurally lacks.
137
+ ibar4_expr = model.fiber_ibar4.xreplace(strain_subs)
138
+ body.append(f"ibar4 = {_to_taichi_math(pycode(ibar4_expr))}")
139
+ body.append("if ibar4 > 1.0:")
140
+ for i in range(3):
141
+ for j in range(3):
142
+ expr = model.fiber_pk2[i, j].xreplace(strain_subs)
143
+ if expr == 0:
144
+ continue
145
+ body.append(f" S[{i}, {j}] += {_to_taichi_math(pycode(expr))}")
146
+ body.append("return 0.5 * (S + S.transpose())")
147
+
148
+ indent = " "
149
+ return "\n".join(head + [indent + line for line in body]) + "\n"
150
+
151
+
152
+ def emit_anisotropic_pk2_numpy_source(
153
+ model: AnisotropicEnergyModel,
154
+ *,
155
+ func_name: str = ANISOTROPIC_PK2_NUMPY_NAME,
156
+ param_names: list[str] | None = None,
157
+ fiber_arg: str = "a",
158
+ ) -> str:
159
+ """Return a host-NumPy ``def`` computing PK2 stress ``S(E, a)`` for the HGO
160
+ energy, used as the central-difference reference of the FD tangent.
161
+
162
+ The internal force is computed by the device ``@ti.func``
163
+ (:func:`emit_anisotropic_constitutive_func`); the matrix-free
164
+ ``tangent_matvec`` is host NumPy and needs a host stress to finite-difference.
165
+ This emits exactly :meth:`AnisotropicEnergyModel.pk2_stress` for a single
166
+ fiber family: the always-on isotropic stress plus the tension-gated fiber
167
+ stress (``Ibar4 > 1``), with the fiber direction normalised, so the device
168
+ and host stresses stay numerically identical.
169
+ """
170
+ strain_subs = {
171
+ model.strain_symbols[i][j]: Symbol(f"E{i}{j}") for i in range(3) for j in range(3)
172
+ }
173
+ args = param_names if param_names is not None else anisotropic_param_names(model)
174
+ arglist = ", ".join(["E", fiber_arg, *args])
175
+ head = [f"def {func_name}({arglist}):"]
176
+ body = [
177
+ '"""Host-NumPy PK2 stress for the HGO energy (FD-tangent reference)."""',
178
+ ]
179
+ for i in range(3):
180
+ for j in range(3):
181
+ body.append(f"E{i}{j} = E[{i}, {j}]")
182
+ body.append(
183
+ f"_anorm = np.sqrt({fiber_arg}[0] ** 2 + {fiber_arg}[1] ** 2 + {fiber_arg}[2] ** 2)"
184
+ )
185
+ for k in range(3):
186
+ body.append(f"a{k} = {fiber_arg}[{k}] / _anorm")
187
+ body.append("S = np.zeros((3, 3), dtype=np.float64)")
188
+ for i in range(3):
189
+ for j in range(3):
190
+ expr = model.iso_pk2[i, j].xreplace(strain_subs)
191
+ body.append(f"S[{i}, {j}] = {_to_numpy_math(pycode(expr))}")
192
+ ibar4_expr = model.fiber_ibar4.xreplace(strain_subs)
193
+ body.append(f"ibar4 = {_to_numpy_math(pycode(ibar4_expr))}")
194
+ body.append("if ibar4 > 1.0:")
195
+ for i in range(3):
196
+ for j in range(3):
197
+ expr = model.fiber_pk2[i, j].xreplace(strain_subs)
198
+ if expr == 0:
199
+ continue
200
+ body.append(f" S[{i}, {j}] += {_to_numpy_math(pycode(expr))}")
201
+ body.append("return 0.5 * (S + S.T)")
202
+
203
+ indent = " "
204
+ return "\n".join(head + [indent + line for line in body]) + "\n"
205
+
206
+
207
+ def emit_anisotropic_tangent_matvec_body(
208
+ model: AnisotropicEnergyModel,
209
+ *,
210
+ pk2_name: str = ANISOTROPIC_PK2_NUMPY_NAME,
211
+ param_names: list[str] | None = None,
212
+ fiber_local: str = "a_fiber",
213
+ ) -> list[str]:
214
+ """Host-NumPy lines for the HGO material term inside ``tangent_matvec``.
215
+
216
+ Returns statements (no leading indent) that, given the Green-Lagrange strain
217
+ ``E``, the linearised strain ``dE`` (both ``(3, 3)`` ``np.ndarray``), the
218
+ fiber direction ``fiber_local`` (a 3-vector in scope), and the material
219
+ parameters already in scope, assign ``S`` (derived PK2 stress) and ``dS``
220
+ (``C : dE`` via the directional central difference). FD is the correct method
221
+ across the non-smooth Macaulay gate (it matches
222
+ :meth:`AnisotropicEnergyModel.material_tangent_4th`).
223
+ """
224
+ args = param_names if param_names is not None else anisotropic_param_names(model)
225
+ fwd = ", ".join(args)
226
+ return [
227
+ "# Derived HGO constitutive law: PK2 stress + central-difference FD tangent.",
228
+ f"S = {pk2_name}(E, {fiber_local}, {fwd})",
229
+ "_fd_eps = 1e-6",
230
+ f"_S_plus = {pk2_name}(E + _fd_eps * dE, {fiber_local}, {fwd})",
231
+ f"_S_minus = {pk2_name}(E - _fd_eps * dE, {fiber_local}, {fwd})",
232
+ "dS = (_S_plus - _S_minus) / (2.0 * _fd_eps)",
233
+ ]