compmech-reference-pack 0.3.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,22 @@
1
+ """compmech_reference_pack — Tier-2 computational-mechanics companion adapter.
2
+
3
+ Bridges AKMS ``akms_learn`` Learning Source Packet (LSP) excerpts to the
4
+ MechDSL Tier-1 ``mechdsl.integration`` façade: it normalises human-authored
5
+ algpseudocode into algo2code-grammar-clean form, then emits executable Python
6
+ and/or compiled-solver summaries with provenance.
7
+
8
+ This package depends on **both** ``akms-learn`` and ``mechdsl-core`` (plus
9
+ ``algo2code``, MechDSL's transpiler backend). It registers the
10
+ ``executable_bridge`` adapter so ``akms_learn`` reports
11
+ ``executable_bridge_adapter = available``.
12
+
13
+ Companion-adapter pattern: MechDSL stays AKMS-unaware; this package is the only
14
+ place that knows about both sides. The public surface is populated across the
15
+ the executable-bridge tier.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ __version__ = "0.1.0"
21
+
22
+ __all__: list[str] = []
@@ -0,0 +1,15 @@
1
+ """compmech_reference_pack adapters — the executable-bridge runner (Tier 2)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from compmech_reference_pack.adapters.mechdsl_runner import (
6
+ MechDSLRunner,
7
+ adapter_registry_overrides,
8
+ available_adapter_registry,
9
+ )
10
+
11
+ __all__ = [
12
+ "MechDSLRunner",
13
+ "adapter_registry_overrides",
14
+ "available_adapter_registry",
15
+ ]
@@ -0,0 +1,321 @@
1
+ """mechdsl_runner.py — MechDSLRunner, the executable_bridge adapter.
2
+
3
+ Bridges a bounded AKMS LSP excerpt to the MechDSL Tier-1 façade
4
+ (``mechdsl.integration``) and assembles a provenance-carrying artefact dict.
5
+
6
+ Contract (the real one, NOT the spec's ``build_artifacts``)::
7
+
8
+ build_executable(excerpt: dict, *, options: dict | None = None) -> dict
9
+
10
+ Behaviour
11
+ ---------
12
+ - **Emit-only by default.** ``build_executable`` normalises the node's
13
+ ``extracted["implementation"]`` (algpseudocode) and calls Tier-1
14
+ ``transpile_algorithm``, and — when a ``% mechanics`` problem source plus an
15
+ energy ``derivation`` are present — Tier-1 ``compile_from_sources``. Both are
16
+ Taichi-free.
17
+ - **Taichi only on demand.** Tier-1 ``verify`` is called **only** when
18
+ ``options["run_verify"]`` is truthy; that is the single Taichi-paying branch.
19
+ - **Never invalidates the packet** (spec 09 §7 rule 5): a transpile/compile
20
+ failure is captured into ``warnings`` and reported as ``status="error"``
21
+ only when the caller explicitly required executable output
22
+ (``options["require_executable"]``); otherwise ``status`` stays ``"ok"``.
23
+
24
+ No-mutation invariant: this adapter never writes to any AKMS path.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import re
30
+ from typing import Any
31
+
32
+ from akms_learn.adapters.status import AdapterStatus, adapter_registry
33
+
34
+ ADAPTER_ID = "compmech.mechdsl_runner"
35
+
36
+ __all__ = [
37
+ "ADAPTER_ID",
38
+ "MechDSLRunner",
39
+ "adapter_registry_overrides",
40
+ "available_adapter_registry",
41
+ ]
42
+
43
+
44
+ def adapter_registry_overrides() -> dict[str, AdapterStatus]:
45
+ """Registry override that marks the executable bridge available."""
46
+ return {"executable_bridge_adapter": AdapterStatus.available}
47
+
48
+
49
+ def available_adapter_registry() -> dict[str, AdapterStatus]:
50
+ """Return the akms_learn adapter registry with this adapter marked available."""
51
+ return adapter_registry(adapter_registry_overrides())
52
+
53
+
54
+ def _algo_name_from_node_id(node_id: str) -> str:
55
+ """Derive a safe algo2code algorithm name from a node id."""
56
+ tail = node_id.rsplit(".", 1)[-1].rsplit("/", 1)[-1]
57
+ cleaned = re.sub(r"[^0-9A-Za-z_]", "_", tail).strip("_")
58
+ return cleaned or "algorithm"
59
+
60
+
61
+ class MechDSLRunner:
62
+ """ExecutableBridgeAdapter implementation backed by MechDSL Tier-1.
63
+
64
+ Satisfies ``akms_learn.adapters.protocols.ExecutableBridgeAdapter`` via
65
+ structural subtyping (the protocol is ``@runtime_checkable``).
66
+ """
67
+
68
+ adapter_id = ADAPTER_ID
69
+
70
+ # -- protocol surface ---------------------------------------------------
71
+
72
+ def build_executable(
73
+ self, excerpt: dict[str, Any], *, options: dict[str, Any] | None = None
74
+ ) -> dict[str, Any]:
75
+ """Build an executable artefact from a bounded LSP excerpt.
76
+
77
+ Returns the 8-key contract dict ``{adapter, status, emitted_source,
78
+ transpiled, normalized_input, provenance, vv_plan, warnings}``. When
79
+ ``options["run_verify"]`` is truthy a 9th key ``verification`` (the
80
+ Tier-1 ``verify`` result) is added — that opt-in path is the only one
81
+ that pays the Taichi cost. Never raises on a malformed excerpt.
82
+ """
83
+ options = options or {}
84
+ run_verify = bool(options.get("run_verify", False))
85
+ require_executable = bool(options.get("require_executable", False))
86
+
87
+ node = self._select_node(excerpt)
88
+ node_id = str(node.get("node_id") or excerpt.get("node_id") or "<unknown>")
89
+ extracted = node.get("extracted")
90
+ if not isinstance(extracted, dict):
91
+ extracted = {}
92
+ implementation = extracted.get("implementation")
93
+ derivation = extracted.get("derivation")
94
+ problem = extracted.get("problem")
95
+
96
+ warnings: list[str] = []
97
+ status = "ok"
98
+ normalized_input: str | None = None
99
+ transpiled: dict[str, Any] | None = None
100
+ emitted_source: str | None = None
101
+
102
+ # --- implementation -> normalize -> Tier-1 transpile (Taichi-free) ---
103
+ if implementation:
104
+ from compmech_reference_pack.normalize import normalize
105
+
106
+ norm = normalize(
107
+ implementation, algorithm_name=_algo_name_from_node_id(node_id)
108
+ )
109
+ normalized_input = norm.normalized
110
+ warnings.extend(norm.warnings)
111
+ if norm.algorithmic_block_found:
112
+ try:
113
+ from mechdsl.integration import transpile_algorithm
114
+ except ImportError as exc:
115
+ warnings.append(
116
+ "MechDSL backend unavailable; install "
117
+ '"compmech-reference-pack[mechdsl]" '
118
+ f"({type(exc).__name__}: {exc})"
119
+ )
120
+ if require_executable:
121
+ status = "error"
122
+ else:
123
+ try:
124
+ transpiled = transpile_algorithm(
125
+ norm.normalized, backend="taichi"
126
+ )
127
+ if not transpiled.get("valid_python", False):
128
+ warnings.append(
129
+ "transpiled code did not compile as valid Python"
130
+ )
131
+ if require_executable:
132
+ status = "error"
133
+ except Exception as exc:
134
+ warnings.append(
135
+ f"transpile failed: {type(exc).__name__}: {exc}"
136
+ )
137
+ if require_executable:
138
+ status = "error"
139
+
140
+ # --- derivation (+ problem) -> Tier-1 compile (Taichi-free) ----------
141
+ if derivation and problem:
142
+ try:
143
+ from mechdsl.integration import compile_from_sources
144
+ except ImportError as exc:
145
+ warnings.append(
146
+ "MechDSL backend unavailable; install "
147
+ '"compmech-reference-pack[mechdsl]" '
148
+ f"({type(exc).__name__}: {exc})"
149
+ )
150
+ if require_executable:
151
+ status = "error"
152
+ else:
153
+ try:
154
+ compiled = compile_from_sources(
155
+ problem_source=problem, energy_source=derivation
156
+ )
157
+ emitted_source = compiled["emitted_source"]
158
+ except Exception as exc:
159
+ warnings.append(
160
+ f"compile_from_sources failed: {type(exc).__name__}: {exc}"
161
+ )
162
+ if require_executable:
163
+ status = "error"
164
+ elif derivation and not problem:
165
+ warnings.append(
166
+ "node carries a derivation but no '% mechanics' problem source; "
167
+ "skipping compile_from_sources (provide extracted['problem'] to compile)"
168
+ )
169
+
170
+ if implementation is None and derivation is None:
171
+ status = "error"
172
+ warnings.append(
173
+ f"excerpt node {node_id!r} carries neither an implementation nor a "
174
+ "derivation; nothing to build"
175
+ )
176
+
177
+ result: dict[str, Any] = {
178
+ "adapter": ADAPTER_ID,
179
+ "status": status,
180
+ "emitted_source": emitted_source,
181
+ "transpiled": transpiled,
182
+ "normalized_input": normalized_input,
183
+ "provenance": {
184
+ node_id: {
185
+ "sections": self._sections(node),
186
+ "references": self._references(node_id, node, excerpt),
187
+ }
188
+ },
189
+ "vv_plan": self._vv_plan(node_id, node, excerpt, options),
190
+ "warnings": warnings,
191
+ }
192
+
193
+ # --- the only Taichi-paying branch ----------------------------------
194
+ if run_verify:
195
+ result["verification"] = self._run_verify(options, warnings)
196
+
197
+ return result
198
+
199
+ # -- extra surface (per plan: status() / can_handle()) ------------------
200
+
201
+ def status(self) -> dict[str, Any]:
202
+ """Lightweight, Taichi-free availability descriptor for status routes."""
203
+ return {
204
+ "adapter": ADAPTER_ID,
205
+ "available": True,
206
+ "companion_role": "executable_bridge",
207
+ "taichi_required_for": ["verify"],
208
+ }
209
+
210
+ def can_handle(self, excerpt: dict[str, Any]) -> bool:
211
+ """True when the excerpt has a node carrying implementation/derivation.
212
+
213
+ Total: returns ``False`` (never raises) for any malformed excerpt
214
+ shape — missing ``nodes``, non-dict nodes, non-dict ``extracted``.
215
+ """
216
+ node = self._select_node(excerpt)
217
+ extracted = node.get("extracted")
218
+ if not isinstance(extracted, dict):
219
+ return False
220
+ return bool(extracted.get("implementation") or extracted.get("derivation"))
221
+
222
+ # -- internals ----------------------------------------------------------
223
+
224
+ @staticmethod
225
+ def _select_node(excerpt: dict[str, Any]) -> dict[str, Any]:
226
+ """Pick the node dict to build from.
227
+
228
+ Accepts a node-shaped excerpt directly, a ``{"nodes": [...]}`` excerpt
229
+ (first node carrying implementation/derivation, else the first node),
230
+ or any dict (treated as the node).
231
+ """
232
+ nodes = excerpt.get("nodes")
233
+ if isinstance(nodes, list):
234
+ dict_nodes = [n for n in nodes if isinstance(n, dict)]
235
+ for node in dict_nodes:
236
+ extracted = node.get("extracted")
237
+ if isinstance(extracted, dict) and (
238
+ extracted.get("implementation") or extracted.get("derivation")
239
+ ):
240
+ return node
241
+ # No build-bearing node — return the first dict node, or {} so
242
+ # callers never touch a non-dict (None, str, …).
243
+ return dict_nodes[0] if dict_nodes else {}
244
+ return excerpt
245
+
246
+ @staticmethod
247
+ def _sections(node: dict[str, Any]) -> Any:
248
+ return node.get("included_sections") or node.get("sections") or []
249
+
250
+ @staticmethod
251
+ def _references(
252
+ node_id: str, node: dict[str, Any], excerpt: dict[str, Any]
253
+ ) -> list[Any]:
254
+ """Collect references attributable to this node.
255
+
256
+ Prefers node-local references; otherwise filters packet-level
257
+ references by ``source_node_ids`` membership (falling back to all).
258
+ """
259
+ node_refs = node.get("references")
260
+ if isinstance(node_refs, list) and node_refs:
261
+ return list(node_refs)
262
+
263
+ packet_refs = excerpt.get("references")
264
+ if not isinstance(packet_refs, list):
265
+ return []
266
+ scoped = [
267
+ ref
268
+ for ref in packet_refs
269
+ if isinstance(ref, dict) and node_id in (ref.get("source_node_ids") or [])
270
+ ]
271
+ return scoped or list(packet_refs)
272
+
273
+ @classmethod
274
+ def _vv_plan(
275
+ cls,
276
+ node_id: str,
277
+ node: dict[str, Any],
278
+ excerpt: dict[str, Any],
279
+ options: dict[str, Any],
280
+ ) -> dict[str, Any]:
281
+ """Assemble a verification-and-validation plan {benchmark, citation}.
282
+
283
+ The citation is drawn from the same node-scoped reference list used for
284
+ provenance (via :meth:`_references`), so the two never disagree.
285
+ """
286
+ extracted = node.get("extracted")
287
+ if not isinstance(extracted, dict):
288
+ extracted = {}
289
+ benchmark = (
290
+ options.get("benchmark")
291
+ or extracted.get("benchmark")
292
+ or excerpt.get("benchmark")
293
+ )
294
+ citation: Any = None
295
+ for ref in cls._references(node_id, node, excerpt):
296
+ if isinstance(ref, dict):
297
+ citation = ref.get("citation") or ref.get("title")
298
+ if citation:
299
+ break
300
+ return {"benchmark": benchmark, "citation": citation}
301
+
302
+ @staticmethod
303
+ def _run_verify(options: dict[str, Any], warnings: list[str]) -> dict[str, Any]:
304
+ """Run the single Taichi-paying Tier-1 verify branch."""
305
+ kind = options.get("verify_kind", "patch_test")
306
+ params = options.get("verify_params", {})
307
+
308
+ try:
309
+ from mechdsl.integration import verify
310
+
311
+ return verify(kind, params)
312
+ except ImportError as exc:
313
+ warnings.append(
314
+ "MechDSL backend unavailable; install "
315
+ '"compmech-reference-pack[mechdsl]" '
316
+ f"({type(exc).__name__}: {exc})"
317
+ )
318
+ return {"kind": kind, "passed": False, "details": {"error": str(exc)}}
319
+ except Exception as exc:
320
+ warnings.append(f"verify failed: {type(exc).__name__}: {exc}")
321
+ return {"kind": kind, "passed": False, "details": {"error": str(exc)}}
@@ -0,0 +1,67 @@
1
+ """Capability declarations for compmech_reference_pack.
2
+
3
+ `CEmM2/Logic-Loom <https://github.com/CEmM2/Logic-Loom>`_ imports this package
4
+ from its ``GET /api/mechdsl/status`` route and reads the capability surface to
5
+ decide whether to expose the MechDSL gate.
6
+ This module is deliberately import-light and **Taichi-free**: it declares
7
+ *what* the package offers; it never runs a solve or initialises Taichi.
8
+
9
+ Two capabilities are declared:
10
+
11
+ - ``executable_bridge_adapter`` — the package registers ``MechDSLRunner`` as
12
+ the ``executable_bridge`` adapter (``akms_learn`` reports it available).
13
+ - ``code_mirror_provenance`` — ``build_executable`` attaches per-node
14
+ provenance (source sections + references) to every emitted artefact.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from compmech_reference_pack import __version__
20
+
21
+ __all__ = [
22
+ "ADAPTER_ID",
23
+ "CODE_MIRROR_PROVENANCE",
24
+ "EXECUTABLE_BRIDGE_ADAPTER",
25
+ "capabilities",
26
+ ]
27
+
28
+ #: Stable id of the executable-bridge adapter this package registers.
29
+ ADAPTER_ID = "compmech.mechdsl_runner"
30
+
31
+ #: Capability keys (also the akms_learn adapter-registry capability names).
32
+ EXECUTABLE_BRIDGE_ADAPTER = "executable_bridge_adapter"
33
+ CODE_MIRROR_PROVENANCE = "code_mirror_provenance"
34
+
35
+
36
+ def capabilities() -> dict:
37
+ """Return the machine-readable capability surface for status discovery.
38
+
39
+ Shape consumed by Logic-Loom's status route::
40
+
41
+ {package, version, domain_pack, adapter_id, capabilities: {...}}
42
+
43
+ The dict is JSON-serialisable and contains no runtime objects.
44
+ """
45
+ return {
46
+ "package": "compmech_reference_pack",
47
+ "version": __version__,
48
+ "domain_pack": "compmech.reference",
49
+ "adapter_id": ADAPTER_ID,
50
+ "capabilities": {
51
+ EXECUTABLE_BRIDGE_ADAPTER: {
52
+ "available": True,
53
+ "adapter_id": ADAPTER_ID,
54
+ "companion_role": "executable_bridge",
55
+ "actions": ["emit", "transpile", "verify"],
56
+ # Mirrors the Tier-1 contract: only ``verify`` pays Taichi.
57
+ "taichi_required_for": ["verify"],
58
+ },
59
+ CODE_MIRROR_PROVENANCE: {
60
+ "available": True,
61
+ "source_pack_id": "compmech.mechdsl",
62
+ "repo": "CEmM2/MechDSL",
63
+ # build_executable attaches {node_id: {sections, references}}.
64
+ "provenance_keys": ["sections", "references"],
65
+ },
66
+ },
67
+ }
@@ -0,0 +1,49 @@
1
+ domain_pack_schema: akms-learn-domain-pack/v1
2
+ domain_id: compmech
3
+ pack_id: compmech.reference
4
+ name: Computational Mechanics Reference Pack
5
+ version: "0.1.0"
6
+ status: reference
7
+ summary: >-
8
+ Reference computational-mechanics domain pack for AKMS Learn. Promoted from
9
+ the AKMS_learn test fixture by compmech_reference_pack (Tier-2 executable
10
+ bridge); the MechDSL companion is now an available executable runner.
11
+ compatibility:
12
+ akms_learn_schema_min: learn/v0.1
13
+ akms_learn_schema_max: learn/v0.1
14
+ roots:
15
+ nodes: nodes/compmech
16
+ code_mirror: code-mirror/compmech
17
+ examples: learning_examples/compmech
18
+ bundles: lesson_bundles/compmech
19
+ capabilities:
20
+ static_markdown: true
21
+ static_notebook: true
22
+ executable_notebook: false
23
+ external_runner: true
24
+ code_mirror: true
25
+ # `constkit` and `symbolic_fem_workbench` stay `planned`: their adapters are
26
+ # not written. Their source is public — both live under
27
+ # https://github.com/SOSOVSKI/Teaching-materials — so `planned` here describes
28
+ # the adapter, not the availability of the material.
29
+ companion_roles:
30
+ - id: constkit
31
+ package_name: compmech.constkit
32
+ runtime_hint: optional
33
+ capability_status: planned
34
+ - id: symbolic_fem_workbench
35
+ package_name: compmech.symbolic_fem_workbench
36
+ runtime_hint: optional
37
+ capability_status: planned
38
+ - id: mechdsl
39
+ package_name: compmech.mechdsl
40
+ runtime_hint: required
41
+ capability_status: available
42
+ source_packs:
43
+ - source_packs/constkit.yaml
44
+ - source_packs/symbolic_fem_workbench.yaml
45
+ - source_packs/mechdsl.yaml
46
+ provenance:
47
+ source_repos: []
48
+ generated_vault_ref: null
49
+ zotero_collection_refs: []
@@ -0,0 +1,28 @@
1
+ source_pack_schema: akms-learn-source-pack/v1
2
+ source_pack_id: compmech.constkit
3
+ name: ConstKit Concept Helpers
4
+ version: "0.1.0"
5
+ companion_role: concept_kit
6
+ # The source is public and obtainable; the adapter that would consume it is
7
+ # not written yet. `planned` describes the adapter, not the availability of
8
+ # the material.
9
+ capability_status: planned
10
+ repo:
11
+ kind: github
12
+ name: SOSOVSKI/Teaching-materials
13
+ ref: main
14
+ roots:
15
+ docs: packages/constkit/docs
16
+ code: packages/constkit/src/constkit
17
+ tests: packages/constkit/tests
18
+ capabilities:
19
+ static_code_mirror: true
20
+ static_lessons: true
21
+ notebook_cells: false
22
+ executable_runner: false
23
+ runtime:
24
+ required_python: ">=3.12,<3.13"
25
+ dependencies: []
26
+ adapter:
27
+ adapter_id: compmech.constkit_adapter
28
+ status: planned
@@ -0,0 +1,26 @@
1
+ source_pack_schema: akms-learn-source-pack/v1
2
+ source_pack_id: compmech.mechdsl
3
+ name: MechDSL Executable Bridge
4
+ version: "0.1.0"
5
+ companion_role: executable_bridge
6
+ capability_status: available
7
+ repo:
8
+ kind: github
9
+ name: CEmM2/MechDSL
10
+ ref: main
11
+ roots:
12
+ docs: dev/design_docs
13
+ code: packages/mechdsl-core/src/mechdsl
14
+ examples: dev/examples
15
+ tests: packages/mechdsl-core/tests
16
+ capabilities:
17
+ static_code_mirror: true
18
+ static_lessons: true
19
+ notebook_cells: false
20
+ executable_runner: true
21
+ runtime:
22
+ required_python: ">=3.12,<3.13"
23
+ dependencies: []
24
+ adapter:
25
+ adapter_id: compmech.mechdsl_runner
26
+ status: available
@@ -0,0 +1,29 @@
1
+ source_pack_schema: akms-learn-source-pack/v1
2
+ source_pack_id: compmech.symbolic_fem_workbench
3
+ name: Symbolic FEM Workbench
4
+ version: "0.1.0"
5
+ companion_role: pedagogical_workbench
6
+ # The source is public and obtainable; the adapter that would consume it is
7
+ # not written yet. `planned` describes the adapter, not the availability of
8
+ # the material.
9
+ capability_status: planned
10
+ repo:
11
+ kind: github
12
+ name: SOSOVSKI/Teaching-materials
13
+ ref: main
14
+ roots:
15
+ docs: packages/symbolic-fem-workbench/docs
16
+ code: packages/symbolic-fem-workbench/src/symbolic_fem_workbench
17
+ examples: packages/symbolic-fem-workbench/notebooks
18
+ tests: packages/symbolic-fem-workbench/tests
19
+ capabilities:
20
+ static_code_mirror: true
21
+ static_lessons: true
22
+ notebook_cells: true
23
+ executable_runner: false
24
+ runtime:
25
+ required_python: ">=3.12,<3.13"
26
+ dependencies: []
27
+ adapter:
28
+ adapter_id: compmech.symbolic_fem_workbench_adapter
29
+ status: planned
@@ -0,0 +1,192 @@
1
+ """normalize.py — rewrite human-authored algpseudocode into algo2code-clean form.
2
+
3
+ The Tier-2 adapter's pre-processing core. An LSP node's
4
+ ``extracted["implementation"]`` is human/markdown-authored algpseudocode;
5
+ ``algo2code.transpile`` expects an ``algorithmic`` block optionally preceded by
6
+ ``% algorithm/backend/args/type`` directive lines.
7
+
8
+ What this pass actually does
9
+ ----------------------------
10
+ Empirically, MechDSL's current ``algo2code`` expression grammar already
11
+ subsumes the rewrites the original plan anticipated (``\\sqrt{3/2}``,
12
+ ``\\frac{a}{b}``, ``\\cdot``, ``\\left/\\right``, Greek letters, ``^{...}``,
13
+ and ``\\If/\\Else/\\ElsIf/\\While/\\For`` control flow all transpile as-is).
14
+ So the genuinely-useful work here is:
15
+
16
+ 1. **Extract** the ``\\begin{algorithmic} … \\end{algorithmic}`` block out of
17
+ surrounding markdown (prose, code fences) so the source is bounded and the
18
+ *absence* of an algorithm is detectable (``algo2code`` silently emits an
19
+ empty function otherwise).
20
+ 2. **Synthesise directives** — prepend ``% algorithm <name>`` / ``% backend
21
+ taichi`` when the author didn't, so the emitted function is meaningfully
22
+ named instead of the ``algo2code`` default ``algorithm``; author-supplied
23
+ directives are preserved.
24
+ 3. **Surface parser failures as structured warnings, never raise** — per spec
25
+ 09 §7 rule 5 (a failed adapter must not invalidate the packet unless
26
+ executable output was explicitly required).
27
+
28
+ The public deliverable is :func:`normalize_algpseudocode` (``str -> str``);
29
+ :func:`normalize` wraps it with a trial parse and the warning report the
30
+ adapter consumes.
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import re
36
+ from dataclasses import dataclass
37
+
38
+ __all__ = [
39
+ "NormalizationResult",
40
+ "extract_algorithmic_block",
41
+ "normalize",
42
+ "normalize_algpseudocode",
43
+ ]
44
+
45
+ _ALGORITHMIC_RE = re.compile(
46
+ r"\\begin\{algorithmic\}(?:\[[^\]]*\])?(?P<body>.*?)\\end\{algorithmic\}",
47
+ re.DOTALL,
48
+ )
49
+ _FENCE_RE = re.compile(r"^[ \t]*```[^\n]*$", re.MULTILINE)
50
+ # ":=" is the only assignment arrow algo2code does not accept; "\gets" and
51
+ # "\leftarrow" parse natively but we canonicalise all three to "=".
52
+ _ARROW_RE = re.compile(r":=|\\gets|\\leftarrow")
53
+ _DIRECTIVE_KEYWORDS = frozenset({"algorithm", "backend", "args", "type"})
54
+
55
+
56
+ @dataclass(frozen=True)
57
+ class NormalizationResult:
58
+ """Outcome of normalising one implementation excerpt.
59
+
60
+ Attributes
61
+ ----------
62
+ normalized:
63
+ algo2code-ready source (synthesised directives + algorithmic block).
64
+ algorithmic_block_found:
65
+ Whether an ``\\begin{algorithmic}`` block was present in the input.
66
+ parses:
67
+ Whether ``algo2code.parse_algorithm`` accepted the normalised source.
68
+ warnings:
69
+ Structured, human-readable warnings (missing block, parse errors).
70
+ Never empty when ``parses`` is ``False`` or the block was absent.
71
+ """
72
+
73
+ normalized: str
74
+ algorithmic_block_found: bool
75
+ parses: bool
76
+ warnings: tuple[str, ...] = ()
77
+
78
+
79
+ def extract_algorithmic_block(impl_md: str) -> str | None:
80
+ """Return the inner body of the first ``algorithmic`` block, or ``None``.
81
+
82
+ Markdown code fences are stripped first so a fenced ``\\begin{algorithmic}``
83
+ is still found. The returned body excludes the ``\\begin``/``\\end`` markers.
84
+ """
85
+ if not impl_md:
86
+ return None
87
+ text = _FENCE_RE.sub("", impl_md)
88
+ match = _ALGORITHMIC_RE.search(text)
89
+ if match is None:
90
+ return None
91
+ return match.group("body").strip("\n")
92
+
93
+
94
+ def _sanitize_identifier(name: str) -> str:
95
+ """Coerce an arbitrary string into a safe algo2code algorithm identifier."""
96
+ cleaned = re.sub(r"[^0-9A-Za-z_]", "_", name).strip("_")
97
+ if not cleaned:
98
+ return "algorithm"
99
+ if cleaned[0].isdigit():
100
+ cleaned = f"a_{cleaned}"
101
+ return cleaned
102
+
103
+
104
+ def _collect_directives(impl_md: str) -> list[str]:
105
+ """Collect author-supplied ``% algorithm/backend/args/type`` directives.
106
+
107
+ Only lines *before* the algorithmic block are considered, so ``%`` lines
108
+ that happen to live inside the block are not mistaken for directives.
109
+ """
110
+ head = impl_md
111
+ begin = impl_md.find(r"\begin{algorithmic}")
112
+ if begin != -1:
113
+ head = impl_md[:begin]
114
+ directives: list[str] = []
115
+ for raw in head.splitlines():
116
+ stripped = raw.strip()
117
+ if not stripped.startswith("%"):
118
+ continue
119
+ content = stripped[1:].strip()
120
+ first = content.split(None, 1)[0] if content else ""
121
+ if first in _DIRECTIVE_KEYWORDS:
122
+ directives.append(content)
123
+ return directives
124
+
125
+
126
+ def normalize_algpseudocode(impl_md: str, *, algorithm_name: str | None = None) -> str:
127
+ """Rewrite a human-authored implementation excerpt into algo2code source.
128
+
129
+ Extracts the ``algorithmic`` block, canonicalises assignment arrows, and
130
+ prepends synthesised ``% algorithm``/``% backend`` directives (preserving
131
+ any the author already supplied). Never raises: a missing block yields an
132
+ empty algorithmic shell, which :func:`normalize` flags as a warning.
133
+ """
134
+ body = extract_algorithmic_block(impl_md)
135
+ if body is None:
136
+ body = ""
137
+ body = _ARROW_RE.sub("=", body)
138
+
139
+ existing = _collect_directives(impl_md)
140
+ # Match both "algorithm <name>" and a bare "algorithm" directive so a
141
+ # name-less author directive doesn't get a synthesised one appended too.
142
+ has_name = any(d == "algorithm" or d.startswith("algorithm ") for d in existing)
143
+ has_backend = any(d == "backend" or d.startswith("backend ") for d in existing)
144
+
145
+ lines: list[str] = []
146
+ if not has_name:
147
+ lines.append(
148
+ f"% algorithm {_sanitize_identifier(algorithm_name or 'algorithm')}"
149
+ )
150
+ if not has_backend:
151
+ lines.append("% backend taichi")
152
+ lines.extend(f"% {d}" for d in existing)
153
+ lines.append(r"\begin{algorithmic}")
154
+ if body:
155
+ lines.append(body)
156
+ lines.append(r"\end{algorithmic}")
157
+ return "\n".join(lines)
158
+
159
+
160
+ def normalize(
161
+ impl_md: str, *, algorithm_name: str | None = None
162
+ ) -> NormalizationResult:
163
+ """Normalise an excerpt and report parse status + structured warnings.
164
+
165
+ Wraps :func:`normalize_algpseudocode` with a trial ``algo2code`` parse so
166
+ the adapter can attach warnings without ever crashing the packet.
167
+ """
168
+ found = extract_algorithmic_block(impl_md) is not None
169
+ normalized = normalize_algpseudocode(impl_md, algorithm_name=algorithm_name)
170
+
171
+ warnings: list[str] = []
172
+ if not found:
173
+ warnings.append(
174
+ r"no \begin{algorithmic} block found in implementation excerpt; "
175
+ "emitted source will be an empty function"
176
+ )
177
+
178
+ parses = False
179
+ try:
180
+ from algo2code import parse_algorithm
181
+
182
+ parse_algorithm(normalized)
183
+ parses = True
184
+ except Exception as exc:
185
+ warnings.append(f"algo2code parse failed: {type(exc).__name__}: {exc}")
186
+
187
+ return NormalizationResult(
188
+ normalized=normalized,
189
+ algorithmic_block_found=found,
190
+ parses=parses,
191
+ warnings=tuple(warnings),
192
+ )
@@ -0,0 +1,30 @@
1
+ """Filesystem accessors for the promoted compmech domain pack.
2
+
3
+ The domain pack and its source packs ship as package data under
4
+ ``compmech_reference_pack/domain_pack/``. These helpers return concrete
5
+ ``Path`` objects so the ``akms_learn`` descriptor loaders — and the
6
+ `CEmM2/Logic-Loom <https://github.com/CEmM2/Logic-Loom>`_ status route — can
7
+ read them without hardcoding the install layout.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from importlib import resources
13
+ from pathlib import Path
14
+
15
+ __all__ = ["domain_pack_dir", "domain_pack_path", "source_pack_path"]
16
+
17
+
18
+ def domain_pack_dir() -> Path:
19
+ """Absolute path to the packaged ``domain_pack/`` data directory."""
20
+ return Path(str(resources.files("compmech_reference_pack") / "domain_pack"))
21
+
22
+
23
+ def domain_pack_path() -> Path:
24
+ """Absolute path to the promoted ``domain_pack.yaml``."""
25
+ return domain_pack_dir() / "domain_pack.yaml"
26
+
27
+
28
+ def source_pack_path(name: str) -> Path:
29
+ """Absolute path to a source-pack yaml by stem (e.g. ``"mechdsl"``)."""
30
+ return domain_pack_dir() / "source_packs" / f"{name}.yaml"
@@ -0,0 +1,74 @@
1
+ Metadata-Version: 2.4
2
+ Name: compmech-reference-pack
3
+ Version: 0.3.1
4
+ Summary: Computational-mechanics companion adapter — bridges AKMS LSP excerpts to the MechDSL executable backend.
5
+ Requires-Python: <3.14,>=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: akms-learn
8
+ Provides-Extra: mechdsl
9
+ Requires-Dist: mechdsl-core; extra == "mechdsl"
10
+ Requires-Dist: algo2code; extra == "mechdsl"
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=9.0.2; extra == "dev"
13
+ Requires-Dist: compmech-reference-pack[mechdsl]; extra == "dev"
14
+
15
+ # compmech-reference-pack
16
+
17
+ The computational-mechanics companion adapter for
18
+ [AKMS](https://github.com/CEmM2/AKMS). It bridges `akms-learn` Learning Source
19
+ Packet excerpts to the [MechDSL](https://github.com/CEmM2/MechDSL) executable
20
+ backend, so an algorithm described in a knowledge node can be transpiled and
21
+ compiled rather than only read.
22
+
23
+ It ships the `compmech` domain pack — the descriptor and source packs that tell
24
+ `akms-learn` which companions exist and what each can do.
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ pip install compmech-reference-pack
30
+ ```
31
+
32
+ That gives you the domain pack and the adapter's declared capability surface,
33
+ and pulls in `akms-learn`. It does **not** pull in the MechDSL backend: every
34
+ `mechdsl` and `algo2code` import in this package is function-scoped, so it
35
+ installs and imports cleanly without them.
36
+
37
+ To enable the actual compile, transpile and verify path:
38
+
39
+ ```bash
40
+ pip install "compmech-reference-pack[mechdsl]"
41
+ ```
42
+
43
+ The extra exists so a downstream consumer can depend on this package without
44
+ dragging the executable backend — and transitively Taichi — into its
45
+ resolution.
46
+
47
+ ## What it provides
48
+
49
+ - **`MechDSLRunner`** — the `executable_bridge` adapter. Given an LSP excerpt
50
+ it normalises the algorithmic block, derives a safe algorithm name, and
51
+ delegates to MechDSL's `transpile_algorithm` / `compile_from_sources`.
52
+ Provenance is preserved from node to generated artefact.
53
+ - **`capabilities()`** — a declaration of what the package offers, kept
54
+ deliberately import-light and Taichi-free so a caller can ask without paying
55
+ for a backend it may not use.
56
+ - **The `compmech` domain pack** — `domain_pack.yaml` plus the source packs
57
+ under `domain_pack/source_packs/`, addressed through
58
+ `compmech_reference_pack.paths`.
59
+
60
+ ## Companion status
61
+
62
+ | Companion | Role | Status |
63
+ |---|---|---|
64
+ | `mechdsl` | executable bridge | available |
65
+ | `constkit` | concept kit | planned |
66
+ | `symbolic_fem_workbench` | pedagogical workbench | planned |
67
+
68
+ `planned` describes the adapter, not the source: the material for both planned
69
+ companions is public, in
70
+ [SOSOVSKI/Teaching-materials](https://github.com/SOSOVSKI/Teaching-materials).
71
+
72
+ ## Licence
73
+
74
+ Apache-2.0, matching AKMS.
@@ -0,0 +1,14 @@
1
+ compmech_reference_pack/__init__.py,sha256=elyoLBFm-eP7OpO4rSDdGr8I85U-I3n3zeYbMDWzdVM,873
2
+ compmech_reference_pack/capabilities.py,sha256=3M-VtSJ08J5DHMsdogVbvdXhjHGG5HackgZUPjsdF-M,2452
3
+ compmech_reference_pack/normalize.py,sha256=ro8aXcVEdGjdT7nvUFdQuvOkOEqmsB2VSaaCaDNN1ew,7084
4
+ compmech_reference_pack/paths.py,sha256=uQxVxnwgBxeSZOyIpP9NzvGqMYWNT2YIbowaz-PInac,1082
5
+ compmech_reference_pack/adapters/__init__.py,sha256=H1MFitiM4TJm7X3JAu4LV3urONw6nP9h5xgeKfejdos,370
6
+ compmech_reference_pack/adapters/mechdsl_runner.py,sha256=MaOKRGIgvtT8kmoqebQuj_kkXXq3aZYNisgmyEHohAk,12759
7
+ compmech_reference_pack/domain_pack/domain_pack.yaml,sha256=1lFR1poc-BmbirLOZBa6MiIFhKIH_sKGUFOhCS6o4Ec,1630
8
+ compmech_reference_pack/domain_pack/source_packs/constkit.yaml,sha256=MTC1nA1SVC63VSlvkRebHouUJ6BkRwJ5lEcgrJXSSyQ,774
9
+ compmech_reference_pack/domain_pack/source_packs/mechdsl.yaml,sha256=iD-woiDkEyHo_5nWDF1a7nUt4c4RWOKRRF1N_HZWQgs,624
10
+ compmech_reference_pack/domain_pack/source_packs/symbolic_fem_workbench.yaml,sha256=UNzr8L5tcXx17qIs1ayCdgRAnVZ7QChK2j-Rv3RM2X4,919
11
+ compmech_reference_pack-0.3.1.dist-info/METADATA,sha256=P0FUlJqk4I0zt-U2FNs-Is1w3jkZDHhCsziEeXqO12E,2726
12
+ compmech_reference_pack-0.3.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
13
+ compmech_reference_pack-0.3.1.dist-info/top_level.txt,sha256=riOWv2q4eOyjzwjlSg7oOsCT_jfRzISqSr8V3jECeyk,24
14
+ compmech_reference_pack-0.3.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ compmech_reference_pack