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

191 lines
7.6 KiB
Markdown

# 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
```bash
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:**
1. Initialize a literal-value table with constant 0/1
2. Set input literals and their negations: `val[lit] = v; val[lit ^ 1] = 1 - v`
3. Set latch literals the same way
4. Evaluate AND gates in order: `val[lhs] = val[rhs0] & val[rhs1]`
5. 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^L` latch states (L = number of latches)
- `2^I` input 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 tuples
- `state_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:
1. Pick a sample combo from the uncovered set
2. Start with all variables fixed to the sample's values (a point cube)
3. Try relaxing each variable (making it don't-care), one at a time
4. 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
5. Repeat relaxation until no more variables can be freed
6. Record this cube, remove its combos from the uncovered set, go to step 1
7. 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
`pyeda` library provides `espresso_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.