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.
- ti_runtime-0.2.1/.gitignore +75 -0
- ti_runtime-0.2.1/LICENSE +21 -0
- ti_runtime-0.2.1/PKG-INFO +58 -0
- ti_runtime-0.2.1/README.md +40 -0
- ti_runtime-0.2.1/pyproject.toml +40 -0
- ti_runtime-0.2.1/src/ti_runtime/__init__.py +49 -0
- ti_runtime-0.2.1/src/ti_runtime/fields.py +44 -0
- ti_runtime-0.2.1/src/ti_runtime/hex8.py +53 -0
- ti_runtime-0.2.1/src/ti_runtime/seams.py +303 -0
- ti_runtime-0.2.1/src/ti_runtime/tensor_ti.py +93 -0
- ti_runtime-0.2.1/src/ti_runtime/vector_ops.py +88 -0
- ti_runtime-0.2.1/tests/conftest.py +13 -0
- ti_runtime-0.2.1/tests/test_hex8.py +46 -0
- ti_runtime-0.2.1/tests/test_seams.py +99 -0
- ti_runtime-0.2.1/tests/test_tensor_ti.py +76 -0
- ti_runtime-0.2.1/tests/test_vector_ops.py +73 -0
|
@@ -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/
|
ti_runtime-0.2.1/LICENSE
ADDED
|
@@ -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)
|