ti-runtime 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,75 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.egg-info/
6
+ *.egg
7
+ dist/
8
+ build/
9
+ *.whl
10
+ .eggs/
11
+
12
+ # Virtual environments
13
+ .venv/
14
+ venv/
15
+ env/
16
+
17
+ # IDE
18
+ .idea/
19
+ .vscode/
20
+ *.swp
21
+ *.swo
22
+ *~
23
+
24
+ # Testing
25
+ .pytest_cache/
26
+ .coverage
27
+ htmlcov/
28
+ .mypy_cache/
29
+ .ruff_cache/
30
+
31
+ # OS
32
+ .DS_Store
33
+ Thumbs.db
34
+
35
+ # Taichi
36
+ *.tcb
37
+ ti_cache/
38
+
39
+ # Artifacts
40
+ *.artifact.json
41
+ *.artifact.yaml
42
+
43
+ # Jupyter
44
+ .ipynb_checkpoints/
45
+
46
+ # Claude Code
47
+ .claude/settings.local.json
48
+
49
+ # Claude CoWork
50
+ /MechDsl/
51
+ *.npz
52
+ # Golden-file regression snapshots must be tracked
53
+ !packages/mechdsl-core/tests/golden/*.npz
54
+ .gitnexus
55
+
56
+ # Local working artifacts (orchestra runtime, plan-edit backups)
57
+ .orchestra/
58
+ **/.orchestra/
59
+ *.original.md
60
+ *.original.[0-9].md
61
+ .claude/worktrees/
62
+ .publisher-worktrees/
63
+ .sesskey
64
+
65
+ # Public-release staging tree (curated copy pushed to CEmM2/MechDSL)
66
+ /dev/MechDSL/
67
+
68
+ # MkDocs build output
69
+ /site/
70
+
71
+ # Generated wiki / code-intelligence index (logic-loom / akms tooling)
72
+ .repo_wiki/
73
+
74
+ # comment-sweep working tree
75
+ /.comment-review/
@@ -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.
@@ -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,40 @@
1
+ # ti-runtime
2
+
3
+ Neutral Taichi runtime for **MechDSL** (PlanJune14, PJ-0).
4
+
5
+ This package is the *seam + primitive* layer of the "Seams & Bodies" architecture:
6
+
7
+ > MechDSL owns the **seams + primitives** (this package); algo2code generates the
8
+ > **bodies** (solvers, preconditioners, constitutive updates, time integrators)
9
+ > from LaTeX, injected into these seams.
10
+
11
+ It contains only stable infrastructure — nothing algorithmic is hardcoded here:
12
+
13
+ | Module | Contents |
14
+ |---|---|
15
+ | `vector_ops` | `@ti.kernel` vector primitives: `copy / axpy / xpay / scal / zero / dot / norm2` |
16
+ | `tensor_ti` | Tier-1 `@ti.func` helpers: `det3 / inv3 / F→C→E→J / Voigt / deviatoric / von_mises` |
17
+ | `seams` | injection plumbing: `Operator`, `PreconditionerBase`/`Identity`/`Diagonal`, `Solver`, `LinearSolveContext` |
18
+ | `fields` | `ti.init` + field-allocation boilerplate |
19
+ | `hex8` | Hex8 shape functions, natural-coord gradients, 2×2×2 Gauss quadrature (test harness / element operators) |
20
+
21
+ ## Provenance & invariants
22
+
23
+ - **One-time harvest** (PlanJune14 D-B) — adapted from NumerixWeave
24
+ (`libs/tisolvers`, `libs/ticonstit`, `apps/tifem`); no ongoing sync.
25
+ - **Portable output** (D-D) — generated artifacts depend on `ti_runtime`, never on
26
+ `mechdsl`, so they can later feed NumerixWeave / MOOSE / MFEM.
27
+ - **Conventions** follow MechDSL `dev/design_docs/07-CONVENTIONS.md`: Voigt order
28
+ `[xx, yy, zz, xy, xz, yz]`, tensorial (unscaled shears), metric `diag(1,1,1,2,2,2)`.
29
+
30
+ ## Operator / solver contract
31
+
32
+ A matrix-free operator is an in-place callable `apply(out, x) -> None` computing
33
+ `out = A @ x` over Taichi fields. Inject it and run a (generated) solver against it:
34
+
35
+ ```python
36
+ ctx = LinearSolveContext()
37
+ ctx.set_operator(my_tangent_matvec) # out = K(u) @ x, matrix-free @ti.kernel
38
+ ctx.set_preconditioner(DiagonalPreconditioner(diag))
39
+ # a generated PCG body calls ctx.apply_A / ctx.apply_preconditioner + vector_ops
40
+ ```
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "ti-runtime"
7
+ version = "0.2.1"
8
+ description = "Neutral Taichi runtime: vector primitives, Tier-1 @ti.func helpers, and solver/operator injection seams for MechDSL-generated code"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.11,<3.14"
12
+ authors = [
13
+ { name = "Shmuel Osovski" },
14
+ ]
15
+ keywords = ["taichi", "fem", "matrix-free", "runtime", "kernels"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Science/Research",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.12",
22
+ ]
23
+
24
+ # Only depends on Taichi. MechDSL-generated artifacts import THIS package
25
+ # (`from ti_runtime import ...`), never `mechdsl` — so generated output stays
26
+ # portable (PlanJune14 D-D). Harvested one-time from NumerixWeave (D-B); MechDSL
27
+ # then diverges freely.
28
+ dependencies = [
29
+ "taichi>=1.7",
30
+ ]
31
+
32
+ [tool.uv]
33
+ # Supply-chain safety: ignore any package version uploaded after this date.
34
+ exclude-newer = "2026-02-20T00:00:00Z"
35
+
36
+ [project.urls]
37
+ Repository = "https://github.com/CEmM2/MechDSL"
38
+
39
+ [tool.hatch.build.targets.wheel]
40
+ packages = ["src/ti_runtime"]
@@ -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
+ ]
@@ -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)
@@ -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
@@ -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,13 @@
1
+ """Shared fixtures for ti-runtime tests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+ import taichi as ti
7
+
8
+
9
+ @pytest.fixture(autouse=True)
10
+ def _ti_cpu():
11
+ """Re-init Taichi on CPU before each test so freshly-allocated fields are valid."""
12
+ ti.init(arch=ti.cpu, default_fp=ti.f64)
13
+ yield
@@ -0,0 +1,46 @@
1
+ """PJ-0 — Hex8 shape functions, gradients, quadrature."""
2
+
3
+ import numpy as np
4
+ import pytest
5
+ import taichi as ti
6
+
7
+ from ti_runtime import hex8
8
+
9
+ pytestmark = pytest.mark.slow # executes Taichi kernels (JIT)
10
+
11
+
12
+ def test_quadrature_tables():
13
+ assert hex8.N_QP == 8
14
+ assert len(hex8.QUAD_POINTS) == 8
15
+ assert hex8.QUAD_WEIGHTS == (1.0,) * 8
16
+ # 2x2x2 Gauss weights sum to the reference cube volume (2^3 = 8).
17
+ assert sum(hex8.QUAD_WEIGHTS) == pytest.approx(8.0)
18
+
19
+
20
+ def test_partition_of_unity_and_gradient_sum():
21
+ N = ti.Vector.field(8, ti.f64, shape=())
22
+ dN = ti.Matrix.field(8, 3, ti.f64, shape=())
23
+
24
+ @ti.kernel
25
+ def run(xi: ti.f64, eta: ti.f64, zeta: ti.f64):
26
+ N[None] = hex8.shape(xi, eta, zeta)
27
+ dN[None] = hex8.shape_grad_natural(xi, eta, zeta)
28
+
29
+ run(0.3, -0.6, 0.2)
30
+ # Shape functions partition unity; natural gradients sum to zero per direction.
31
+ assert N[None].to_numpy().sum() == pytest.approx(1.0, rel=1e-12)
32
+ np.testing.assert_allclose(dN[None].to_numpy().sum(axis=0), 0.0, atol=1e-12)
33
+
34
+
35
+ def test_shape_is_nodal_delta_at_corners():
36
+ N = ti.Vector.field(8, ti.f64, shape=())
37
+
38
+ @ti.kernel
39
+ def run(xi: ti.f64, eta: ti.f64, zeta: ti.f64):
40
+ N[None] = hex8.shape(xi, eta, zeta)
41
+
42
+ for a, (sx, sy, sz) in enumerate(hex8._CORNERS):
43
+ run(float(sx), float(sy), float(sz))
44
+ expected = np.zeros(8)
45
+ expected[a] = 1.0
46
+ np.testing.assert_allclose(N[None].to_numpy(), expected, atol=1e-12)
@@ -0,0 +1,99 @@
1
+ """PJ-0 — injection seams + a mini-CG composition (PJ-1 preview).
2
+
3
+ The composition test is the important one: it injects a matrix-free operator and
4
+ runs a hand-written CG (the shape algo2code will *generate*) using only the seam
5
+ (`apply_A`) and `vector_ops` primitives — proving the seams + primitives compose
6
+ into a working solver with no NumPy in the operator/solve hot path.
7
+ """
8
+
9
+ import numpy as np
10
+ import pytest
11
+ import taichi as ti
12
+
13
+ from ti_runtime import vector_ops as v
14
+ from ti_runtime.seams import (
15
+ DiagonalPreconditioner,
16
+ IdentityPreconditioner,
17
+ LinearSolveContext,
18
+ Operator,
19
+ )
20
+
21
+ pytestmark = pytest.mark.slow # executes Taichi kernels (JIT)
22
+
23
+
24
+ def _vfield(vals: np.ndarray):
25
+ vals = np.ascontiguousarray(vals, dtype=np.float64)
26
+ f = ti.Vector.field(vals.shape[1], ti.f64, shape=vals.shape[0])
27
+ f.from_numpy(vals)
28
+ return f
29
+
30
+
31
+ def test_operator_requires_body():
32
+ op = Operator()
33
+ out = ti.Vector.field(3, ti.f64, shape=2)
34
+ x = ti.Vector.field(3, ti.f64, shape=2)
35
+ with pytest.raises(RuntimeError, match="no body injected"):
36
+ op.apply(out, x)
37
+
38
+
39
+ def test_identity_preconditioner_copies():
40
+ r = _vfield(np.arange(6.0).reshape(2, 3))
41
+ z = ti.Vector.field(3, ti.f64, shape=2)
42
+ IdentityPreconditioner().apply(z, r)
43
+ np.testing.assert_allclose(z.to_numpy(), r.to_numpy())
44
+
45
+
46
+ def test_diagonal_preconditioner_inverts():
47
+ rv = np.array([[2.0, 4.0, 6.0]])
48
+ dv = np.array([[2.0, 4.0, 8.0]])
49
+ r, d = _vfield(rv), _vfield(dv)
50
+ z = ti.Vector.field(3, ti.f64, shape=1)
51
+ DiagonalPreconditioner(d).apply(z, r)
52
+ np.testing.assert_allclose(z.to_numpy(), rv / dv, rtol=1e-12)
53
+
54
+
55
+ # Block-diagonal SPD operator: out[i] = M @ x[i] with a fixed SPD 3x3 M.
56
+ _M = np.array([[4.0, 1.0, 0.0], [1.0, 3.0, 1.0], [0.0, 1.0, 2.0]])
57
+
58
+
59
+ @ti.kernel
60
+ def _apply_M(out: ti.template(), x: ti.template()):
61
+ M = ti.Matrix([[4.0, 1.0, 0.0], [1.0, 3.0, 1.0], [0.0, 1.0, 2.0]], dt=ti.f64)
62
+ for i in out:
63
+ out[i] = M @ x[i]
64
+
65
+
66
+ def _cg(ctx: LinearSolveContext, b, x, tol=1e-12, maxiter=100):
67
+ """Hand-written CG (the body algo2code generates) over the seam + primitives."""
68
+ n = b.shape[0]
69
+ r = ti.Vector.field(3, ti.f64, shape=n)
70
+ p = ti.Vector.field(3, ti.f64, shape=n)
71
+ ap = ti.Vector.field(3, ti.f64, shape=n)
72
+ v.copy(r, b) # x0 = 0 -> r = b - A x0 = b
73
+ v.copy(p, r)
74
+ rz = v.dot(r, r)
75
+ for _ in range(maxiter):
76
+ ctx.apply_A(ap, p)
77
+ alpha = rz / v.dot(p, ap)
78
+ v.axpy(x, alpha, p)
79
+ v.axpy(r, -alpha, ap)
80
+ rz_new = v.dot(r, r)
81
+ if rz_new**0.5 < tol:
82
+ break
83
+ v.xpay(p, rz_new / rz, r) # p = beta*p + r
84
+ rz = rz_new
85
+ return x
86
+
87
+
88
+ def test_cg_composition_solves_spd_system():
89
+ rng = np.random.default_rng(7)
90
+ n = 5
91
+ bv = rng.standard_normal((n, 3))
92
+ b = _vfield(bv)
93
+ x = ti.Vector.field(3, ti.f64, shape=n) # zero initial guess
94
+
95
+ ctx = LinearSolveContext().set_operator(_apply_M)
96
+ _cg(ctx, b, x)
97
+
98
+ expected = np.linalg.solve(_M, bv.T).T # per-node M^{-1} b
99
+ np.testing.assert_allclose(x.to_numpy(), expected, atol=1e-9)
@@ -0,0 +1,76 @@
1
+ """PJ-0 — Tier-1 tensor/kinematics @ti.func helpers."""
2
+
3
+ import numpy as np
4
+ import pytest
5
+ import taichi as ti
6
+
7
+ from ti_runtime import tensor_ti as T
8
+
9
+ pytestmark = pytest.mark.slow # executes Taichi kernels (JIT)
10
+
11
+
12
+ def _mat_in(vals: np.ndarray):
13
+ f = ti.Matrix.field(3, 3, ti.f64, shape=())
14
+ f[None] = ti.Matrix(np.ascontiguousarray(vals, dtype=np.float64).tolist())
15
+ return f
16
+
17
+
18
+ def test_det_and_kinematics():
19
+ rng = np.random.default_rng(0)
20
+ Fv = np.eye(3) + 0.1 * rng.standard_normal((3, 3))
21
+ Fin = _mat_in(Fv)
22
+ out = ti.field(ti.f64, shape=4)
23
+ Cout = ti.Matrix.field(3, 3, ti.f64, shape=())
24
+ Eout = ti.Matrix.field(3, 3, ti.f64, shape=())
25
+
26
+ @ti.kernel
27
+ def run():
28
+ F = Fin[None]
29
+ out[0] = T.det3(F)
30
+ out[1] = T.jacobian(F)
31
+ Cout[None] = T.right_cauchy_green(F)
32
+ Eout[None] = T.green_lagrange(F)
33
+
34
+ run()
35
+ assert out[0] == pytest.approx(np.linalg.det(Fv), rel=1e-12)
36
+ assert out[1] == pytest.approx(np.linalg.det(Fv), rel=1e-12)
37
+ np.testing.assert_allclose(Cout[None].to_numpy(), Fv.T @ Fv, rtol=1e-12)
38
+ np.testing.assert_allclose(
39
+ Eout[None].to_numpy(), 0.5 * (Fv.T @ Fv - np.eye(3)), rtol=1e-12, atol=1e-14
40
+ )
41
+
42
+
43
+ def test_deformation_gradient_from_grad_u():
44
+ gu = np.array([[0.1, 0.0, 0.0], [0.0, -0.05, 0.0], [0.0, 0.0, 0.02]])
45
+ gin = _mat_in(gu)
46
+ Fout = ti.Matrix.field(3, 3, ti.f64, shape=())
47
+
48
+ @ti.kernel
49
+ def run():
50
+ Fout[None] = T.deformation_gradient(gin[None])
51
+
52
+ run()
53
+ np.testing.assert_allclose(Fout[None].to_numpy(), np.eye(3) + gu, rtol=1e-12)
54
+
55
+
56
+ def test_voigt_roundtrip_and_von_mises():
57
+ s = np.array([[10.0, 4.0, 2.0], [4.0, -5.0, 1.0], [2.0, 1.0, 3.0]]) # symmetric
58
+ sin = _mat_in(s)
59
+ voigt = ti.Vector.field(6, ti.f64, shape=())
60
+ back = ti.Matrix.field(3, 3, ti.f64, shape=())
61
+ vm = ti.field(ti.f64, shape=())
62
+
63
+ @ti.kernel
64
+ def run():
65
+ v6 = T.to_voigt(sin[None])
66
+ voigt[None] = v6
67
+ back[None] = T.from_voigt(v6)
68
+ vm[None] = T.von_mises(sin[None])
69
+
70
+ run()
71
+ np.testing.assert_allclose(voigt[None].to_numpy(), [10.0, -5.0, 3.0, 4.0, 2.0, 1.0], rtol=1e-12)
72
+ np.testing.assert_allclose(back[None].to_numpy(), s, rtol=1e-12)
73
+ # reference von Mises: sqrt(3/2 dev:dev)
74
+ dev = s - np.trace(s) / 3.0 * np.eye(3)
75
+ ref = np.sqrt(1.5 * np.sum(dev * dev))
76
+ assert vm[None] == pytest.approx(ref, rel=1e-12)
@@ -0,0 +1,73 @@
1
+ """PJ-0 — vector-primitive kernels."""
2
+
3
+ import numpy as np
4
+ import pytest
5
+ import taichi as ti
6
+
7
+ from ti_runtime import vector_ops as v
8
+
9
+ pytestmark = pytest.mark.slow # executes Taichi kernels (JIT)
10
+
11
+
12
+ def _vfield(vals: np.ndarray):
13
+ vals = np.ascontiguousarray(vals, dtype=np.float64)
14
+ f = ti.Vector.field(vals.shape[1], ti.f64, shape=vals.shape[0])
15
+ f.from_numpy(vals)
16
+ return f
17
+
18
+
19
+ def test_copy():
20
+ rng = np.random.default_rng(0)
21
+ xv = rng.standard_normal((5, 3))
22
+ x = _vfield(xv)
23
+ y = ti.Vector.field(3, ti.f64, shape=5)
24
+ v.copy(y, x)
25
+ np.testing.assert_allclose(y.to_numpy(), xv, rtol=1e-12)
26
+
27
+
28
+ def test_axpy():
29
+ rng = np.random.default_rng(1)
30
+ xv, yv = rng.standard_normal((4, 3)), rng.standard_normal((4, 3))
31
+ x, y = _vfield(xv), _vfield(yv)
32
+ v.axpy(y, 2.5, x)
33
+ np.testing.assert_allclose(y.to_numpy(), yv + 2.5 * xv, rtol=1e-12)
34
+
35
+
36
+ def test_xpay():
37
+ rng = np.random.default_rng(2)
38
+ xv, yv = rng.standard_normal((4, 3)), rng.standard_normal((4, 3))
39
+ x, y = _vfield(xv), _vfield(yv)
40
+ v.xpay(x, -0.5, y)
41
+ np.testing.assert_allclose(x.to_numpy(), -0.5 * xv + yv, rtol=1e-12)
42
+
43
+
44
+ def test_scal():
45
+ rng = np.random.default_rng(3)
46
+ xv = rng.standard_normal((6, 3))
47
+ x = _vfield(xv)
48
+ v.scal(x, 3.0)
49
+ np.testing.assert_allclose(x.to_numpy(), 3.0 * xv, rtol=1e-12)
50
+
51
+
52
+ def test_dot_and_norm():
53
+ rng = np.random.default_rng(4)
54
+ xv, yv = rng.standard_normal((7, 3)), rng.standard_normal((7, 3))
55
+ x, y = _vfield(xv), _vfield(yv)
56
+ assert v.dot(x, y) == pytest.approx(float((xv * yv).sum()), rel=1e-12)
57
+ assert v.norm2(x) == pytest.approx(float(np.linalg.norm(xv)), rel=1e-12)
58
+
59
+
60
+ def test_vec_add():
61
+ rng = np.random.default_rng(5)
62
+ xv, yv = rng.standard_normal((5, 3)), rng.standard_normal((5, 3))
63
+ x, y = _vfield(xv), _vfield(yv)
64
+ out = ti.Vector.field(3, ti.f64, shape=5)
65
+ a, b = 2.0, -0.5
66
+ v.vec_add(out, a, x, b, y)
67
+ np.testing.assert_allclose(out.to_numpy(), a * xv + b * yv, rtol=1e-12)
68
+
69
+
70
+ def test_zero():
71
+ x = _vfield(np.ones((3, 3)))
72
+ v.zero(x)
73
+ np.testing.assert_allclose(x.to_numpy(), 0.0)