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.
- mechdsl/__init__.py +263 -0
- mechdsl/codegen/__init__.py +90 -0
- mechdsl/codegen/_experimental.py +71 -0
- mechdsl/codegen/anisotropic_emitter.py +233 -0
- mechdsl/codegen/artifact.py +319 -0
- mechdsl/codegen/boundary_codegen.py +277 -0
- mechdsl/codegen/common.py +1 -0
- mechdsl/codegen/einsum_optimizer.py +507 -0
- mechdsl/codegen/energy_emitter.py +316 -0
- mechdsl/codegen/family_registry.py +226 -0
- mechdsl/codegen/hex20_tables.py +314 -0
- mechdsl/codegen/hex8_reduced_tables.py +91 -0
- mechdsl/codegen/hex8_tables.py +214 -0
- mechdsl/codegen/hourglass.py +359 -0
- mechdsl/codegen/mfem_printer.py +819 -0
- mechdsl/codegen/mfem_template/CMakeLists.txt +51 -0
- mechdsl/codegen/moose_printer.py +603 -0
- mechdsl/codegen/moose_template/__init__.py +6 -0
- mechdsl/codegen/moose_template/input_template.i +112 -0
- mechdsl/codegen/spectral_emitter.py +284 -0
- mechdsl/codegen/taichi_printer.py +3597 -0
- mechdsl/codegen/templates/__init__.py +1 -0
- mechdsl/codegen/tet10_tables.py +267 -0
- mechdsl/codegen/tet4_tables.py +178 -0
- mechdsl/frontend/ARCHITECTURE.md +55 -0
- mechdsl/frontend/__init__.py +406 -0
- mechdsl/frontend/directives.py +748 -0
- mechdsl/frontend/math_parser.py +541 -0
- mechdsl/frontend/parser.py +294 -0
- mechdsl/frontend/two_point.py +12 -0
- mechdsl/integration/__init__.py +1100 -0
- mechdsl/ir/ARCHITECTURE.md +40 -0
- mechdsl/ir/__init__.py +39 -0
- mechdsl/ir/element_factory.py +192 -0
- mechdsl/ir/element_ir.py +822 -0
- mechdsl/ir/mechanics_ir.py +1438 -0
- mechdsl/lawgen/README.md +32 -0
- mechdsl/lawgen/REUSE.md +120 -0
- mechdsl/lawgen/__init__.py +40 -0
- mechdsl/lawgen/budgets.py +404 -0
- mechdsl/lawgen/carrier_emitter.py +447 -0
- mechdsl/lawgen/cli.py +588 -0
- mechdsl/lawgen/contracts.py +276 -0
- mechdsl/lawgen/diagnostics.py +263 -0
- mechdsl/lawgen/guard_transforms.py +234 -0
- mechdsl/lawgen/manifest.py +461 -0
- mechdsl/lawgen/sympy_to_taichi.py +738 -0
- mechdsl/lawgen/test_emitter.py +503 -0
- mechdsl/lib/__init__.py +25 -0
- mechdsl/lib/plasticity.py +214 -0
- mechdsl/lib/plasticity_kinematic.py +316 -0
- mechdsl/lib/plasticity_mixed.py +368 -0
- mechdsl/lib/tensor_ops.py +141 -0
- mechdsl/lowering/__init__.py +29 -0
- mechdsl/lowering/boundary.py +152 -0
- mechdsl/lowering/einsum_extract.py +255 -0
- mechdsl/lowering/fe_localise.py +411 -0
- mechdsl/solver/__init__.py +76 -0
- mechdsl/solver/_seam_runtime.py +67 -0
- mechdsl/solver/critical_timestep.py +300 -0
- mechdsl/solver/history_fields.py +116 -0
- mechdsl/solver/import_adapter.py +551 -0
- mechdsl/solver/integration.py +60 -0
- mechdsl/solver/jacobi_preconditioner.py +90 -0
- mechdsl/solver/load_stepping.py +143 -0
- mechdsl/solver/lumped_mass.py +139 -0
- mechdsl/solver/mesh_io.py +449 -0
- mechdsl/solver/newton.py +230 -0
- mechdsl/solver/seam_integrate.py +129 -0
- mechdsl/solver/seam_solve.py +173 -0
- mechdsl/symbolic/__init__.py +11 -0
- mechdsl/symbolic/anisotropic_energy.py +344 -0
- mechdsl/symbolic/bridge.py +234 -0
- mechdsl/symbolic/constitutive.py +97 -0
- mechdsl/symbolic/convected.py +586 -0
- mechdsl/symbolic/energy.py +492 -0
- mechdsl/symbolic/invariants.py +186 -0
- mechdsl/symbolic/kinematics.py +177 -0
- mechdsl/symbolic/models/__init__.py +12 -0
- mechdsl/symbolic/models/hgo.py +238 -0
- mechdsl/symbolic/models/j2_power_law.py +441 -0
- mechdsl/symbolic/models/johnson_cook.py +598 -0
- mechdsl/symbolic/models/lemaitre.py +375 -0
- mechdsl/symbolic/models/mooney_rivlin.py +214 -0
- mechdsl/symbolic/models/neo_hookean.py +204 -0
- mechdsl/symbolic/models/ogden.py +227 -0
- mechdsl/symbolic/models/perzyna.py +443 -0
- mechdsl/symbolic/models/svk.py +142 -0
- mechdsl/symbolic/objective_rates.py +370 -0
- mechdsl/symbolic/spectral_energy.py +291 -0
- mechdsl/symbolic/voigt.py +166 -0
- mechdsl/verify/__init__.py +1 -0
- mechdsl/verify/_assembly.py +278 -0
- mechdsl/verify/_patch_test_kernels.py +185 -0
- mechdsl/verify/ad_oracle.py +481 -0
- mechdsl/verify/analytical.py +284 -0
- mechdsl/verify/benchmarks/__init__.py +101 -0
- mechdsl/verify/benchmarks/_core.py +143 -0
- mechdsl/verify/benchmarks/_elastic_solver.py +226 -0
- mechdsl/verify/benchmarks/_j2_solver.py +185 -0
- mechdsl/verify/benchmarks/_meshes.py +398 -0
- mechdsl/verify/benchmarks/_taylor_runtime.py +961 -0
- mechdsl/verify/benchmarks/cantilever.py +164 -0
- mechdsl/verify/benchmarks/cook_membrane.py +266 -0
- mechdsl/verify/benchmarks/hgo_strip.py +448 -0
- mechdsl/verify/benchmarks/necking_bar.py +402 -0
- mechdsl/verify/benchmarks/notched_bar.py +589 -0
- mechdsl/verify/benchmarks/plate_with_hole.py +587 -0
- mechdsl/verify/benchmarks/taylor_impact.py +399 -0
- mechdsl/verify/benchmarks/thick_cylinder.py +629 -0
- mechdsl/verify/convergence.py +587 -0
- mechdsl/verify/mms_matrix.py +319 -0
- mechdsl/verify/patch_test.py +523 -0
- mechdsl/verify/perf/__init__.py +47 -0
- mechdsl/verify/perf/baseline.py +267 -0
- mechdsl/verify/perf/regenerate_baseline.py +62 -0
- mechdsl/verify/perf/registry.py +577 -0
- mechdsl/verify/perf/run_compare.py +119 -0
- mechdsl_core-0.2.1.dist-info/METADATA +41 -0
- mechdsl_core-0.2.1.dist-info/RECORD +123 -0
- mechdsl_core-0.2.1.dist-info/WHEEL +4 -0
- mechdsl_core-0.2.1.dist-info/entry_points.txt +2 -0
- 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
|
+
]
|