Dane Sabo cebf8c167a Initial umbrella repo: thesis + FRET pipeline + plant model with first controllers
Folds three previously-separate pieces into one preliminary-example repo
for the HAHACS thesis:

- thesis/ (submodule) → gitea Thesis.git — the PhD proposal
- fret-pipeline/ — FRET requirements to AIGER controller (was
  ~/Documents/fret_processing/; prior single-commit history abandoned
  per user decision)
- plant-model/ — 10-state PKE + lumped T/H PWR model (was
  ~/Documents/PKE_Playground/; never version-controlled before)
- presentations/2026DICE/ (submodule) → gitea 2026DICE.git
- reachability/, hardware/ — empty placeholders for Thrust 3 and HIL
- docs/architecture.md — how the discrete and continuous layers compose
- claude_memory/ — session notes and scratch knowledge pattern

Plant model refactored to thesis naming (x, plant, u, ref); pke_th_rhs
now takes u as an explicit arg instead of reading rho_ext from the
params struct. First two controllers built to the contract
u = ctrl_<mode>(t, x, plant, ref): ctrl_null (baseline) and
ctrl_operation (stabilizing, proportional on T_avg). Validated under a
100% -> 80% Q_sg step: ctrl_operation reduces steady-state T_avg drift
~47% vs. the unforced plant.

Root CLAUDE.md emphasizes that CLAUDE.md files are living documents and
that any knowledge not captured before a session ends is lost forever;
claude_memory/ holds the session-level notes that haven't stabilized
enough to graduate into a CLAUDE.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 16:24:11 -04:00

259 lines
9.0 KiB
Python

#!/usr/bin/env python3
"""Convert FRET JSON export to ltlsynt synthesis config.
Automatically detects:
- Mode variables: any variable matching `control_* = q_*` in the LTL.
Each control group (e.g. control_mode) gets its own set of boolean
outputs (e.g. in_mode_shutdown, in_mode_heatup) with a mutual
exclusion constraint.
- Environment inputs: variables that appear only in conditions
(pre_condition / regular_condition) and are not mode variables.
- System outputs: the boolean mode propositions derived from control_*.
Naming convention in FRET:
- Mode variables MUST start with "control_" (e.g. control_mode, control_pump)
- Mode values MUST start with "q_" (e.g. q_shutdown, q_heatup)
- All other variables are classified by their FRET role.
Usage: python3 scripts/fret_to_synth.py <fret_export.json> [output.json]
"""
import json
import re
import sys
from collections import defaultdict
from pathlib import Path
def discover_modes(requirements):
"""Scan all LTL formulas for (control_X = q_Y) patterns.
Returns a dict: { "control_X": ["q_Y", "q_Z", ...] }
Each key is a mode group, values are the discovered mode values.
"""
# Match patterns like (control_mode = q_shutdown), (control_pump = q_off)
pattern = re.compile(r'\((\s*control_\w+)\s*=\s*(q_\w+)\s*\)')
mode_groups = defaultdict(set)
for req in requirements:
sem = req["semantics"]
for field in ("ftInfAUExpanded", "ft", "ftExpanded"):
ltl = sem.get(field, "")
if ltl:
for match in pattern.finditer(ltl):
ctrl_var = match.group(1).strip()
mode_val = match.group(2).strip()
mode_groups[ctrl_var].add(mode_val)
# Sort mode values for deterministic output
return {k: sorted(v) for k, v in sorted(mode_groups.items())}
def build_mode_map(mode_groups):
"""Build the string replacement map and output variable list.
For a single control group like control_mode with values
[q_heatup, q_operation, q_scram, q_shutdown], produces:
- replacements: {"(control_mode = q_heatup)": "in_mode_heatup", ...}
- outputs: ["in_mode_heatup", "in_mode_operation", ...]
For multiple groups, the boolean name includes the group suffix:
control_mode -> in_mode_X
control_pump -> in_pump_X
"""
replacements = {}
outputs_by_group = {}
for ctrl_var, mode_vals in mode_groups.items():
# Derive group suffix: control_mode -> mode, control_pump -> pump
suffix = ctrl_var.replace("control_", "", 1)
group_outputs = []
for qval in mode_vals:
# q_shutdown -> shutdown
val_name = qval.replace("q_", "", 1)
bool_name = f"in_{suffix}_{val_name}"
replacements[f"({ctrl_var} = {qval})"] = bool_name
group_outputs.append(bool_name)
outputs_by_group[ctrl_var] = group_outputs
return replacements, outputs_by_group
def classify_variables(requirements, mode_groups):
"""Classify non-mode variables as environment inputs.
A variable is an environment input if:
- It is NOT a control_* or q_* variable (those are handled by mode encoding)
- It appears in any requirement's semantics.variables
All mode propositions are system outputs (handled separately).
"""
# Collect all known mode-related variable names to exclude
mode_var_names = set()
for ctrl_var, mode_vals in mode_groups.items():
mode_var_names.add(ctrl_var)
mode_var_names.update(mode_vals)
# Collect all variables referenced across requirements
all_vars = set()
for req in requirements:
sem = req["semantics"]
for v in sem.get("variables", []):
all_vars.add(v)
# Everything that's not a mode variable is an environment input
env_inputs = sorted(all_vars - mode_var_names)
return env_inputs
def encode_ltl(formula, replacements):
"""Replace all (control_X = q_Y) with boolean mode propositions."""
result = formula
for pattern, replacement in replacements.items():
result = result.replace(pattern, replacement)
# Sanity check: no control_ or q_ references should remain
leftover = re.findall(r'control_\w+|q_\w+', result)
if leftover:
raise ValueError(
f"Unhandled mode references in LTL: {leftover}\n"
f" Formula: {result}\n"
f" Known replacements: {list(replacements.keys())}"
)
return result
def mutual_exclusion_ltl(group_outputs):
"""Generate LTL for exactly-one-active constraint over a mode group.
Produces: G( (a & !b & !c & !d) | (!a & b & !c & !d) | ... )
"""
clauses = []
for i, m in enumerate(group_outputs):
parts = []
for j, n in enumerate(group_outputs):
if i == j:
parts.append(n)
else:
parts.append(f"(! {n})")
clauses.append(f"({' & '.join(parts)})")
exactly_one = " | ".join(clauses)
return f"G ({exactly_one})"
def main():
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <fret_export.json> [output.json]", file=sys.stderr)
sys.exit(1)
input_path = Path(sys.argv[1])
output_path = Path(sys.argv[2]) if len(sys.argv) > 2 else Path("specs/synthesis_config_v3.json")
with open(input_path) as f:
fret_data = json.load(f)
requirements = fret_data["requirements"]
project = requirements[0]["project"]
component = requirements[0]["semantics"]["component_name"]
# --- Auto-discover everything ---
mode_groups = discover_modes(requirements)
replacements, outputs_by_group = build_mode_map(mode_groups)
env_inputs = classify_variables(requirements, mode_groups)
all_outputs = [out for group in outputs_by_group.values() for out in group]
print(f"Discovered {len(mode_groups)} mode group(s):")
for ctrl_var, modes in mode_groups.items():
print(f" {ctrl_var}: {modes}")
print(f" -> booleans: {outputs_by_group[ctrl_var]}")
print(f"Environment inputs: {env_inputs}")
print(f"System outputs: {all_outputs}")
print()
# --- Encode requirements ---
encoded_reqs = []
for req in requirements:
reqid = req["reqid"]
fulltext = req["fulltext"]
sem = req["semantics"]
# Prefer ftInfAUExpanded (cleanest infinite-horizon LTL)
raw_ltl = sem.get("ftInfAUExpanded") or sem.get("ft")
if not raw_ltl:
print(f"WARNING: No LTL for {reqid}, skipping", file=sys.stderr)
continue
encoded = encode_ltl(raw_ltl, replacements)
encoded_reqs.append({
"req_id": reqid,
"fulltext": fulltext,
"project": project,
"component": component,
"ltl": encoded,
"ltl_original": raw_ltl,
"condition_type": sem.get("condition", "unknown"),
})
# --- Build structural constraints (one mutex per mode group) ---
structural = []
for ctrl_var, group_outs in outputs_by_group.items():
mutex = mutual_exclusion_ltl(group_outs)
structural.append({
"name": f"mutex_{ctrl_var}",
"description": f"Exactly one {ctrl_var} value active at all times",
"ltl": mutex,
})
# --- Conjoin all formulas ---
all_formulas = [r["ltl"] for r in encoded_reqs]
all_formulas += [s["ltl"] for s in structural]
conjoined = " & ".join(f"({f})" for f in all_formulas)
# --- Write config ---
config = {
"_comment": (
f"Generated from {input_path.name} by fret_to_synth.py. "
f"Boolean mode encoding with auto-discovered mode groups. "
f"Mutual exclusion constraints are synthesis artifacts — they enforce "
f"the single-valued semantics of control_* variables after decomposition "
f"into independent booleans. They are NOT needed in FRET itself, where "
f"control_mode = q_X is inherently single-valued."
),
"spec_name": f"{project}_{component}",
"source_file": str(input_path),
"mode_groups": {
ctrl_var: {
"values": modes,
"booleans": outputs_by_group[ctrl_var],
}
for ctrl_var, modes in mode_groups.items()
},
"inputs": env_inputs,
"outputs": all_outputs,
"requirements": encoded_reqs,
"structural_constraints": structural,
"conjoined_ltl": conjoined,
}
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, "w") as f:
json.dump(config, f, indent=2)
print(f"Synthesis config written to: {output_path}")
print(f" Requirements: {len(encoded_reqs)}")
print(f" Structural constraints: {len(structural)}")
print(f" Conjoined formula length: {len(conjoined)} chars")
print("\nEncoded requirements:")
for r in encoded_reqs:
print(f" {r['req_id']} [{r['condition_type']}]:")
print(f" {r['ltl'][:120]}{'...' if len(r['ltl']) > 120 else ''}")
if __name__ == "__main__":
main()