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>
7.6 KiB
trace_aiger.py -- Circuit Tracer and State Machine Visualizer
Purpose
Takes a synthesized AIGER circuit and produces a complete state machine analysis: full transition tables, reachability analysis, and a Graphviz DOT diagram with human-readable guard condition labels. Every label is derived from exhaustive circuit evaluation -- no manual annotation, no probabilistic inference.
Usage
python3 scripts/trace_aiger.py <circuit.aag> [output_dir]
dot -Tpng diagrams/SPEC_states.dot -o diagrams/SPEC_states.png
The script is generic -- it works with any .aag file, not just the PWR
reactor controller.
How It Works
Phase 1: Parse (parse_aag)
Reads the ASCII AIGER file and extracts:
- Input literals: the environment signals (even numbers)
- Latch pairs:
(current_literal, next_state_literal)for each state bit - Output literals: which computed values are controller outputs
- AND gates: the combinational logic
(lhs, rhs0, rhs1) - Symbol table: human-readable names for inputs/outputs
Returns a dictionary that eval_circuit can simulate.
Phase 2: Simulate (eval_circuit)
Evaluates the circuit for one clock cycle given specific latch values (current state) and input values (environment signals).
How it works:
- Initialize a literal-value table with constant 0/1
- Set input literals and their negations:
val[lit] = v; val[lit ^ 1] = 1 - v - Set latch literals the same way
- Evaluate AND gates in order:
val[lhs] = val[rhs0] & val[rhs1] - Read output values and next-state latch values from the table
The ^ 1 trick exploits AIGER's literal encoding: even literals are positive,
odd literals are negated. XOR-ing with 1 flips between positive and negative.
This means we never need explicit negation logic -- every gate's inputs are
already in the table, whether positive or negated.
Critical assumption: AND gates are listed in topological order in the AIGER file, so evaluating them sequentially guarantees all inputs are computed before they're needed. This is a property of the AIGER format specification.
Phase 3: Enumerate
For every combination of latch state and input values:
2^Llatch states (L = number of latches)2^Iinput combinations (I = number of inputs)- Total:
2^(L+I)evaluations
For the PWR reactor: 2 latches, 6 inputs = 256 evaluations. Trivial.
For each evaluation, records:
- Which outputs are active (determines the "mode")
- What the next latch state is (determines the transition)
Results are stored in:
transitions[src_state][dst_state]= set of input tuplesstate_modes[state]= set of mode labels
Phase 4: Reachability Analysis
Starting from the initial state (all latches = 0), performs a breadth-first search through the transition graph to identify which states are actually reachable during operation. Unreachable states are grayed out in the diagram.
Phase 5: Guard Extraction (extract_guard)
This is the core algorithm that makes the diagram human-readable. Given a set of input combinations that all produce the same transition, it finds a minimal boolean expression describing exactly that set.
Simple case -- single conjunction:
For each input variable, check if it's fixed (always 0 or always 1) or
don't-care (both 0 and 1 appear) across all combos. If the fixed variables
alone perfectly predict the combo count (2^(don't-cares) == combo count),
the guard is a single AND of the fixed variables.
Example: the shutdown-to-heatup edge has 32 combos. In all 32,
t_avg_above_min = 1. The other 5 inputs vary freely: 2^5 = 32. So the
guard is simply t_avg_above_min.
Complex case -- cube covering:
When a single conjunction can't explain the combo set, the algorithm uses greedy cube covering:
- Pick a sample combo from the uncovered set
- Start with all variables fixed to the sample's values (a point cube)
- Try relaxing each variable (making it don't-care), one at a time
- Accept the relaxation only if the expanded cube is a strict subset of the
edge's combos (
trial_set <= combo_set) -- this prevents the cube from "leaking" into other transitions - Repeat relaxation until no more variables can be freed
- Record this cube, remove its combos from the uncovered set, go to step 1
- OR all the cubes together
What is a "cube"? A conjunction where some variables are fixed and the
rest are don't-care. Example: inv1_holds & !t_avg_in_range fixes 2 of 6
variables, so it covers 2^4 = 16 input combinations. The term comes from
digital logic (Karnaugh maps), where a cube is a rectangular group of cells.
Correctness guarantee: The subset check trial_set <= combo_set on line
176 ensures that every cube covers ONLY combos belonging to this edge. The
check tests set containment, not size -- even one foreign combo causes
rejection. The union of all cubes exactly equals the original combo set.
Optimality caveat: The greedy algorithm does not guarantee a minimal number of cubes. The order in which variables are relaxed can affect the result. For guaranteed minimality, a Quine-McCluskey or Espresso algorithm would be needed. In practice, with 6 inputs, the greedy result is typically optimal or off by at most one cube.
Phase 6: DOT Generation
Produces a Graphviz DOT file with:
- State nodes labeled with human-readable mode names (stripping
in_prefix, uppercasing) - Edge labels showing the extracted guard conditions
- Color coding:
- State fill: blue (initial), yellow (transitory), green (operation), red (scram/emergency), gray (unreachable)
- Edge color: blue (self-loop/stay), green (normal transition), red (scram transition)
- Edge width: thicker for edges with more input combinations
Color assignment uses keyword heuristics on the output variable names (e.g., "scram" in the name -> red). This is cosmetic and does not affect correctness.
Scaling Considerations
The exhaustive enumeration approach has complexity O(2^(L+I) * A) where
A is the number of AND gates. This is perfectly tractable for small
controllers:
| Latches | Inputs | Evaluations | Feasible? |
|---|---|---|---|
| 2 | 6 | 256 | Instant |
| 3 | 8 | 2,048 | Instant |
| 4 | 10 | 16,384 | < 1 second |
| 5 | 12 | 131,072 | ~ seconds |
| 8 | 16 | 16M | ~ minutes |
| 10 | 20 | 1B | Impractical |
For controllers with more than ~12 latches, a symbolic (BDD-based) analysis would be needed instead of exhaustive enumeration.
Future Improvements
-
Symbolic analysis: Replace exhaustive enumeration with BDD-based reachability for large controllers. Libraries like
dd(Python) or direct use of Spot's automata operations could handle this. -
Guard minimization: Replace the greedy cube-covering algorithm with Espresso or Quine-McCluskey for guaranteed minimal expressions. The Python
pyedalibrary providesespresso_exprs()for this. -
Trace simulation: Add a mode that simulates specific input sequences step-by-step, showing the controller's response. Useful for validating against specific scenarios (e.g., "what happens if t_avg_above_min is asserted, then inv1_holds drops?").
-
Cross-reference with FRET requirements: Annotate each edge with which FRET requirement(s) it satisfies. This would provide traceability from the synthesized controller back to the natural-language specifications.
-
Multi-output modes: Currently assumes each state has exactly one active output (mode). If the controller has overlapping outputs, the labeling would need to handle composite states.
-
Interactive HTML output: Generate an interactive diagram (e.g., using D3.js or vis.js) instead of static SVG, allowing users to click states and see the full transition table for that state.