algo2code 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.
@@ -0,0 +1,127 @@
1
+ r"""Canonical Preconditioned Conjugate Gradient (PCG) algorithm in algpseudocode.
2
+
3
+ This module is the **algo2code interface hook** consumed by
4
+ ``mechdsl-core``'s ``Algo2CodePCGSolver`` adapter (Plan recovery task P6-1).
5
+ It exposes:
6
+
7
+ * :data:`PCG_ALGORITHM_LATEX` — the verbatim LaTeX source of the canonical
8
+ PCG algorithm, mirrored from
9
+ ``dev/tasks/recovery_plan_latex_contract/json/P6-1.json`` field
10
+ ``pcg_algorithm_latex.latex``.
11
+ * :func:`get_pcg_algorithm_latex` — accessor returning that text.
12
+
13
+ Runtime invariant
14
+ -----------------
15
+
16
+ This module imports **only stdlib**. ``algo2code`` is the runtime-free
17
+ sibling package of ``mechdsl-core`` (see ``.claude/CLAUDE.md`` /
18
+ ``dev/design_docs/11-ALGO2CODE.md``); nothing here imports — directly or
19
+ transitively — ``mechdsl``. Consumers that want a runnable implementation
20
+ own the translation step.
21
+
22
+ Transpilation status (issue #307, updated)
23
+ ------------------------------------------
24
+
25
+ This LaTeX **now transpiles** via :func:`algo2code.transpile`. The original
26
+ deferral — multi-letter scratch identifiers such as ``pq`` tokenising as
27
+ ``p * q`` — was resolved by the tokenizer's multi-character identifier rule
28
+ (``expr_parser.TOKEN_PATTERNS``), and the compound updates lower correctly via
29
+ the SSA vector-lowering pass (issue #307 F1/F2). The generated ``pcg`` driver is
30
+ runnable Taichi.
31
+
32
+ ``mechdsl-core``'s ``Algo2CodePCGSolver`` is the hand-written, line-by-line
33
+ translation. A numeric comparison (hand-written vs ``transpile`` output, run under
34
+ Taichi) now shows them **bit-identical on every path** — both the converged path
35
+ and the max-iteration-exhausted path — after the for-loop range lowering was
36
+ fixed to emit the inclusive ``range(start, end + 1)`` (issue #307; the generated
37
+ ``\For{$k = 1, ..., \text{maxiter}$}`` previously did one fewer iteration). The
38
+ parity is guarded by
39
+ ``...recovery_plan_latex_contract/test_pcg_transpiler_parity.py``.
40
+
41
+ The hand-translation is **retained** despite the parity, because the generated
42
+ code and the consumer have different runtime models: ``transpile`` emits **Taichi**
43
+ operating on a **dense matrix field** ``A``, whereas the Newton seam
44
+ (``mechdsl.solver.newton``) is **matrix-free numpy** — it passes the tangent as a
45
+ matvec callback and never forms ``A``. Swapping the generated Taichi code in would
46
+ break that matrix-free contract. A literal code replacement would require a numpy
47
+ backend for algo2code that emits a matrix-free PCG matching ``LinearSolverInterface``
48
+ (the numpy backend is currently a stub). Until then the adapter stays, but it is
49
+ no longer a maintenance hazard: the parity test mechanically proves it stays
50
+ faithful to this canonical LaTeX. Behaviour is also pinned by
51
+ ``...recovery_plan_latex_contract/test_p6_1.py``.
52
+ """
53
+
54
+ from __future__ import annotations
55
+
56
+ # ── Canonical algorithm source ───────────────────────────────────────────────
57
+ PCG_ALGORITHM_LATEX: str = r"""% algorithm pcg
58
+ % backend taichi
59
+ % args A:matrix, b:vector, x:vector, apply_M_inv:callable, tol:scalar, maxiter:scalar
60
+
61
+ % type r vector
62
+ % type z vector
63
+ % type p vector
64
+ % type q vector
65
+ % type rho scalar
66
+ % type rho_new scalar
67
+ % type alpha scalar
68
+ % type beta scalar
69
+ % type pq scalar
70
+ % type r0_norm scalar
71
+ % type r_norm scalar
72
+
73
+ \begin{algorithmic}
74
+ \State $r = b - A \cdot x$ % vector
75
+ \State $r_0 = \lVert r \rVert_2$ % scalar
76
+ \If{$r_0 = 0$}
77
+ \Return $x, 0, 0$
78
+ \EndIf
79
+ \State $z = \text{apply\_M\_inv}(r)$ % vector
80
+ \State $p = z$ % vector
81
+ \State $\rho = r^\top z$ % scalar
82
+ \For{$k = 1, 2, \ldots, \text{maxiter}$}
83
+ \State $q = A \cdot p$ % vector
84
+ \State $pq = p^\top q$ % scalar
85
+ \If{$|pq| < 10^{-300}$}
86
+ \State \textbf{break}
87
+ \EndIf
88
+ \State $\alpha = \frac{\rho}{pq}$ % scalar
89
+ \State $x = x + \alpha \, p$ % vector
90
+ \State $r = r - \alpha \, q$ % vector
91
+ \State $r_n = \lVert r \rVert_2$ % scalar
92
+ \If{$r_n < \text{tol} \cdot r_0$}
93
+ \Return $x, k, r_n$
94
+ \EndIf
95
+ \State $z = \text{apply\_M\_inv}(r)$ % vector
96
+ \State $\rho_{\text{new}} = r^\top z$ % scalar
97
+ \State $\beta = \frac{\rho_{\text{new}}}{\rho}$ % scalar
98
+ \State $p = z + \beta \, p$ % vector
99
+ \State $\rho = \rho_{\text{new}}$ % scalar
100
+ \EndFor
101
+ \Return $x, \text{maxiter}, \lVert r \rVert_2$
102
+ \end{algorithmic}"""
103
+
104
+
105
+ def get_pcg_algorithm_latex() -> str:
106
+ """Return the canonical PCG algpseudocode LaTeX source.
107
+
108
+ Consumers (e.g. ``mechdsl.solver.import_adapter.Algo2CodePCGSolver``)
109
+ use the returned string both as the specification artifact for their
110
+ runtime translation and as input to ``algo2code.transpile`` once the
111
+ parser supports the full surface (see *Parser deferral note* in this
112
+ module's docstring).
113
+
114
+ Returns
115
+ -------
116
+ str
117
+ Verbatim LaTeX source of the canonical PCG algorithm, including
118
+ the leading ``% algorithm`` / ``% backend`` / ``% args`` / ``% type``
119
+ directive comments.
120
+ """
121
+ return PCG_ALGORITHM_LATEX
122
+
123
+
124
+ __all__ = [
125
+ "PCG_ALGORITHM_LATEX",
126
+ "get_pcg_algorithm_latex",
127
+ ]
@@ -0,0 +1,90 @@
1
+ r"""Canonical J2 radial-return algorithm (algo2code interface hook).
2
+
3
+ post_recovery_plan Phase 5 (P5-1, P5-3). Mirrors the
4
+ ``algo2code.library.pcg`` pattern but goes one step further:
5
+
6
+ - :data:`RADIAL_RETURN_J2_LATEX` exposes the verbatim algpseudocode.
7
+ - :func:`get_radial_return_j2_latex` returns it (accessor symmetry
8
+ with PCG).
9
+ - :func:`transpile_radial_return_j2` runs ``algo2code.transpile`` on
10
+ the source and returns the emitted Python module text. Phase 5's
11
+ parser fixes (multi-letter scratch identifiers; binary ``/`` in
12
+ assignment LHS) and codegen fix (``n = b.shape[0]`` regression for
13
+ scalar-only algorithms) make this work end-to-end.
14
+
15
+ The mechdsl-core wrapper at ``mechdsl.lib.plasticity`` execs the
16
+ transpiled module text into a namespace at import time and consumes
17
+ the resulting ``radial_return_j2`` callable inside its dispatcher.
18
+
19
+ Runtime invariant
20
+ -----------------
21
+
22
+ This module imports **only stdlib** (and ``algo2code`` siblings). The
23
+ runtime-free contract holds.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ from pathlib import Path
29
+
30
+
31
+ def _load_canonical_source() -> str:
32
+ """Read the canonical algpseudocode source from
33
+ ``dev/algorithms/radial_return_j2.tex``.
34
+
35
+ Walks up from this module to the repo root, then reads the
36
+ file. Single source of truth — the LaTeX file is authoritative.
37
+ """
38
+ here = Path(__file__).resolve()
39
+ for parent in here.parents:
40
+ candidate = parent / "dev" / "algorithms" / "radial_return_j2.tex"
41
+ if candidate.is_file():
42
+ return candidate.read_text(encoding="utf-8")
43
+ raise FileNotFoundError(
44
+ "post_recovery_plan Phase 5 (P5-1): canonical algorithm source "
45
+ "not found. Expected at <repo>/dev/algorithms/radial_return_j2.tex."
46
+ )
47
+
48
+
49
+ RADIAL_RETURN_J2_LATEX: str = _load_canonical_source()
50
+
51
+
52
+ def get_radial_return_j2_latex() -> str:
53
+ """Return the canonical J2 radial-return algpseudocode LaTeX source."""
54
+ return RADIAL_RETURN_J2_LATEX
55
+
56
+
57
+ def transpile_radial_return_j2(backend: str = "taichi") -> str:
58
+ """Transpile the canonical algpseudocode to ``backend`` and return
59
+ the emitted Python module source.
60
+
61
+ post_recovery_plan Phase 5 lifted the deferral that previously
62
+ blocked this round-trip: the algo2code expr_parser now handles
63
+ multi-letter scratch identifiers (``sy``, ``Hp``, ``ap``) and
64
+ binary ``/`` in assignment LHS contexts, and the Taichi codegen
65
+ no longer emits ``n = b.shape[0]`` for scalar-only algorithms.
66
+
67
+ Parameters
68
+ ----------
69
+ backend:
70
+ Target backend name. ``"taichi"`` is the only one consumed by
71
+ ``mechdsl-core`` today.
72
+
73
+ Returns
74
+ -------
75
+ str
76
+ Transpiled Python module source. Pass to ``exec`` with a fresh
77
+ namespace dict to load the ``radial_return_j2`` callable.
78
+ """
79
+ # Local import keeps the runtime-free invariant — algo2code itself
80
+ # has no third-party imports beyond stdlib.
81
+ from algo2code import transpile
82
+
83
+ return transpile(RADIAL_RETURN_J2_LATEX, backend=backend)
84
+
85
+
86
+ __all__ = [
87
+ "RADIAL_RETURN_J2_LATEX",
88
+ "get_radial_return_j2_latex",
89
+ "transpile_radial_return_j2",
90
+ ]
@@ -0,0 +1,89 @@
1
+ r"""J2 kinematic linear-hardening radial-return (algo2code interface hook).
2
+
3
+ constitutive_latex Phase 6 (P6-2). Mirrors
4
+ :mod:`algo2code.library.radial_return_j2` (the isotropic power-law
5
+ variant) for the linear-kinematic (Prager) hardening model:
6
+
7
+ - :data:`RADIAL_RETURN_J2_KINEMATIC_LATEX` exposes the verbatim
8
+ algpseudocode.
9
+ - :func:`get_radial_return_j2_kinematic_latex` returns it.
10
+ - :func:`transpile_radial_return_j2_kinematic` runs
11
+ ``algo2code.transpile`` on the source and returns the emitted module
12
+ text.
13
+
14
+ The scalar source owns only the plastic-multiplier solve. The yield
15
+ surface for kinematic hardening translates rather than expands, so the
16
+ scalar loop operates on the trial RELATIVE-stress equivalent
17
+ ``xi_eq`` (von Mises of ``dev(S) - beta``) with a constant yield radius
18
+ ``sigy0``. The closed-form linear consistency condition
19
+ ``dl = (xi_eq - sigy0) / (3*mu + H_kin)`` is authored as the same
20
+ fixed-iteration loop structure as the isotropic variant for
21
+ transpile-pattern consistency.
22
+
23
+ Runtime invariant
24
+ -----------------
25
+
26
+ This module imports **only stdlib** (and ``algo2code`` siblings). The
27
+ runtime-free contract holds.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ from pathlib import Path
33
+
34
+
35
+ def _load_canonical_source() -> str:
36
+ """Read the canonical algpseudocode source from
37
+ ``dev/algorithms/radial_return_j2_kinematic.tex``.
38
+
39
+ Walks up from this module to the repo root, then reads the file.
40
+ Single source of truth — the LaTeX file is authoritative.
41
+ """
42
+ here = Path(__file__).resolve()
43
+ for parent in here.parents:
44
+ candidate = parent / "dev" / "algorithms" / "radial_return_j2_kinematic.tex"
45
+ if candidate.is_file():
46
+ return candidate.read_text(encoding="utf-8")
47
+ raise FileNotFoundError(
48
+ "constitutive_latex Phase 6 (P6-2): canonical kinematic algorithm "
49
+ "source not found. Expected at "
50
+ "<repo>/dev/algorithms/radial_return_j2_kinematic.tex."
51
+ )
52
+
53
+
54
+ RADIAL_RETURN_J2_KINEMATIC_LATEX: str = _load_canonical_source()
55
+
56
+
57
+ def get_radial_return_j2_kinematic_latex() -> str:
58
+ """Return the canonical J2 kinematic radial-return algpseudocode LaTeX."""
59
+ return RADIAL_RETURN_J2_KINEMATIC_LATEX
60
+
61
+
62
+ def transpile_radial_return_j2_kinematic(backend: str = "taichi") -> str:
63
+ """Transpile the canonical kinematic algpseudocode to ``backend`` and
64
+ return the emitted Python module source.
65
+
66
+ Parameters
67
+ ----------
68
+ backend:
69
+ Target backend name. ``"taichi"`` is the only one consumed by
70
+ ``mechdsl-core`` today.
71
+
72
+ Returns
73
+ -------
74
+ str
75
+ Transpiled Python module source. Pass to ``exec`` with a fresh
76
+ namespace dict to load the ``radial_return_j2_kinematic`` callable.
77
+ """
78
+ # Local import keeps the runtime-free invariant — algo2code itself
79
+ # has no third-party imports beyond stdlib.
80
+ from algo2code import transpile
81
+
82
+ return transpile(RADIAL_RETURN_J2_KINEMATIC_LATEX, backend=backend)
83
+
84
+
85
+ __all__ = [
86
+ "RADIAL_RETURN_J2_KINEMATIC_LATEX",
87
+ "get_radial_return_j2_kinematic_latex",
88
+ "transpile_radial_return_j2_kinematic",
89
+ ]
@@ -0,0 +1,91 @@
1
+ r"""J2 mixed-hardening radial-return (algo2code interface hook).
2
+
3
+ constitutive_latex Phase 6 (P6-3). Mirrors
4
+ :mod:`algo2code.library.radial_return_j2` (isotropic power-law) and
5
+ :mod:`algo2code.library.radial_return_j2_kinematic` (linear kinematic)
6
+ for the MIXED-hardening model — the yield surface BOTH translates
7
+ (back-stress ``beta``, Prager) AND expands (power-law radius
8
+ ``sigma_y(alpha) = sigy0 + K*alpha^n``) simultaneously:
9
+
10
+ - :data:`RADIAL_RETURN_J2_MIXED_LATEX` exposes the verbatim algpseudocode.
11
+ - :func:`get_radial_return_j2_mixed_latex` returns it.
12
+ - :func:`transpile_radial_return_j2_mixed` runs ``algo2code.transpile``
13
+ on the source and returns the emitted module text.
14
+
15
+ The scalar source owns only the plastic-multiplier solve. Yield is on
16
+ the trial RELATIVE-stress equivalent ``xi_eq`` (von Mises of
17
+ ``dev(S) - beta``) against the EXPANDING radius ``sigma_y(alpha)``.
18
+ Because the isotropic part is a nonlinear power law in
19
+ ``alpha = alpha_old + dl``, the consistency condition is nonlinear in
20
+ ``dl`` and is solved by the same scalar NEWTON loop as the isotropic
21
+ variant — but with the kinematic ``(3*mu + H_kin)*dl`` linear term added
22
+ to the residual and the Prager modulus folded into the Newton
23
+ denominator alongside the isotropic slope.
24
+
25
+ Runtime invariant
26
+ -----------------
27
+
28
+ This module imports **only stdlib** (and ``algo2code`` siblings). The
29
+ runtime-free contract holds.
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ from pathlib import Path
35
+
36
+
37
+ def _load_canonical_source() -> str:
38
+ """Read the canonical algpseudocode source from
39
+ ``dev/algorithms/radial_return_j2_mixed.tex``.
40
+
41
+ Walks up from this module to the repo root, then reads the file.
42
+ Single source of truth — the LaTeX file is authoritative.
43
+ """
44
+ here = Path(__file__).resolve()
45
+ for parent in here.parents:
46
+ candidate = parent / "dev" / "algorithms" / "radial_return_j2_mixed.tex"
47
+ if candidate.is_file():
48
+ return candidate.read_text(encoding="utf-8")
49
+ raise FileNotFoundError(
50
+ "constitutive_latex Phase 6 (P6-3): canonical mixed algorithm "
51
+ "source not found. Expected at "
52
+ "<repo>/dev/algorithms/radial_return_j2_mixed.tex."
53
+ )
54
+
55
+
56
+ RADIAL_RETURN_J2_MIXED_LATEX: str = _load_canonical_source()
57
+
58
+
59
+ def get_radial_return_j2_mixed_latex() -> str:
60
+ """Return the canonical J2 mixed radial-return algpseudocode LaTeX."""
61
+ return RADIAL_RETURN_J2_MIXED_LATEX
62
+
63
+
64
+ def transpile_radial_return_j2_mixed(backend: str = "taichi") -> str:
65
+ """Transpile the canonical mixed algpseudocode to ``backend`` and
66
+ return the emitted Python module source.
67
+
68
+ Parameters
69
+ ----------
70
+ backend:
71
+ Target backend name. ``"taichi"`` is the only one consumed by
72
+ ``mechdsl-core`` today.
73
+
74
+ Returns
75
+ -------
76
+ str
77
+ Transpiled Python module source. Pass to ``exec`` with a fresh
78
+ namespace dict to load the ``radial_return_j2_mixed`` callable.
79
+ """
80
+ # Local import keeps the runtime-free invariant — algo2code itself
81
+ # has no third-party imports beyond stdlib.
82
+ from algo2code import transpile
83
+
84
+ return transpile(RADIAL_RETURN_J2_MIXED_LATEX, backend=backend)
85
+
86
+
87
+ __all__ = [
88
+ "RADIAL_RETURN_J2_MIXED_LATEX",
89
+ "get_radial_return_j2_mixed_latex",
90
+ "transpile_radial_return_j2_mixed",
91
+ ]
@@ -0,0 +1,219 @@
1
+ """
2
+ Type inference for algorithm AST nodes.
3
+
4
+ Given type annotations on variables (from % comments or directives),
5
+ propagate types through expressions to resolve:
6
+ - Which operations are scalar (stay in Python scope)
7
+ - Which operations are vector/matrix (become Taichi kernels)
8
+ - What each binary operator actually means:
9
+ * vector * vector → dot product
10
+ * matrix * vector → matvec
11
+ * scalar * vector → axpy
12
+ * scalar / scalar → division
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from .ast_nodes import (
18
+ Algorithm,
19
+ Assign,
20
+ BinOp,
21
+ Branch,
22
+ Expr,
23
+ ForLoop,
24
+ FuncCall,
25
+ Number,
26
+ Return,
27
+ Stmt,
28
+ UnaryOp,
29
+ Var,
30
+ VarType,
31
+ WhileLoop,
32
+ )
33
+
34
+
35
+ class TypeInferrer:
36
+ """
37
+ Walk the AST and annotate every Expr node with its inferred VarType.
38
+ Also resolves BinOp.op to semantically specific operations:
39
+ '*' with (matrix, vector) → 'matvec'
40
+ '*' with (vector, vector) → 'dot' (only when one is transposed)
41
+ '*' with (scalar, vector) → 'scale'
42
+ """
43
+
44
+ def __init__(self, type_annotations: dict[str, VarType]):
45
+ self.types = dict(type_annotations)
46
+
47
+ def infer_algorithm(self, algo: Algorithm):
48
+ """Run type inference on the full algorithm."""
49
+ for name, vtype in algo.args:
50
+ if vtype != VarType.UNKNOWN:
51
+ self.types[name] = vtype
52
+
53
+ self._infer_block(algo.body)
54
+
55
+ def _infer_block(self, stmts: list[Stmt]):
56
+ for stmt in stmts:
57
+ self._infer_stmt(stmt)
58
+
59
+ def _infer_stmt(self, stmt: Stmt):
60
+ if isinstance(stmt, Assign):
61
+ self._infer_expr(stmt.value)
62
+ var_name = stmt.target.display
63
+ if var_name not in self.types:
64
+ self.types[var_name] = stmt.value.inferred_type
65
+ stmt.target.inferred_type = self.types.get(var_name, VarType.UNKNOWN)
66
+
67
+ elif isinstance(stmt, ForLoop):
68
+ self.types[stmt.var] = VarType.SCALAR
69
+ self._infer_block(stmt.body)
70
+
71
+ elif isinstance(stmt, WhileLoop):
72
+ self._infer_expr(stmt.condition)
73
+ self._infer_block(stmt.body)
74
+
75
+ elif isinstance(stmt, Branch):
76
+ self._infer_expr(stmt.condition)
77
+ self._infer_block(stmt.if_body)
78
+ for cond, body in stmt.elif_branches:
79
+ self._infer_expr(cond)
80
+ self._infer_block(body)
81
+ self._infer_block(stmt.else_body)
82
+
83
+ elif isinstance(stmt, Return):
84
+ for v in stmt.values:
85
+ self._infer_expr(v)
86
+
87
+ def _infer_expr(self, expr: Expr) -> VarType:
88
+ """Infer and set the type of an expression, returning the type."""
89
+ if isinstance(expr, Number):
90
+ expr.inferred_type = VarType.SCALAR
91
+ return VarType.SCALAR
92
+
93
+ if isinstance(expr, Var):
94
+ name = expr.display
95
+ t = self.types.get(name, VarType.UNKNOWN)
96
+ # Single-letter uppercase → default to matrix if unknown
97
+ if t == VarType.UNKNOWN and len(expr.name) == 1 and expr.name.isupper():
98
+ t = VarType.MATRIX
99
+ expr.inferred_type = t
100
+ return t
101
+
102
+ if isinstance(expr, UnaryOp):
103
+ inner = self._infer_expr(expr.operand)
104
+ if expr.op == "norm":
105
+ expr.inferred_type = VarType.SCALAR
106
+ elif expr.op == "transpose":
107
+ expr.inferred_type = inner # transpose preserves type
108
+ elif expr.op == "neg" or expr.op == "inverse":
109
+ expr.inferred_type = inner
110
+ else:
111
+ expr.inferred_type = inner
112
+ return expr.inferred_type
113
+
114
+ if isinstance(expr, BinOp):
115
+ lt = self._infer_expr(expr.left)
116
+ rt = self._infer_expr(expr.right)
117
+ expr.inferred_type = self._resolve_binop(expr, lt, rt)
118
+ return expr.inferred_type
119
+
120
+ if isinstance(expr, FuncCall):
121
+ for arg in expr.args:
122
+ self._infer_expr(arg)
123
+ self._infer_expr(expr.func)
124
+ if expr.args:
125
+ expr.inferred_type = expr.args[0].inferred_type
126
+ else:
127
+ expr.inferred_type = VarType.UNKNOWN
128
+ # Check if func is an inverse of a callable
129
+ if isinstance(expr.func, UnaryOp) and expr.func.op == "inverse":
130
+ base = expr.func.operand
131
+ if isinstance(base, Var):
132
+ self.types[base.display] = VarType.CALLABLE
133
+ return expr.inferred_type
134
+
135
+ expr.inferred_type = VarType.UNKNOWN
136
+ return VarType.UNKNOWN
137
+
138
+ def _resolve_binop(self, expr: BinOp, lt: VarType, rt: VarType) -> VarType:
139
+ """Resolve the semantic meaning of a binary operation given operand types."""
140
+ op = expr.op
141
+
142
+ if op in ("+", "-"):
143
+ if lt == VarType.VECTOR or rt == VarType.VECTOR:
144
+ return VarType.VECTOR
145
+ if lt == VarType.MATRIX or rt == VarType.MATRIX:
146
+ return VarType.MATRIX
147
+ return VarType.SCALAR
148
+
149
+ if op == "/":
150
+ return VarType.SCALAR
151
+
152
+ if op == "*":
153
+ return self._resolve_multiply(expr, lt, rt)
154
+
155
+ if op == "pow":
156
+ return lt
157
+
158
+ if op in ("<", ">", "<=", ">=", "==", "!="):
159
+ return VarType.SCALAR
160
+
161
+ return VarType.UNKNOWN
162
+
163
+ def _resolve_multiply(self, expr: BinOp, lt: VarType, rt: VarType) -> VarType:
164
+ """
165
+ Resolve multiplication semantics:
166
+ transposed_vector * vector → dot product (scalar result)
167
+ matrix * vector → matvec (vector result)
168
+ callable * vector → matvec (vector result, matrix-free seam)
169
+ scalar * vector → scale (vector result)
170
+ scalar * scalar → multiply (scalar result)
171
+ """
172
+ left_is_transposed = isinstance(expr.left, UnaryOp) and expr.left.op == "transpose"
173
+
174
+ if left_is_transposed and rt == VarType.VECTOR:
175
+ expr.op = "dot"
176
+ return VarType.SCALAR
177
+
178
+ if left_is_transposed and rt == VarType.MATRIX:
179
+ expr.op = "dot"
180
+ return VarType.VECTOR
181
+
182
+ if lt == VarType.MATRIX and rt == VarType.VECTOR:
183
+ expr.op = "matvec"
184
+ return VarType.VECTOR
185
+
186
+ # Matrix-free operator seam (11-ALGO2CODE §8.3): a `callable` operator A
187
+ # applied to a vector — `A \cdot p` — is a matvec whose body is an
188
+ # injected in-place call A(out, p), not a dense stored-matrix multiply.
189
+ # The backend distinguishes the two by the operand's CALLABLE type.
190
+ if lt == VarType.CALLABLE and rt == VarType.VECTOR:
191
+ expr.op = "matvec"
192
+ return VarType.VECTOR
193
+
194
+ if lt == VarType.MATRIX and rt == VarType.MATRIX:
195
+ expr.op = "matmul"
196
+ return VarType.MATRIX
197
+
198
+ if lt == VarType.SCALAR and rt == VarType.VECTOR:
199
+ expr.op = "scale"
200
+ return VarType.VECTOR
201
+
202
+ if lt == VarType.VECTOR and rt == VarType.SCALAR:
203
+ expr.op = "scale"
204
+ return VarType.VECTOR
205
+
206
+ if lt == VarType.SCALAR and rt == VarType.MATRIX:
207
+ expr.op = "scale"
208
+ return VarType.MATRIX
209
+
210
+ # Default: scalar multiply
211
+ return VarType.SCALAR
212
+
213
+
214
+ def infer_types(algo: Algorithm):
215
+ """Run type inference on an Algorithm AST, mutating nodes in place."""
216
+ inferrer = TypeInferrer(algo.type_annotations)
217
+ inferrer.infer_algorithm(algo)
218
+ # Write back discovered types
219
+ algo.type_annotations = inferrer.types
@@ -0,0 +1,30 @@
1
+ Metadata-Version: 2.4
2
+ Name: algo2code
3
+ Version: 0.2.1
4
+ Summary: Transpile LaTeX algorithm boxes (algpseudocode) to executable Taichi/NumPy/C code
5
+ Project-URL: Repository, https://github.com/CEmM2/MechDSL
6
+ Author: Shmuel Osovski
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: algorithm,code-generation,latex,taichi,transpiler
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
+ Description-Content-Type: text/markdown
17
+
18
+ # algo2code
19
+
20
+ Transpile LaTeX algorithm boxes (`algpseudocode`) to executable code targeting Taichi, NumPy, or C/PETSc.
21
+
22
+ Parses `\begin{algorithmic}...\end{algorithmic}` environments with type-directed code generation.
23
+ Zero runtime dependencies — standard library only.
24
+
25
+ ## Documentation
26
+
27
+ - **User docs:** <https://sosovski.group/MechDSL/algo2code/> — introduction, getting
28
+ started, usage, and examples.
29
+ - **Design spec:** `dev/design_docs/11-ALGO2CODE.md` (authoritative source of truth).
30
+ - See the [monorepo root](../../README.md) for the full project overview.
@@ -0,0 +1,20 @@
1
+ algo2code/__init__.py,sha256=FWaPHkz13pKOXfuA0ahh_uKXGRe80UGCz_1nUs_hpIc,2061
2
+ algo2code/algo_parser.py,sha256=eC0Dl2X7vPojmViTk-3XCXBpySevBeptgI24vVS7KI4,15880
3
+ algo2code/ast_nodes.py,sha256=p7n-ZPoR7SizrcbgIMUWigWrk8kfifBLYfSchlrlQxo,4561
4
+ algo2code/errors.py,sha256=506gr0O_ufY1eqceHicuP8RcGzwNT1TuCGoquJHoA1Q,1098
5
+ algo2code/expr_parser.py,sha256=R5PQ3ey3DQYTTl-xOJ4KvqKSpUbEvhrs82htSae6W0A,21564
6
+ algo2code/type_inference.py,sha256=Pa3bK-QV_IlMx32jh1xx39lj_-dC4DOC-eO9aU1keF4,7596
7
+ algo2code/backends/__init__.py,sha256=hNwHmY9S56cZsMNlP95kFYMz8qojJKqXl7s1aFcXg84,32
8
+ algo2code/backends/c_petsc_codegen.py,sha256=YlRVHC8vwugD1ZGz0UVGMKzMzhZBlAzLBwkbO90FpXM,33
9
+ algo2code/backends/numpy_codegen.py,sha256=WpOm7GkpD51L_kYkoZiVnv-lwn_5ZwtcveU9porEdWY,31
10
+ algo2code/backends/taichi_codegen.py,sha256=ME7BGQDX8nOOYiQFe7Mw0hDYezGU-KVQ9f0bv-nKKYw,35942
11
+ algo2code/library/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ algo2code/library/__init__.py,sha256=HZf5doUkzAjS2D1It3Jc-9fUIRp04BH4wYteaOVPGqc,686
13
+ algo2code/library/pcg.py,sha256=uFQnr3U5a0L3hz_d_S3g9D4pUu8vFthiN55bOy-FBeA,5639
14
+ algo2code/library/radial_return_j2.py,sha256=qW-3avbzW9WQQytX2fZwg7zwQaWhDMYcJCx_B6m9aXM,3136
15
+ algo2code/library/radial_return_j2_kinematic.py,sha256=MaP0IPmrZSafjzH8KuzdBtC2XbKw8tYK7uaLW3M7LQc,3100
16
+ algo2code/library/radial_return_j2_mixed.py,sha256=q9AKkf6Z7uRtMcKTCFWXFI74xHJi0qiMje8hzGWdPl8,3298
17
+ algo2code-0.2.1.dist-info/METADATA,sha256=EOl2z0f4pzkiX-les0_93qdcL-fZ_fHznqmLfGD9uUI,1215
18
+ algo2code-0.2.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
19
+ algo2code-0.2.1.dist-info/licenses/LICENSE,sha256=09AX3cq_TtJwbbZ00zsqmmGL_p_FINJ4l8asH_rhVhk,1071
20
+ algo2code-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