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>
191 lines
7.6 KiB
Markdown
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.
|