gdk9-cli 0.3.0__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.
- examples/01_analyze.py +28 -0
- examples/02_profile.py +24 -0
- examples/03_assign.py +19 -0
- examples/04_dcg.py +41 -0
- examples/05_tokenize.py +22 -0
- examples/06_synthesize.py +20 -0
- examples/07_api.py +57 -0
- examples/08_kernel.py +35 -0
- examples/09_kernel_search_walkthrough.py +102 -0
- examples/10_conserve_vs_naive.py +100 -0
- gdk9/__init__.py +5 -0
- gdk9/ansi.py +57 -0
- gdk9/cli.py +1186 -0
- gdk9/crdt.py +88 -0
- gdk9/crypto.py +132 -0
- gdk9/data/ninefold.json +40 -0
- gdk9/data/official.example.json +16 -0
- gdk9/data/official.json +132 -0
- gdk9/data/subs.json +436 -0
- gdk9/dcg.py +356 -0
- gdk9/egglog_bridge/__init__.py +6 -0
- gdk9/egglog_bridge/dr_egglog.py +107 -0
- gdk9/egglog_bridge/fallback.py +34 -0
- gdk9/energy.py +211 -0
- gdk9/errors.py +15 -0
- gdk9/fmt.py +212 -0
- gdk9/imply.py +83 -0
- gdk9/io_utils.py +24 -0
- gdk9/kernel/__init__.py +24 -0
- gdk9/kernel/engine.py +123 -0
- gdk9/kernel/errors.py +17 -0
- gdk9/kernel/expression.py +43 -0
- gdk9/kernel/principle.py +43 -0
- gdk9/kernel/proof.py +24 -0
- gdk9/kernel/rule.py +30 -0
- gdk9/kernel/symbol.py +19 -0
- gdk9/kernel_cli.py +195 -0
- gdk9/log.py +30 -0
- gdk9/optimize.py +288 -0
- gdk9/parser.py +35 -0
- gdk9/plugins/__init__.py +9 -0
- gdk9/plugins/loader.py +346 -0
- gdk9/plugins/registry.py +42 -0
- gdk9/principles.py +131 -0
- gdk9/saturate.py +89 -0
- gdk9/state.py +138 -0
- gdk9/subs.py +108 -0
- gdk9/tokenize.py +386 -0
- gdk9/tui.py +115 -0
- gdk9/utilization.py +15 -0
- gdk9_cli-0.3.0.dist-info/METADATA +295 -0
- gdk9_cli-0.3.0.dist-info/RECORD +82 -0
- gdk9_cli-0.3.0.dist-info/WHEEL +5 -0
- gdk9_cli-0.3.0.dist-info/entry_points.txt +3 -0
- gdk9_cli-0.3.0.dist-info/licenses/LICENSE +661 -0
- gdk9_cli-0.3.0.dist-info/top_level.txt +4 -0
- scripts/batch_analyze.py +109 -0
- scripts/export_json.py +125 -0
- scripts/make_zip.py +57 -0
- scripts/profile_compare.py +165 -0
- scripts/watch_file.py +124 -0
- tests/conftest.py +26 -0
- tests/experiment/__init__.py +1 -0
- tests/experiment/test_conserve_vs_naive.py +287 -0
- tests/kernel/test_kernel_energy.py +18 -0
- tests/kernel/test_kernel_rules.py +58 -0
- tests/kernel/test_kernel_search_bound.py +34 -0
- tests/test_crypto.py +36 -0
- tests/test_crypto_secure.py +26 -0
- tests/test_dcg.py +318 -0
- tests/test_dcg_golden.py +67 -0
- tests/test_egglog_bridge.py +41 -0
- tests/test_energy.py +34 -0
- tests/test_kernel_cli_handbook.py +101 -0
- tests/test_optimize.py +21 -0
- tests/test_plugins.py +64 -0
- tests/test_rules.py +43 -0
- tests/test_rules_commit.py +40 -0
- tests/test_rules_reversibility.py +50 -0
- tests/test_state_crdt.py +87 -0
- tests/test_tokenize.py +38 -0
- tests/test_tokenize_metrics.py +33 -0
gdk9/kernel/errors.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Kernel-specific exceptions."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class KernelError(Exception):
|
|
5
|
+
"""Base exception for symbolic implication kernel failures."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ConservationError(KernelError):
|
|
9
|
+
"""Raised when a transformation violates an invariant."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ImplicationError(KernelError):
|
|
13
|
+
"""Raised when a rule cannot be applied to an expression."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class UnknownSymbolError(KernelError):
|
|
17
|
+
"""Raised when an expression references a symbol absent from a principle."""
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Iterable, Iterator, Sequence
|
|
5
|
+
|
|
6
|
+
from .principle import KernelPrinciple
|
|
7
|
+
from .symbol import Symbol
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class Expression:
|
|
12
|
+
"""An ordered symbolic expression."""
|
|
13
|
+
|
|
14
|
+
symbols: tuple[Symbol, ...]
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def from_names(cls, names: Iterable[str], principle: KernelPrinciple) -> "Expression":
|
|
18
|
+
return cls(tuple(Symbol(name, principle.energy_of(name)) for name in names))
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def from_text(cls, text: str, principle: KernelPrinciple) -> "Expression":
|
|
22
|
+
return cls.from_names(text, principle)
|
|
23
|
+
|
|
24
|
+
def __iter__(self) -> Iterator[Symbol]:
|
|
25
|
+
return iter(self.symbols)
|
|
26
|
+
|
|
27
|
+
def __len__(self) -> int:
|
|
28
|
+
return len(self.symbols)
|
|
29
|
+
|
|
30
|
+
def names(self) -> tuple[str, ...]:
|
|
31
|
+
return tuple(symbol.name for symbol in self.symbols)
|
|
32
|
+
|
|
33
|
+
def total_energy(self) -> float:
|
|
34
|
+
return sum(symbol.energy for symbol in self.symbols)
|
|
35
|
+
|
|
36
|
+
def digital_root(self, principle: KernelPrinciple) -> int:
|
|
37
|
+
return principle.total_root(self.total_energy())
|
|
38
|
+
|
|
39
|
+
def text(self) -> str:
|
|
40
|
+
return "".join(self.names())
|
|
41
|
+
|
|
42
|
+
def replace(self, symbols: Sequence[Symbol]) -> "Expression":
|
|
43
|
+
return Expression(tuple(symbols))
|
gdk9/kernel/principle.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Mapping
|
|
5
|
+
|
|
6
|
+
from gdk9.energy import char_energy, digital_root
|
|
7
|
+
from gdk9.principles import Principle
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class KernelPrinciple:
|
|
12
|
+
"""Immutable valuation context for kernel expressions."""
|
|
13
|
+
|
|
14
|
+
name: str
|
|
15
|
+
symbol_energy: Mapping[str, float] = field(default_factory=dict)
|
|
16
|
+
source: Principle | None = None
|
|
17
|
+
normalize_zero_to_nine: bool = True
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def default(cls) -> "KernelPrinciple":
|
|
21
|
+
principle = Principle.default()
|
|
22
|
+
return cls(
|
|
23
|
+
name=principle.name,
|
|
24
|
+
symbol_energy=dict(principle.symbol_energy),
|
|
25
|
+
source=principle,
|
|
26
|
+
normalize_zero_to_nine=principle.normalize_zero_to_nine,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def energy_of(self, name: str) -> float:
|
|
30
|
+
if len(name) == 1:
|
|
31
|
+
if self.source is not None:
|
|
32
|
+
return float(char_energy(name, self.source))
|
|
33
|
+
explicit = self.symbol_energy.get(name)
|
|
34
|
+
if explicit is not None:
|
|
35
|
+
return float(explicit)
|
|
36
|
+
return float(digital_root(ord(name), self.normalize_zero_to_nine))
|
|
37
|
+
explicit = self.symbol_energy.get(name)
|
|
38
|
+
if explicit is not None:
|
|
39
|
+
return float(explicit)
|
|
40
|
+
return sum(self.energy_of(ch) for ch in name)
|
|
41
|
+
|
|
42
|
+
def total_root(self, total: float) -> int:
|
|
43
|
+
return digital_root(int(round(total)), self.normalize_zero_to_nine)
|
gdk9/kernel/proof.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from .expression import Expression
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class Judgment:
|
|
10
|
+
"""The result of applying one implication rule."""
|
|
11
|
+
|
|
12
|
+
rule: str
|
|
13
|
+
before: Expression
|
|
14
|
+
after: Expression
|
|
15
|
+
conserved: bool
|
|
16
|
+
delta: float
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class ProofStep:
|
|
21
|
+
"""A traceable implication step suitable for proof paths."""
|
|
22
|
+
|
|
23
|
+
judgment: Judgment
|
|
24
|
+
index: int
|
gdk9/kernel/rule.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Callable
|
|
6
|
+
|
|
7
|
+
from .expression import Expression
|
|
8
|
+
|
|
9
|
+
Transform = Callable[[Expression], Expression]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RuleKind(str, Enum):
|
|
13
|
+
FUSION = "fusion"
|
|
14
|
+
SPLIT = "split"
|
|
15
|
+
REWRITE = "rewrite"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class ImplicationRule:
|
|
20
|
+
"""A pure implication transform with invariant metadata."""
|
|
21
|
+
|
|
22
|
+
name: str
|
|
23
|
+
kind: RuleKind
|
|
24
|
+
transform: Transform
|
|
25
|
+
reversible: bool = False
|
|
26
|
+
preserves_energy: bool = True
|
|
27
|
+
tolerance: float = 1e-9
|
|
28
|
+
|
|
29
|
+
def apply(self, expression: Expression) -> Expression:
|
|
30
|
+
return self.transform(expression)
|
gdk9/kernel/symbol.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Mapping
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class Symbol:
|
|
9
|
+
"""A named symbolic unit with an evaluated energy."""
|
|
10
|
+
|
|
11
|
+
name: str
|
|
12
|
+
energy: float
|
|
13
|
+
traits: Mapping[str, str] = field(default_factory=dict)
|
|
14
|
+
|
|
15
|
+
def __post_init__(self) -> None:
|
|
16
|
+
if not self.name:
|
|
17
|
+
raise ValueError("Symbol name must be non-empty")
|
|
18
|
+
if not isinstance(self.energy, (int, float)):
|
|
19
|
+
raise TypeError("Symbol energy must be numeric")
|
gdk9/kernel_cli.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Thin CLI/API adapter for the pure implication kernel.
|
|
2
|
+
|
|
3
|
+
Lives outside ``gdk9.kernel`` so the kernel package stays free of CLI, state,
|
|
4
|
+
plugin, crypto, TUI, and terminal-formatting imports.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from typing import Any, Sequence
|
|
11
|
+
|
|
12
|
+
from gdk9.errors import InputError
|
|
13
|
+
from gdk9.kernel import Expression, ImplicationEngine, KernelPrinciple
|
|
14
|
+
from gdk9.kernel.engine import fusion_rule, split_rule
|
|
15
|
+
from gdk9.kernel.errors import ConservationError, ImplicationError, KernelError
|
|
16
|
+
from gdk9.kernel.proof import Judgment, ProofStep
|
|
17
|
+
from gdk9.kernel.rule import ImplicationRule
|
|
18
|
+
from gdk9.principles import Principle
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def principle_to_kernel(principle: Principle) -> KernelPrinciple:
|
|
22
|
+
"""Adapt a GDk9 ``Principle`` into an immutable ``KernelPrinciple``."""
|
|
23
|
+
return KernelPrinciple(
|
|
24
|
+
name=principle.name,
|
|
25
|
+
symbol_energy=dict(principle.symbol_energy),
|
|
26
|
+
source=principle,
|
|
27
|
+
normalize_zero_to_nine=principle.normalize_zero_to_nine,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def expression_payload(expression: Expression) -> dict[str, Any]:
|
|
32
|
+
return {
|
|
33
|
+
"names": list(expression.names()),
|
|
34
|
+
"text": expression.text(),
|
|
35
|
+
"total_energy": expression.total_energy(),
|
|
36
|
+
"symbols": [
|
|
37
|
+
{"name": symbol.name, "energy": symbol.energy} for symbol in expression.symbols
|
|
38
|
+
],
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def judgment_payload(judgment: Judgment) -> dict[str, Any]:
|
|
43
|
+
return {
|
|
44
|
+
"rule": judgment.rule,
|
|
45
|
+
"before": expression_payload(judgment.before),
|
|
46
|
+
"after": expression_payload(judgment.after),
|
|
47
|
+
"conserved": judgment.conserved,
|
|
48
|
+
"delta": judgment.delta,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def proof_steps_payload(steps: Sequence[ProofStep]) -> list[dict[str, Any]]:
|
|
53
|
+
return [
|
|
54
|
+
{"index": step.index, "judgment": judgment_payload(step.judgment)} for step in steps
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _parse_float_list(raw: str, label: str) -> tuple[float, ...]:
|
|
59
|
+
parts = [p.strip() for p in raw.split(",") if p.strip()]
|
|
60
|
+
if not parts:
|
|
61
|
+
raise InputError(f"{label} must list at least one value")
|
|
62
|
+
try:
|
|
63
|
+
return tuple(float(p) for p in parts)
|
|
64
|
+
except ValueError as exc:
|
|
65
|
+
raise InputError(f"Invalid {label}: {raw}") from exc
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _parse_name_list(raw: str, label: str) -> tuple[str, ...]:
|
|
69
|
+
parts = [p.strip() for p in raw.split(",") if p.strip()]
|
|
70
|
+
if not parts:
|
|
71
|
+
raise InputError(f"{label} must list at least one symbol name")
|
|
72
|
+
return tuple(parts)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def builtin_rules(
|
|
76
|
+
names: Sequence[str],
|
|
77
|
+
*,
|
|
78
|
+
split_parts: tuple[str, ...] | None = None,
|
|
79
|
+
split_energies: tuple[float, ...] | None = None,
|
|
80
|
+
) -> tuple[ImplicationRule, ...]:
|
|
81
|
+
"""Build the small set of demo rules used by the kernel smoke path."""
|
|
82
|
+
rules: list[ImplicationRule] = []
|
|
83
|
+
for name in names:
|
|
84
|
+
key = name.strip().lower()
|
|
85
|
+
if key in {"fuse", "fusion"}:
|
|
86
|
+
rules.append(fusion_rule(name="fuse"))
|
|
87
|
+
elif key == "split":
|
|
88
|
+
if not split_parts or not split_energies:
|
|
89
|
+
raise InputError("split requires --parts and --energies")
|
|
90
|
+
rules.append(split_rule(split_parts, split_energies, name="split"))
|
|
91
|
+
else:
|
|
92
|
+
raise InputError(f"Unknown builtin kernel rule: {name}")
|
|
93
|
+
return tuple(rules)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _print_json(payload: dict[str, Any]) -> int:
|
|
97
|
+
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
|
98
|
+
return 0
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def cmd_kernel_eval(args: Any, principle: Principle) -> int:
|
|
102
|
+
kp = principle_to_kernel(principle)
|
|
103
|
+
expr = Expression.from_text(args.text, kp)
|
|
104
|
+
total, root = ImplicationEngine(kp).evaluate(expr)
|
|
105
|
+
return _print_json(
|
|
106
|
+
{
|
|
107
|
+
"ok": True,
|
|
108
|
+
"cmd": "eval",
|
|
109
|
+
"expression": expression_payload(expr),
|
|
110
|
+
"total": total,
|
|
111
|
+
"digital_root": root,
|
|
112
|
+
"principle": kp.name,
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def cmd_kernel_apply(args: Any, principle: Principle) -> int:
|
|
118
|
+
kp = principle_to_kernel(principle)
|
|
119
|
+
names = tuple(args.symbols)
|
|
120
|
+
if not names:
|
|
121
|
+
raise InputError("apply requires at least one symbol name")
|
|
122
|
+
expr = Expression.from_names(names, kp)
|
|
123
|
+
split_parts = _parse_name_list(args.parts, "parts") if getattr(args, "parts", None) else None
|
|
124
|
+
split_energies = (
|
|
125
|
+
_parse_float_list(args.energies, "energies") if getattr(args, "energies", None) else None
|
|
126
|
+
)
|
|
127
|
+
rules = builtin_rules(
|
|
128
|
+
[args.rule],
|
|
129
|
+
split_parts=split_parts,
|
|
130
|
+
split_energies=split_energies,
|
|
131
|
+
)
|
|
132
|
+
engine = ImplicationEngine(kp, rules)
|
|
133
|
+
try:
|
|
134
|
+
judgment = engine.apply(rules[0].name, expr)
|
|
135
|
+
except KernelError as exc:
|
|
136
|
+
return _print_json({"ok": False, "cmd": "apply", "error": str(exc)})
|
|
137
|
+
return _print_json(
|
|
138
|
+
{
|
|
139
|
+
"ok": True,
|
|
140
|
+
"cmd": "apply",
|
|
141
|
+
"judgment": judgment_payload(judgment),
|
|
142
|
+
"principle": kp.name,
|
|
143
|
+
}
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def cmd_kernel_search(args: Any, principle: Principle) -> int:
|
|
148
|
+
kp = principle_to_kernel(principle)
|
|
149
|
+
source_names = tuple(args.source)
|
|
150
|
+
target_names = tuple(args.target)
|
|
151
|
+
if not source_names or not target_names:
|
|
152
|
+
raise InputError("search requires --source and --target symbol names")
|
|
153
|
+
source = Expression.from_names(source_names, kp)
|
|
154
|
+
target = Expression.from_names(target_names, kp)
|
|
155
|
+
rule_names = list(args.rules) if getattr(args, "rules", None) else ["fuse"]
|
|
156
|
+
split_parts = _parse_name_list(args.parts, "parts") if getattr(args, "parts", None) else None
|
|
157
|
+
split_energies = (
|
|
158
|
+
_parse_float_list(args.energies, "energies") if getattr(args, "energies", None) else None
|
|
159
|
+
)
|
|
160
|
+
rules = builtin_rules(
|
|
161
|
+
rule_names,
|
|
162
|
+
split_parts=split_parts,
|
|
163
|
+
split_energies=split_energies,
|
|
164
|
+
)
|
|
165
|
+
engine = ImplicationEngine(kp, rules)
|
|
166
|
+
max_depth = int(getattr(args, "max_depth", 4))
|
|
167
|
+
path = engine.infer(source, target, max_depth=max_depth)
|
|
168
|
+
return _print_json(
|
|
169
|
+
{
|
|
170
|
+
"ok": path is not None,
|
|
171
|
+
"cmd": "search",
|
|
172
|
+
"found": path is not None,
|
|
173
|
+
"max_depth": max_depth,
|
|
174
|
+
"source": expression_payload(source),
|
|
175
|
+
"target": expression_payload(target),
|
|
176
|
+
"rules": [rule.name for rule in rules],
|
|
177
|
+
"steps": proof_steps_payload(path or ()),
|
|
178
|
+
"principle": kp.name,
|
|
179
|
+
}
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def run_kernel(args: Any, principle: Principle) -> int:
|
|
184
|
+
"""Dispatch ``gdk9 kernel …`` subcommands."""
|
|
185
|
+
cmd = getattr(args, "kernel_cmd", None)
|
|
186
|
+
try:
|
|
187
|
+
if cmd == "eval":
|
|
188
|
+
return cmd_kernel_eval(args, principle)
|
|
189
|
+
if cmd == "apply":
|
|
190
|
+
return cmd_kernel_apply(args, principle)
|
|
191
|
+
if cmd == "search":
|
|
192
|
+
return cmd_kernel_search(args, principle)
|
|
193
|
+
except (ConservationError, ImplicationError) as exc:
|
|
194
|
+
return _print_json({"ok": False, "cmd": cmd, "error": str(exc)})
|
|
195
|
+
raise InputError(f"Unknown kernel subcommand: {cmd}")
|
gdk9/log.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Logger:
|
|
9
|
+
def __init__(self, enabled: bool = False) -> None:
|
|
10
|
+
self.enabled = enabled or (os.getenv("GDK9_DEBUG") == "1")
|
|
11
|
+
|
|
12
|
+
def set_enabled(self, enabled: bool) -> None:
|
|
13
|
+
self.enabled = bool(enabled)
|
|
14
|
+
|
|
15
|
+
def debug(self, *args: Any) -> None:
|
|
16
|
+
if self.enabled:
|
|
17
|
+
msg = " ".join(str(a) for a in args)
|
|
18
|
+
print(f"[gdk9:debug] {msg}", file=sys.stderr)
|
|
19
|
+
|
|
20
|
+
def info(self, *args: Any) -> None:
|
|
21
|
+
msg = " ".join(str(a) for a in args)
|
|
22
|
+
print(msg)
|
|
23
|
+
|
|
24
|
+
def error(self, *args: Any) -> None:
|
|
25
|
+
msg = " ".join(str(a) for a in args)
|
|
26
|
+
print(msg, file=sys.stderr)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
logger = Logger()
|
|
30
|
+
|
gdk9/optimize.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import deque
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Dict, List, Optional, Tuple
|
|
6
|
+
|
|
7
|
+
from .energy import digital_root, string_energy, char_energy
|
|
8
|
+
from .principles import Principle
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Plan:
|
|
13
|
+
method: str
|
|
14
|
+
target: int
|
|
15
|
+
steps: List[Tuple[str, int]] # (symbol, count)
|
|
16
|
+
total_before: int
|
|
17
|
+
dr_before: int
|
|
18
|
+
total_after: int
|
|
19
|
+
dr_after: int
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _residue(n: int) -> int:
|
|
23
|
+
return n % 9
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _target_residue(total: int, target: int) -> int:
|
|
27
|
+
cur_r = _residue(total)
|
|
28
|
+
tgt_r = target % 9 # 9 -> 0
|
|
29
|
+
return (tgt_r - cur_r) % 9
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _allowed_residues(allowed: List[Tuple[str, int]]) -> Dict[int, List[str]]:
|
|
33
|
+
by_res: Dict[int, List[str]] = {i: [] for i in range(9)}
|
|
34
|
+
for s, e in allowed:
|
|
35
|
+
r = e % 9
|
|
36
|
+
by_res[r].append(s)
|
|
37
|
+
return by_res
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def minimal_residue_combo(delta_res: int, allowed: List[Tuple[str, int]], max_steps: int = 64) -> Optional[List[int]]:
|
|
41
|
+
# BFS on residues 0..8; each step adds one symbol residue
|
|
42
|
+
residues = [e % 9 for _, e in allowed]
|
|
43
|
+
if delta_res == 0:
|
|
44
|
+
return []
|
|
45
|
+
# If all residues are 0 we cannot change the residue
|
|
46
|
+
if all(r == 0 for r in residues):
|
|
47
|
+
return None
|
|
48
|
+
parent = {0: (-1, -1)} # residue -> (prev_residue, symbol_index)
|
|
49
|
+
q = deque([0])
|
|
50
|
+
depth = {0: 0}
|
|
51
|
+
while q:
|
|
52
|
+
cur = q.popleft()
|
|
53
|
+
d = depth[cur]
|
|
54
|
+
if d >= max_steps:
|
|
55
|
+
continue
|
|
56
|
+
for idx, r in enumerate(residues):
|
|
57
|
+
nxt = (cur + r) % 9
|
|
58
|
+
if nxt not in parent:
|
|
59
|
+
parent[nxt] = (cur, idx)
|
|
60
|
+
depth[nxt] = d + 1
|
|
61
|
+
if nxt == delta_res:
|
|
62
|
+
# reconstruct path of indices
|
|
63
|
+
seq: List[int] = []
|
|
64
|
+
at = nxt
|
|
65
|
+
while at != 0:
|
|
66
|
+
prev, i = parent[at]
|
|
67
|
+
seq.append(i)
|
|
68
|
+
at = prev
|
|
69
|
+
seq.reverse()
|
|
70
|
+
return seq
|
|
71
|
+
q.append(nxt)
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def build_steps_from_seq(seq: List[int], allowed: List[Tuple[str, int]]) -> List[Tuple[str, int]]:
|
|
76
|
+
counts: Dict[int, int] = {}
|
|
77
|
+
for i in seq:
|
|
78
|
+
counts[i] = counts.get(i, 0) + 1
|
|
79
|
+
steps: List[Tuple[str, int]] = []
|
|
80
|
+
for i, (sym, _e) in enumerate(allowed):
|
|
81
|
+
if i in counts:
|
|
82
|
+
steps.append((sym, counts[i]))
|
|
83
|
+
return steps
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def optimize_attunement(
|
|
87
|
+
text: str,
|
|
88
|
+
target: int,
|
|
89
|
+
principle: Principle,
|
|
90
|
+
allowed_symbols: Optional[str] = None,
|
|
91
|
+
method: str = "append",
|
|
92
|
+
max_steps: int = 64,
|
|
93
|
+
) -> Plan:
|
|
94
|
+
total, dr = string_energy(text, principle)
|
|
95
|
+
if not (1 <= target <= 9):
|
|
96
|
+
raise ValueError("Target energy must be 1..9")
|
|
97
|
+
if dr == target:
|
|
98
|
+
return Plan(method, target, [], total, dr, total, dr)
|
|
99
|
+
# Determine allowed symbol energies
|
|
100
|
+
if not allowed_symbols:
|
|
101
|
+
allowed_symbols = ".!?,*+" # sensible defaults with varied residues
|
|
102
|
+
allowed: List[Tuple[str, int]] = []
|
|
103
|
+
for s in allowed_symbols:
|
|
104
|
+
e = char_energy(s, principle)
|
|
105
|
+
if e == 0:
|
|
106
|
+
continue
|
|
107
|
+
allowed.append((s, e))
|
|
108
|
+
if not allowed:
|
|
109
|
+
raise ValueError("No valid allowed symbols; provide a non-empty set.")
|
|
110
|
+
delta_res = _target_residue(total, target)
|
|
111
|
+
seq = minimal_residue_combo(delta_res, allowed, max_steps=max_steps)
|
|
112
|
+
if seq is None:
|
|
113
|
+
raise ValueError("No feasible combination to reach target with given symbols.")
|
|
114
|
+
steps = build_steps_from_seq(seq, allowed)
|
|
115
|
+
added = sum(count * char_energy(sym, principle) for sym, count in steps)
|
|
116
|
+
new_total = total + added
|
|
117
|
+
new_dr = digital_root(new_total, principle.normalize_zero_to_nine)
|
|
118
|
+
return Plan(method, target, steps, total, dr, new_total, new_dr)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def apply_plan(text: str, plan: Plan, spread: Optional[int] = None) -> str:
|
|
122
|
+
if not plan.steps:
|
|
123
|
+
return text
|
|
124
|
+
if plan.method == "append":
|
|
125
|
+
suffix = "".join(sym * count for sym, count in plan.steps)
|
|
126
|
+
return text + suffix
|
|
127
|
+
if plan.method == "prepend":
|
|
128
|
+
prefix = "".join(sym * count for sym, count in plan.steps)
|
|
129
|
+
return prefix + text
|
|
130
|
+
if plan.method == "intersperse":
|
|
131
|
+
# Spread symbols approximately evenly through text
|
|
132
|
+
chars = list(text)
|
|
133
|
+
inserts: List[str] = []
|
|
134
|
+
for sym, count in plan.steps:
|
|
135
|
+
inserts.extend([sym] * count)
|
|
136
|
+
if not chars:
|
|
137
|
+
return "".join(inserts)
|
|
138
|
+
n = len(inserts)
|
|
139
|
+
if n == 0:
|
|
140
|
+
return text
|
|
141
|
+
interval = spread or max(1, len(chars) // (n + 1))
|
|
142
|
+
out: List[str] = []
|
|
143
|
+
i_ins = 0
|
|
144
|
+
for i, ch in enumerate(chars):
|
|
145
|
+
out.append(ch)
|
|
146
|
+
if (i + 1) % interval == 0 and i_ins < n:
|
|
147
|
+
out.append(inserts[i_ins])
|
|
148
|
+
i_ins += 1
|
|
149
|
+
while i_ins < n:
|
|
150
|
+
out.append(inserts[i_ins])
|
|
151
|
+
i_ins += 1
|
|
152
|
+
return "".join(out)
|
|
153
|
+
raise ValueError(f"Unsupported method: {plan.method}")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@dataclass
|
|
157
|
+
class EditOp:
|
|
158
|
+
kind: str # 'sub', 'del', 'ins'
|
|
159
|
+
pos: int
|
|
160
|
+
ch: Optional[str] = None
|
|
161
|
+
count: int = 1
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@dataclass
|
|
165
|
+
class EditPlan:
|
|
166
|
+
method: str # 'substitute' or 'edit'
|
|
167
|
+
target: int
|
|
168
|
+
ops: List[EditOp]
|
|
169
|
+
total_before: int
|
|
170
|
+
dr_before: int
|
|
171
|
+
total_after: int
|
|
172
|
+
dr_after: int
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def apply_edit_plan(text: str, plan: EditPlan) -> str:
|
|
176
|
+
if not plan.ops:
|
|
177
|
+
return text
|
|
178
|
+
chars = list(text)
|
|
179
|
+
# Apply deletions first from highest pos to lowest
|
|
180
|
+
dels = [op for op in plan.ops if op.kind == 'del']
|
|
181
|
+
for op in sorted(dels, key=lambda o: o.pos, reverse=True):
|
|
182
|
+
if 0 <= op.pos < len(chars):
|
|
183
|
+
del chars[op.pos]
|
|
184
|
+
# Apply substitutions
|
|
185
|
+
subs = [op for op in plan.ops if op.kind == 'sub']
|
|
186
|
+
for op in subs:
|
|
187
|
+
if 0 <= op.pos < len(chars) and op.ch is not None:
|
|
188
|
+
chars[op.pos] = op.ch
|
|
189
|
+
# Apply insertions from lowest pos to highest
|
|
190
|
+
ins = [op for op in plan.ops if op.kind == 'ins']
|
|
191
|
+
for op in sorted(ins, key=lambda o: o.pos):
|
|
192
|
+
insert_str = (op.ch or '') * max(1, op.count)
|
|
193
|
+
if 0 <= op.pos <= len(chars):
|
|
194
|
+
left = ''.join(chars[:op.pos])
|
|
195
|
+
right = ''.join(chars[op.pos:])
|
|
196
|
+
chars = list(left + insert_str + right)
|
|
197
|
+
return ''.join(chars)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def optimize_substitution(
|
|
201
|
+
text: str,
|
|
202
|
+
target: int,
|
|
203
|
+
principle: Principle,
|
|
204
|
+
subs: Optional[Dict[str, List[str]]] = None,
|
|
205
|
+
allow_delete: bool = False,
|
|
206
|
+
allowed_inserts: Optional[str] = None,
|
|
207
|
+
max_edits: int = 8,
|
|
208
|
+
) -> EditPlan:
|
|
209
|
+
total, dr = string_energy(text, principle)
|
|
210
|
+
if not (1 <= target <= 9):
|
|
211
|
+
raise ValueError("Target energy must be 1..9")
|
|
212
|
+
if dr == target:
|
|
213
|
+
return EditPlan('substitute', target, [], total, dr, total, dr)
|
|
214
|
+
|
|
215
|
+
# Build per-position residue deltas for substitutions (and deletions)
|
|
216
|
+
n = len(text)
|
|
217
|
+
pos_deltas: List[List[Tuple[int, Optional[str]]]] = [[] for _ in range(n)]
|
|
218
|
+
for i, ch in enumerate(text):
|
|
219
|
+
cur_e = char_energy(ch, principle)
|
|
220
|
+
seen: Dict[int, str] = {}
|
|
221
|
+
# substitution options
|
|
222
|
+
if subs and ch in subs:
|
|
223
|
+
for repl in subs[ch]:
|
|
224
|
+
new_e = char_energy(repl, principle)
|
|
225
|
+
r = (new_e - cur_e) % 9
|
|
226
|
+
if r not in seen:
|
|
227
|
+
seen[r] = repl
|
|
228
|
+
# trivial case-change option for letters
|
|
229
|
+
if ch.isalpha():
|
|
230
|
+
alt = ch.swapcase()
|
|
231
|
+
if alt != ch:
|
|
232
|
+
new_e = char_energy(alt, principle)
|
|
233
|
+
r = (new_e - cur_e) % 9
|
|
234
|
+
if r not in seen:
|
|
235
|
+
seen[r] = alt
|
|
236
|
+
for r, repl in seen.items():
|
|
237
|
+
if r != 0: # ignore no-ops
|
|
238
|
+
pos_deltas[i].append((r, repl))
|
|
239
|
+
# deletion option
|
|
240
|
+
if allow_delete:
|
|
241
|
+
rdel = (-cur_e) % 9
|
|
242
|
+
if rdel != 0:
|
|
243
|
+
pos_deltas[i].append((rdel, None))
|
|
244
|
+
|
|
245
|
+
delta_res = _target_residue(total, target)
|
|
246
|
+
# DP over residues with minimal edits, tracking choices
|
|
247
|
+
# state: residue -> (count, prev_residue, (pos, r, repl))
|
|
248
|
+
best: Dict[int, Tuple[int, int, Tuple[int, int, Optional[str]]]] = {0: (0, -1, (-1, 0, None))}
|
|
249
|
+
frontier = [0]
|
|
250
|
+
for i in range(n):
|
|
251
|
+
new_best = best.copy()
|
|
252
|
+
for res, (cnt, prev, info) in best.items():
|
|
253
|
+
for r, repl in pos_deltas[i]:
|
|
254
|
+
nr = (res + r) % 9
|
|
255
|
+
nc = cnt + 1
|
|
256
|
+
if nc > max_edits:
|
|
257
|
+
continue
|
|
258
|
+
if nr not in new_best or nc < new_best[nr][0]:
|
|
259
|
+
new_best[nr] = (nc, res, (i, r, repl))
|
|
260
|
+
best = new_best
|
|
261
|
+
|
|
262
|
+
if delta_res not in best:
|
|
263
|
+
# Try insertion-only fallback using symbol residues
|
|
264
|
+
if allowed_inserts:
|
|
265
|
+
append_plan = optimize_attunement(text, target, principle, allowed_symbols=allowed_inserts, method='append')
|
|
266
|
+
ops: List[EditOp] = []
|
|
267
|
+
for sym, cnt in append_plan.steps:
|
|
268
|
+
ops.append(EditOp('ins', len(text), sym, cnt))
|
|
269
|
+
out = apply_edit_plan(text, EditPlan('edit', target, ops, total, dr, total, dr))
|
|
270
|
+
new_total, new_dr = string_energy(out, principle)
|
|
271
|
+
return EditPlan('edit', target, ops, total, dr, new_total, new_dr)
|
|
272
|
+
raise ValueError("No feasible substitution/deletion plan to reach target.")
|
|
273
|
+
|
|
274
|
+
# Reconstruct
|
|
275
|
+
ops: List[EditOp] = []
|
|
276
|
+
cur = delta_res
|
|
277
|
+
while cur != 0:
|
|
278
|
+
cnt, prev, (pos, r, repl) = best[cur]
|
|
279
|
+
if pos >= 0:
|
|
280
|
+
if repl is None:
|
|
281
|
+
ops.append(EditOp('del', pos))
|
|
282
|
+
else:
|
|
283
|
+
ops.append(EditOp('sub', pos, repl))
|
|
284
|
+
cur = prev
|
|
285
|
+
# Apply and compute totals
|
|
286
|
+
out = apply_edit_plan(text, EditPlan('edit', target, list(reversed(ops)), total, dr, total, dr))
|
|
287
|
+
new_total, new_dr = string_energy(out, principle)
|
|
288
|
+
return EditPlan('edit', target, list(reversed(ops)), total, dr, new_total, new_dr)
|