ti-runtime 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.
ti_runtime/__init__.py ADDED
@@ -0,0 +1,49 @@
1
+ """ti-runtime — neutral Taichi runtime for MechDSL-generated code (PlanJune14 PJ-0).
2
+
3
+ Seams + primitives only; algorithmic bodies are generated by algo2code and injected.
4
+ See the module-level READMEs for the Seams & Bodies contract.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ __version__ = "0.2.1"
10
+
11
+ from . import fields, hex8, seams, tensor_ti, vector_ops
12
+ from .seams import (
13
+ AccelSolve,
14
+ DiagonalPreconditioner,
15
+ IdentityPreconditioner,
16
+ Integrator,
17
+ LinearSolveContext,
18
+ Operator,
19
+ PreconditionerBase,
20
+ Solver,
21
+ TimeIntegrationContext,
22
+ )
23
+ from .vector_ops import axpy, copy, dot, ediv, norm2, scal, vec_add, xpay, zero
24
+
25
+ __all__ = [
26
+ "AccelSolve",
27
+ "DiagonalPreconditioner",
28
+ "IdentityPreconditioner",
29
+ "Integrator",
30
+ "LinearSolveContext",
31
+ "Operator",
32
+ "PreconditionerBase",
33
+ "Solver",
34
+ "TimeIntegrationContext",
35
+ "axpy",
36
+ "copy",
37
+ "dot",
38
+ "ediv",
39
+ "fields",
40
+ "hex8",
41
+ "norm2",
42
+ "scal",
43
+ "seams",
44
+ "tensor_ti",
45
+ "vec_add",
46
+ "vector_ops",
47
+ "xpay",
48
+ "zero",
49
+ ]
ti_runtime/fields.py ADDED
@@ -0,0 +1,44 @@
1
+ """Field allocation + ``ti.init`` boilerplate (PlanJune14 PJ-0).
2
+
3
+ The minimal runtime preamble generated code (and the test harness) targets:
4
+ choose a backend, allocate the standard field kinds. Kept deliberately thin —
5
+ heavy field-registry / domain-manager machinery from NumerixWeave is *not*
6
+ harvested (PlanJune14 "avoid" list).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import taichi as ti
12
+
13
+ _ARCH = {
14
+ "cpu": ti.cpu,
15
+ "gpu": ti.gpu,
16
+ "cuda": ti.cuda,
17
+ "metal": ti.metal,
18
+ "vulkan": ti.vulkan,
19
+ }
20
+
21
+
22
+ def init(arch: str = "cpu", default_fp=ti.f64, **kwargs) -> None:
23
+ """Initialise Taichi for a named backend (``cpu``/``gpu``/``cuda``/``metal``)."""
24
+ if arch not in _ARCH:
25
+ raise ValueError(f"Unknown arch {arch!r}; expected one of {sorted(_ARCH)}.")
26
+ ti.init(arch=_ARCH[arch], default_fp=default_fp, **kwargs)
27
+
28
+
29
+ def vector_field(dim: int, n: int, dtype=ti.f64):
30
+ """``n`` nodes × ``dim`` dof (e.g. displacement, force, residual)."""
31
+ return ti.Vector.field(dim, dtype=dtype, shape=n)
32
+
33
+
34
+ def scalar_field(n: int, dtype=ti.f64):
35
+ return ti.field(dtype=dtype, shape=n)
36
+
37
+
38
+ def matrix_field(rows: int, cols: int, shape, dtype=ti.f64):
39
+ return ti.Matrix.field(rows, cols, dtype=dtype, shape=shape)
40
+
41
+
42
+ def index_field(shape, dtype=ti.i32):
43
+ """Integer field, e.g. element→node connectivity."""
44
+ return ti.field(dtype=dtype, shape=shape)
ti_runtime/hex8.py ADDED
@@ -0,0 +1,53 @@
1
+ """Hex8 reference element: shape functions, gradients, quadrature (PlanJune14 PJ-0).
2
+
3
+ Trilinear 8-node hexahedron with 2×2×2 Gauss quadrature — the minimal element
4
+ the SVK spike (PJ-1) and the matrix-free tangent operator build on. Shape
5
+ functions/gradients are ``@ti.func`` (call from a kernel); quadrature tables are
6
+ Python constants iterated with ``ti.static``. Standard node ordering (corner
7
+ signs below). Adapted from NumerixWeave ``apps/tifem`` ``Ref_elements/HEX8``.
8
+ """
9
+
10
+ import taichi as ti
11
+
12
+ N_NODES = 8
13
+ DIM = 3
14
+ N_QP = 8
15
+
16
+ # Natural-coordinate corner signs (standard Hex8 node ordering).
17
+ _CORNERS = (
18
+ (-1.0, -1.0, -1.0),
19
+ (1.0, -1.0, -1.0),
20
+ (1.0, 1.0, -1.0),
21
+ (-1.0, 1.0, -1.0),
22
+ (-1.0, -1.0, 1.0),
23
+ (1.0, -1.0, 1.0),
24
+ (1.0, 1.0, 1.0),
25
+ (-1.0, 1.0, 1.0),
26
+ )
27
+
28
+ _G = 1.0 / (3.0**0.5)
29
+ # 2×2×2 Gauss points (corner sign pattern, scaled by 1/√3) and unit weights.
30
+ QUAD_POINTS = tuple((sx * _G, sy * _G, sz * _G) for (sx, sy, sz) in _CORNERS)
31
+ QUAD_WEIGHTS = (1.0,) * N_QP
32
+
33
+
34
+ @ti.func
35
+ def shape(xi, eta, zeta):
36
+ """Trilinear shape functions at natural coords → ``ti.Vector(8)``."""
37
+ N = ti.Vector.zero(ti.f64, 8)
38
+ for a in ti.static(range(8)):
39
+ sx, sy, sz = ti.static(_CORNERS[a])
40
+ N[a] = 0.125 * (1.0 + sx * xi) * (1.0 + sy * eta) * (1.0 + sz * zeta)
41
+ return N
42
+
43
+
44
+ @ti.func
45
+ def shape_grad_natural(xi, eta, zeta):
46
+ """``∂N_a/∂(ξ,η,ζ)`` → ``ti.Matrix(8, 3)``."""
47
+ dN = ti.Matrix.zero(ti.f64, 8, 3)
48
+ for a in ti.static(range(8)):
49
+ sx, sy, sz = ti.static(_CORNERS[a])
50
+ dN[a, 0] = 0.125 * sx * (1.0 + sy * eta) * (1.0 + sz * zeta)
51
+ dN[a, 1] = 0.125 * sy * (1.0 + sx * xi) * (1.0 + sz * zeta)
52
+ dN[a, 2] = 0.125 * sz * (1.0 + sx * xi) * (1.0 + sy * eta)
53
+ return dN
ti_runtime/seams.py ADDED
@@ -0,0 +1,303 @@
1
+ """Injection plumbing — the *seams* generated bodies plug into (PlanJune14 PJ-0).
2
+
3
+ MechDSL owns the stable wrappers; algo2code generates the bodies. The same
4
+ ``LinearSolveContext`` applies *whatever* operator / preconditioner / solver was
5
+ injected, so the plumbing is algorithm-agnostic.
6
+
7
+ A matrix-free operator is an in-place callable ``apply(out, x) -> None`` computing
8
+ ``out = A @ x`` over Taichi fields (typically a generated ``@ti.kernel``).
9
+ Preconditioner protocol mirrors NumerixWeave ``tisolvers``: ``apply(z, r)`` sets
10
+ ``z = M^{-1} r``.
11
+ """
12
+
13
+ import math
14
+ from collections.abc import Callable
15
+
16
+ import taichi as ti
17
+
18
+ from . import vector_ops as vops
19
+
20
+ # out = A @ x, in place.
21
+ OperatorApply = Callable[..., None]
22
+
23
+
24
+ @ti.data_oriented
25
+ class PreconditionerBase:
26
+ """SPD preconditioner interface: ``apply(z, r)`` sets ``z = M^{-1} r``."""
27
+
28
+ def apply(self, z, r) -> None:
29
+ raise NotImplementedError
30
+
31
+ def assemble(self) -> None:
32
+ """Rebuild internal state from the current operator/tangent.
33
+
34
+ No-op for stateless preconditioners (Identity, point-Jacobi); stateful
35
+ ones (block-Jacobi) override.
36
+ """
37
+
38
+
39
+ @ti.data_oriented
40
+ class IdentityPreconditioner(PreconditionerBase):
41
+ """``M = I`` — unpreconditioned."""
42
+
43
+ def apply(self, z, r) -> None:
44
+ vops.copy(z, r)
45
+
46
+
47
+ @ti.data_oriented
48
+ class DiagonalPreconditioner(PreconditionerBase):
49
+ """Point-Jacobi: ``M = diag(d)``, ``M^{-1} = diag(1/d)``.
50
+
51
+ ``diag`` is a Taichi field of the same layout as the vectors; division is
52
+ guarded by ``max(d, eps)`` to avoid NaN on a zero/near-zero diagonal.
53
+ """
54
+
55
+ def __init__(self, diag: ti.template(), eps: float = 1e-12):
56
+ self.diag = diag
57
+ self.eps = eps
58
+
59
+ @ti.kernel
60
+ def _apply(self, z: ti.template(), r: ti.template(), eps: float):
61
+ for I in ti.grouped(r):
62
+ z[I] = r[I] / ti.max(self.diag[I], eps)
63
+
64
+ def apply(self, z, r) -> None:
65
+ self._apply(z, r, self.eps)
66
+
67
+
68
+ class Operator:
69
+ """Holds the injected matrix-free operator ``apply(out, x): out = A @ x``."""
70
+
71
+ def __init__(self) -> None:
72
+ self._apply: OperatorApply | None = None
73
+
74
+ def set_apply(self, fn: OperatorApply) -> "Operator":
75
+ self._apply = fn
76
+ return self
77
+
78
+ def apply(self, out, x) -> None:
79
+ if self._apply is None:
80
+ raise RuntimeError("Operator has no body injected; call set_apply(...) first.")
81
+ self._apply(out, x)
82
+
83
+
84
+ class Solver:
85
+ """Holds an injected (generated) solver body ``solve(ctx, b, x, ...)``."""
86
+
87
+ def __init__(self) -> None:
88
+ self._solve: Callable[..., object] | None = None
89
+
90
+ def set_solve(self, fn: Callable[..., object]) -> "Solver":
91
+ self._solve = fn
92
+ return self
93
+
94
+ def solve(self, *args, **kwargs):
95
+ if self._solve is None:
96
+ raise RuntimeError("Solver has no body injected; call set_solve(...) first.")
97
+ return self._solve(*args, **kwargs)
98
+
99
+
100
+ class LinearSolveContext:
101
+ """The plumbing a generated linear solver targets.
102
+
103
+ Bundles the injected matrix-free operator and preconditioner; the generated
104
+ solver body calls :meth:`apply_A` / :meth:`apply_preconditioner` plus the
105
+ :mod:`ti_runtime.vector_ops` primitives. Defaults to the identity
106
+ preconditioner (unpreconditioned).
107
+ """
108
+
109
+ def __init__(self) -> None:
110
+ self.operator = Operator()
111
+ self.preconditioner: PreconditionerBase = IdentityPreconditioner()
112
+ self.solver = Solver()
113
+
114
+ def set_operator(self, fn: OperatorApply) -> "LinearSolveContext":
115
+ self.operator.set_apply(fn)
116
+ return self
117
+
118
+ def set_preconditioner(self, precond: PreconditionerBase) -> "LinearSolveContext":
119
+ self.preconditioner = precond
120
+ return self
121
+
122
+ def set_solver(self, fn: Callable[..., object]) -> "LinearSolveContext":
123
+ self.solver.set_solve(fn)
124
+ return self
125
+
126
+ def apply_A(self, out, x) -> None:
127
+ self.operator.apply(out, x)
128
+
129
+ def apply_preconditioner(self, z, r) -> None:
130
+ self.preconditioner.apply(z, r)
131
+
132
+
133
+ # ── Time-integration seam ────────────────────────────────────────────────────
134
+ #
135
+ # The temporal analogue of LinearSolveContext: a generated *time integrator*
136
+ # (e.g. a Newmark-beta step transpiled from its LaTeX algorithm spec) plugs
137
+ # into a stable wrapper here, exactly as a generated linear solver plugs into
138
+ # set_solver. The wrapper is integrator-agnostic — it applies *whatever* step
139
+ # body was injected (Newmark-beta, central difference, HHT, ...).
140
+ #
141
+ # An integrator step is an in-place callable advancing the dynamic state one
142
+ # step. The seam-injected acceleration solve mirrors the matrix-free operator:
143
+ # accel_solve(u_pred, v_pred, a_out) sets a_out = a_{n+1} (out LAST), folding the
144
+ # mass/damping/stiffness/external-force data — the box itself stays
145
+ # mass-/material-agnostic, just as the PCG box is operator-agnostic.
146
+
147
+ # (u, v, a, accel_solve, dt, beta, gamma) -> (u, v, a); advances state in place.
148
+ IntegratorStep = Callable[..., object]
149
+
150
+ # accel_solve(u_pred, v_pred, a_out): a_out = a_{n+1}, in place (out LAST).
151
+ # Returns an optional status: ``None`` for a solve that cannot fail
152
+ # (e.g. an elementwise SDOF/diagonal-mass solve), or a convergence flag for an
153
+ # iterative solve (a falsy flag == not converged). TimeIntegrationContext.step
154
+ # consumes it to roll back and fail loud rather than advancing on a bad solve.
155
+ AccelSolveApply = Callable[..., object]
156
+
157
+
158
+ class Integrator:
159
+ """Holds an injected (generated) time-integrator step body.
160
+
161
+ The body advances ``(u, v, a)`` one step in place, calling the injected
162
+ acceleration solve plus the :mod:`ti_runtime.vector_ops` primitives — the
163
+ shape ``dev/algorithms/newmark.tex`` transpiles to.
164
+ """
165
+
166
+ def __init__(self) -> None:
167
+ self._step: IntegratorStep | None = None
168
+
169
+ def set_step(self, fn: IntegratorStep) -> "Integrator":
170
+ self._step = fn
171
+ return self
172
+
173
+ def step(self, *args, **kwargs):
174
+ if self._step is None:
175
+ raise RuntimeError("Integrator has no body injected; call set_integrator(...) first.")
176
+ return self._step(*args, **kwargs)
177
+
178
+
179
+ class AccelSolve:
180
+ """Holds the injected acceleration solve ``apply(u_pred, v_pred, a_out)``.
181
+
182
+ Sets ``a_out = a_{n+1}`` — the solution of
183
+ ``(M + gamma*dt*C + beta*dt^2*K) a_{n+1} = F_{n+1} - C*v_pred - K*u_pred``.
184
+ For an SDOF / diagonal-mass problem this is an elementwise solve; for a full
185
+ FEM system it is itself a (linear) solve. Matrix-free: the wrapper applies
186
+ whatever callable was injected.
187
+ """
188
+
189
+ def __init__(self) -> None:
190
+ self._apply: AccelSolveApply | None = None
191
+
192
+ def set_apply(self, fn: AccelSolveApply) -> "AccelSolve":
193
+ self._apply = fn
194
+ return self
195
+
196
+ def apply(self, u_pred, v_pred, a_out) -> object:
197
+ if self._apply is None:
198
+ raise RuntimeError("AccelSolve has no body injected; call set_accel_solve(...) first.")
199
+ # Forward the injected solve's return value: a convergence status
200
+ # when the solve is iterative, else ``None``. The caller (step) decides.
201
+ return self._apply(u_pred, v_pred, a_out)
202
+
203
+
204
+ class TimeIntegrationContext:
205
+ """The plumbing a generated time integrator targets (PlanJune14 P6-1).
206
+
207
+ The temporal twin of :class:`LinearSolveContext`. Bundles the injected
208
+ acceleration solve and the injected integrator step; the generated step body
209
+ calls :meth:`apply_accel_solve` (the matrix-free seam) plus the
210
+ :mod:`ti_runtime.vector_ops` primitives. :meth:`step` advances the dynamic
211
+ state ``(u, v, a)`` one step in place by applying the injected integrator,
212
+ with no NumPy in the hot path.
213
+
214
+ Parameters ``dt`` / ``beta`` / ``gamma`` are the Newmark step controls; they
215
+ default to the average-acceleration scheme (``beta = 1/4``, ``gamma = 1/2``)
216
+ — unconditionally stable, second-order, no algorithmic damping.
217
+ """
218
+
219
+ def __init__(self, dt: float = 1.0, beta: float = 0.25, gamma: float = 0.5) -> None:
220
+ self.integrator = Integrator()
221
+ self.accel = AccelSolve()
222
+ self.dt = dt
223
+ self.beta = beta
224
+ self.gamma = gamma
225
+
226
+ def set_integrator(self, fn: IntegratorStep) -> "TimeIntegrationContext":
227
+ self.integrator.set_step(fn)
228
+ return self
229
+
230
+ def set_accel_solve(self, fn: AccelSolveApply) -> "TimeIntegrationContext":
231
+ self.accel.set_apply(fn)
232
+ return self
233
+
234
+ def apply_accel_solve(self, u_pred, v_pred, a_out) -> object:
235
+ return self.accel.apply(u_pred, v_pred, a_out)
236
+
237
+ def step(self, u, v, a):
238
+ """Advance the state ``(u, v, a)`` one step via the injected integrator.
239
+
240
+ Wires the seam conventions to the generated body's callable arguments:
241
+ the integrator's ``solve_a(u_pred, v_pred, a_out)`` argument is bound to
242
+ :meth:`apply_accel_solve` (the injected acceleration solve), and the
243
+ ``(dt, beta, gamma)`` controls come from this context. Returns whatever
244
+ the generated step returns (typically ``(u, v, a)``).
245
+
246
+ Fail-loud contract (WI-3). The step is committed only if the injected
247
+ acceleration solve succeeded and the advanced state is finite. Before
248
+ stepping we snapshot ``(u, v, a)``; if the injected solve **raises**
249
+ (e.g. the WI-2 seam PCG on non-convergence), **reports** a falsy
250
+ convergence status, or the advanced acceleration is **non-finite**, we
251
+ restore the snapshot and raise ``RuntimeError`` rather than leaving the
252
+ dynamic state half-advanced on a bad solve. This mirrors the host
253
+ Newton driver's snapshot/rollback + isfinite guard (newton.py). P6-1 is
254
+ not yet production-wired, so this establishes the contract without
255
+ redesigning the matrix-free body; the per-step snapshot is a
256
+ step-boundary archival copy (the same ``.to_numpy()`` round-trip the
257
+ P6-1 tests use at step boundaries), not a hot-path operation.
258
+ """
259
+ # Step-boundary snapshot for rollback (not the generated hot path).
260
+ u_snap = u.to_numpy()
261
+ v_snap = v.to_numpy()
262
+ a_snap = a.to_numpy()
263
+
264
+ def _restore() -> None:
265
+ u.from_numpy(u_snap)
266
+ v.from_numpy(v_snap)
267
+ a.from_numpy(a_snap)
268
+
269
+ status: dict[str, object] = {}
270
+
271
+ def accel_solve(u_pred, v_pred, a_out):
272
+ status["accel"] = self.apply_accel_solve(u_pred, v_pred, a_out)
273
+
274
+ try:
275
+ result = self.integrator.step(u, v, a, accel_solve, self.dt, self.beta, self.gamma)
276
+ except Exception:
277
+ # A raising solve (e.g. the seam-injected PCG) may have left state
278
+ # half-advanced -- restore before propagating.
279
+ _restore()
280
+ raise
281
+
282
+ # A reported non-convergence status (a falsy flag that is not ``None``;
283
+ # ``None`` means "no status / cannot fail", e.g. an elementwise solve).
284
+ accel_status = status.get("accel")
285
+ if accel_status is not None and not accel_status:
286
+ _restore()
287
+ raise RuntimeError(
288
+ "Time-integration acceleration solve did not converge "
289
+ f"(status={accel_status!r}); restored the pre-step state rather "
290
+ "than advancing (u, v, a) on a non-converged acceleration."
291
+ )
292
+
293
+ # Non-finite advanced acceleration poisons all subsequent steps; catch
294
+ # it here (mirrors the Newton driver's isfinite residual guard).
295
+ if not math.isfinite(vops.norm2(a)):
296
+ _restore()
297
+ raise RuntimeError(
298
+ "Time-integration step produced a non-finite acceleration "
299
+ "(||a|| is NaN/Inf); restored the pre-step state rather than "
300
+ "advancing on a corrupt acceleration."
301
+ )
302
+
303
+ return result
@@ -0,0 +1,93 @@
1
+ """Tier-1 tensor / kinematics ``@ti.func`` helpers (PlanJune14 PJ-0).
2
+
3
+ The building blocks generated constitutive / element code calls — kept as
4
+ ``@ti.func`` so they inline cheaply and don't burn JIT budget. Conventions follow
5
+ MechDSL ``dev/design_docs/07-CONVENTIONS.md``: spatial/material 3×3 tensors, Voigt
6
+ order ``[xx, yy, zz, xy, xz, yz]``, **tensorial** (unscaled shears), metric
7
+ ``G = diag(1,1,1,2,2,2)``. Harvested/adapted from NumerixWeave ``ticonstit``.
8
+
9
+ These are ``@ti.func`` — call them from inside a ``@ti.kernel``.
10
+ """
11
+
12
+ import taichi as ti
13
+
14
+ mat3 = ti.types.matrix(3, 3, ti.f64)
15
+ vec6 = ti.types.vector(6, ti.f64)
16
+
17
+
18
+ @ti.func
19
+ def identity3() -> mat3:
20
+ return ti.Matrix.identity(ti.f64, 3)
21
+
22
+
23
+ @ti.func
24
+ def det3(A) -> ti.f64:
25
+ """Determinant of a 3×3 matrix."""
26
+ return A.determinant()
27
+
28
+
29
+ @ti.func
30
+ def inv3(A) -> mat3:
31
+ """Inverse of a 3×3 matrix."""
32
+ return A.inverse()
33
+
34
+
35
+ @ti.func
36
+ def deformation_gradient(grad_u) -> mat3:
37
+ """``F = I + ∂u_i/∂X_J`` from the material displacement gradient (3×3)."""
38
+ return ti.Matrix.identity(ti.f64, 3) + grad_u
39
+
40
+
41
+ @ti.func
42
+ def jacobian(F) -> ti.f64:
43
+ """``J = det(F)``."""
44
+ return F.determinant()
45
+
46
+
47
+ @ti.func
48
+ def right_cauchy_green(F) -> mat3:
49
+ """``C = F^T F``."""
50
+ return F.transpose() @ F
51
+
52
+
53
+ @ti.func
54
+ def green_lagrange(F) -> mat3:
55
+ """``E = ½(F^T F − I)``."""
56
+ return 0.5 * (F.transpose() @ F - ti.Matrix.identity(ti.f64, 3))
57
+
58
+
59
+ @ti.func
60
+ def trace3(A) -> ti.f64:
61
+ return A[0, 0] + A[1, 1] + A[2, 2]
62
+
63
+
64
+ @ti.func
65
+ def deviatoric(A) -> mat3:
66
+ """Deviatoric part ``A − (tr A / 3) I``."""
67
+ p = (A[0, 0] + A[1, 1] + A[2, 2]) / 3.0
68
+ return A - p * ti.Matrix.identity(ti.f64, 3)
69
+
70
+
71
+ @ti.func
72
+ def von_mises(S) -> ti.f64:
73
+ """von Mises equivalent stress ``sqrt(3/2 · s:s)`` of a symmetric 3×3 ``S``."""
74
+ s = deviatoric(S)
75
+ return ti.sqrt(1.5 * (s * s).sum())
76
+
77
+
78
+ @ti.func
79
+ def to_voigt(S) -> vec6:
80
+ """Symmetric 3×3 → Voigt-6 ``[xx, yy, zz, xy, xz, yz]`` (unscaled shears)."""
81
+ return ti.Vector([S[0, 0], S[1, 1], S[2, 2], S[0, 1], S[0, 2], S[1, 2]], dt=ti.f64)
82
+
83
+
84
+ @ti.func
85
+ def from_voigt(v) -> mat3:
86
+ """Voigt-6 → symmetric 3×3 (off-diagonals not doubled — tensorial)."""
87
+ return ti.Matrix([[v[0], v[3], v[4]], [v[3], v[1], v[5]], [v[4], v[5], v[2]]], dt=ti.f64)
88
+
89
+
90
+ @ti.func
91
+ def double_contract(A, B) -> ti.f64:
92
+ """Full tensor double contraction ``A:B`` of two 3×3 tensors."""
93
+ return (A * B).sum()
@@ -0,0 +1,88 @@
1
+ """Vector-primitive ``@ti.kernel`` operations (PlanJune14 PJ-0).
2
+
3
+ Layout-agnostic primitives over Taichi *vector* fields (``ti.Vector.field(d)``),
4
+ the shape FEM degrees-of-freedom use. Generated solvers call these instead of
5
+ re-emitting their own (PlanJune14 PJ-2).
6
+
7
+ The scalar coefficient ``a`` is a **runtime ``float`` argument**, not
8
+ ``ti.template()`` — annotating a Python scalar as ``ti.template()`` bakes the
9
+ value into the compiled kernel, triggering a fresh JIT per distinct value
10
+ (observed as 100k+ kernel variants in tight Krylov loops). Harvested from
11
+ NumerixWeave ``tisolvers``.
12
+ """
13
+
14
+ import taichi as ti
15
+
16
+
17
+ @ti.kernel
18
+ def copy(dst: ti.template(), src: ti.template()):
19
+ """``dst <- src`` (elementwise)."""
20
+ for I in ti.grouped(src):
21
+ dst[I] = src[I]
22
+
23
+
24
+ @ti.kernel
25
+ def axpy(y: ti.template(), a: float, x: ti.template()):
26
+ """``y <- y + a*x``."""
27
+ for I in ti.grouped(y):
28
+ y[I] += a * x[I]
29
+
30
+
31
+ @ti.kernel
32
+ def xpay(x: ti.template(), a: float, y: ti.template()):
33
+ """``x <- a*x + y`` (the CG search-direction update)."""
34
+ for I in ti.grouped(x):
35
+ x[I] = a * x[I] + y[I]
36
+
37
+
38
+ @ti.kernel
39
+ def scal(x: ti.template(), a: float):
40
+ """``x <- a*x``."""
41
+ for I in ti.grouped(x):
42
+ x[I] = a * x[I]
43
+
44
+
45
+ @ti.kernel
46
+ def _dot(x: ti.template(), y: ti.template()) -> ti.f64:
47
+ s = 0.0
48
+ for I in ti.grouped(x):
49
+ s += x[I].dot(y[I])
50
+ return s
51
+
52
+
53
+ def dot(x, y) -> float:
54
+ """Euclidean inner product ``x . y`` over a vector field (returns a float)."""
55
+ return float(_dot(x, y))
56
+
57
+
58
+ def norm2(x) -> float:
59
+ """Euclidean 2-norm ``||x||_2``."""
60
+ return float(_dot(x, x)) ** 0.5
61
+
62
+
63
+ @ti.kernel
64
+ def vec_add(out: ti.template(), a: float, x: ti.template(), b: float, y: ti.template()):
65
+ """``out[I] = a*x[I] + b*y[I]`` (AXPBY, elementwise over any field shape)."""
66
+ for I in ti.grouped(out):
67
+ out[I] = a * x[I] + b * y[I]
68
+
69
+
70
+ def zero(x) -> None:
71
+ """Zero a field in place."""
72
+ x.fill(0.0)
73
+
74
+
75
+ @ti.kernel
76
+ def ediv(z: ti.template(), r: ti.template(), d: ti.template(), eps: float):
77
+ """Elementwise guarded divide: ``z[I] = r[I] / max(d[I], eps)``.
78
+
79
+ Point-Jacobi preconditioner kernel (PlanJune14 P4-1): applies M^{-1} r
80
+ where M = diag(d). The ``eps`` guard prevents division by zero on a
81
+ near-zero diagonal; the caller is responsible for choosing a physically
82
+ appropriate value (default: 1e-12).
83
+
84
+ Layout-agnostic: works for any ``ti.Vector.field`` or scalar ``ti.field``
85
+ where element-wise max and division are defined.
86
+ """
87
+ for I in ti.grouped(r):
88
+ z[I] = r[I] / ti.max(d[I], eps)
@@ -0,0 +1,58 @@
1
+ Metadata-Version: 2.4
2
+ Name: ti-runtime
3
+ Version: 0.2.1
4
+ Summary: Neutral Taichi runtime: vector primitives, Tier-1 @ti.func helpers, and solver/operator injection seams for MechDSL-generated code
5
+ Project-URL: Repository, https://github.com/CEmM2/MechDSL
6
+ Author: Shmuel Osovski
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: fem,kernels,matrix-free,runtime,taichi
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Requires-Python: <3.14,>=3.11
16
+ Requires-Dist: taichi>=1.7
17
+ Description-Content-Type: text/markdown
18
+
19
+ # ti-runtime
20
+
21
+ Neutral Taichi runtime for **MechDSL** (PlanJune14, PJ-0).
22
+
23
+ This package is the *seam + primitive* layer of the "Seams & Bodies" architecture:
24
+
25
+ > MechDSL owns the **seams + primitives** (this package); algo2code generates the
26
+ > **bodies** (solvers, preconditioners, constitutive updates, time integrators)
27
+ > from LaTeX, injected into these seams.
28
+
29
+ It contains only stable infrastructure — nothing algorithmic is hardcoded here:
30
+
31
+ | Module | Contents |
32
+ |---|---|
33
+ | `vector_ops` | `@ti.kernel` vector primitives: `copy / axpy / xpay / scal / zero / dot / norm2` |
34
+ | `tensor_ti` | Tier-1 `@ti.func` helpers: `det3 / inv3 / F→C→E→J / Voigt / deviatoric / von_mises` |
35
+ | `seams` | injection plumbing: `Operator`, `PreconditionerBase`/`Identity`/`Diagonal`, `Solver`, `LinearSolveContext` |
36
+ | `fields` | `ti.init` + field-allocation boilerplate |
37
+ | `hex8` | Hex8 shape functions, natural-coord gradients, 2×2×2 Gauss quadrature (test harness / element operators) |
38
+
39
+ ## Provenance & invariants
40
+
41
+ - **One-time harvest** (PlanJune14 D-B) — adapted from NumerixWeave
42
+ (`libs/tisolvers`, `libs/ticonstit`, `apps/tifem`); no ongoing sync.
43
+ - **Portable output** (D-D) — generated artifacts depend on `ti_runtime`, never on
44
+ `mechdsl`, so they can later feed NumerixWeave / MOOSE / MFEM.
45
+ - **Conventions** follow MechDSL `dev/design_docs/07-CONVENTIONS.md`: Voigt order
46
+ `[xx, yy, zz, xy, xz, yz]`, tensorial (unscaled shears), metric `diag(1,1,1,2,2,2)`.
47
+
48
+ ## Operator / solver contract
49
+
50
+ A matrix-free operator is an in-place callable `apply(out, x) -> None` computing
51
+ `out = A @ x` over Taichi fields. Inject it and run a (generated) solver against it:
52
+
53
+ ```python
54
+ ctx = LinearSolveContext()
55
+ ctx.set_operator(my_tangent_matvec) # out = K(u) @ x, matrix-free @ti.kernel
56
+ ctx.set_preconditioner(DiagonalPreconditioner(diag))
57
+ # a generated PCG body calls ctx.apply_A / ctx.apply_preconditioner + vector_ops
58
+ ```
@@ -0,0 +1,10 @@
1
+ ti_runtime/__init__.py,sha256=b_wiD-TDvRIjisDaEbQG1f_Okm68g_JBwExxMQWgCPg,1057
2
+ ti_runtime/fields.py,sha256=nPF-iF0uVZa-mau1lTz3ow8xGP9uqsCd5tGDZZjC3g4,1381
3
+ ti_runtime/hex8.py,sha256=LdpIrAhqkQmGdkOtOsqC-jBSAmjwElz5bb-mfjtRE_E,1765
4
+ ti_runtime/seams.py,sha256=KkkJWJKzMtw3RXjdvZAPWLOVheMzz3lqSLAqcKJgYmI,11907
5
+ ti_runtime/tensor_ti.py,sha256=OjXJQvJzI4IGOLni5XJyGiGx0qK4eLB3GWuYrH0RrQg,2432
6
+ ti_runtime/vector_ops.py,sha256=i3F6bnWRgmMYzjCpDlO7R06VQ-cJ1DFFArUe9ivJllc,2584
7
+ ti_runtime-0.2.1.dist-info/METADATA,sha256=aWU6QMF-Ldg4SVPZP9ganWAuE3PTFDLSR-opOtJ9IfQ,2657
8
+ ti_runtime-0.2.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
9
+ ti_runtime-0.2.1.dist-info/licenses/LICENSE,sha256=09AX3cq_TtJwbbZ00zsqmmGL_p_FINJ4l8asH_rhVhk,1071
10
+ ti_runtime-0.2.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shmuel Osovski
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.