-
Notifications
You must be signed in to change notification settings - Fork 23
engine: measure and hill-climb generated diagram quality #1065
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
6e71aab
engine: rate-based layout metric and a taste-checking eval harness
bpowers c42b5c9
engine: replay incremental model builds in the layout eval harness
bpowers 369a2c4
engine: give each side flow its own stock face
bpowers bd61d96
engine: link builtin call inputs and lookup tables in layouts
bpowers b7ff853
engine: choose label sides by what the metric charges
bpowers dd7e343
engine: move incremental layout into its own module
bpowers 15068be
engine: declutter what an incremental layout adds
bpowers 46d3586
engine: lay out chains an edit adds whole as chains
bpowers 42c09ca
engine: continue a drawn chain's row for a new stock
bpowers b935db0
engine: bound a rendered module by its label too
bpowers 2282781
engine: index the label chooser's scene by a grid
bpowers 04cccea
engine: broad-phase the declutter relaxation
bpowers 41c2167
engine: declutter to the clearance the metric charges
bpowers 3b53995
engine: re-seed the layout eval baseline
bpowers c15571b
engine: polish free nodes off link crossings
bpowers 96d14bd
engine: charge the crossing polish like the metric
bpowers File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,177 @@ | ||
| # Layout quality: the metric and the eval harness | ||
|
|
||
| Simlin generates stock-and-flow diagrams for models that have none: an agent | ||
| building a model over MCP, a notebook user patching a model from Python, an | ||
| imported equation file. This document describes how diagram quality is | ||
| measured -- the layout-quality metric (`src/simlin-engine/src/layout/metrics.rs`), | ||
| its taste checks (`layout/taste.rs`), and the on-demand eval harness | ||
| (`src/simlin-engine/examples/layout_eval/`) -- and the loop for improving the | ||
| layout algorithm against them. | ||
|
|
||
| ## The metric | ||
|
|
||
| `compute_layout_metrics(view)` scores a diagram as a vector of terms, each `0` | ||
| when ideal, and `weighted_cost` collapses them with `MetricWeights::default()`. | ||
| `generate_best_layout` picks the cheapest of several seeds, so the metric is | ||
| production code, not just an evaluation tool. | ||
|
|
||
| ### The scene | ||
|
|
||
| Every term is computed over the geometry the renderer actually draws: node | ||
| shapes at their drawn size (a flow valve is the 9px circle `render_flow` draws), | ||
| flow pipes as 4px-thick segments, links as the exact polylines | ||
| `diagram::connector` draws (arcs sampled along their circle), and labels at the | ||
| boxes `diagram::label` measures. The declutter pass (`layout/declutter.rs`) | ||
| avoids the same obstacles the metric charges: it pushes apart the footprints | ||
| the metric finds overlapping, and chooses each label's side by the metric's own | ||
| charge for that label (`metrics::LabelScene`), so what the optimizer removes is | ||
| what the score counts. | ||
|
|
||
| ### Terms | ||
|
|
||
| The defect terms are RATES (means over the nodes, labels, or connectors they | ||
| concern). A model's cost therefore does not grow with its size, one defect costs | ||
| `weight / n` in a model of `n` things, the trade-off between terms is the same | ||
| for a 10-variable model as for a 300-variable one, and the corpus aggregate is | ||
| not dominated by the largest models. | ||
|
|
||
| | term | measures | weight | | ||
| |---|---|---| | ||
| | `node_overlap` | mean covered fraction of each node's shape by other shapes | 3.5 | | ||
| | `label_overlap` | mean covered fraction of each label by other labels and shapes | 3.5 | | ||
| | `node_connector_overlap` | fraction of connector length under non-incident shapes or pipes (a false causal link) | 2.0 | | ||
| | `label_connector_overlap` | mean over labels of connector length -- links and other flows' pipes -- through the text, relative to the box's smaller side; a node's own links count half | 1.5 | | ||
| | `crossings` | connector crossings per connector | 1.0 | | ||
| | `crowding` | clearance deficits `(1 - gap/8px)^2` between non-cloud footprints per node, plus links too short to show their arrow per link | 1.0 | | ||
| | `long_connectors` | mean excess of links beyond 3x the median link length | 0.5 | | ||
| | `sprawl` | mean connector length over characteristic node size | 0.25 | | ||
| | `loop_compactness` | isoperimetric penalty of feedback-loop polygons | 0.4 | | ||
| | `flow_bends` | bends per flow pipe | 0.15 | | ||
| | `misalignment` | fraction of nodes sharing no row or column with a nearby node | 0.1 | | ||
| | `loop_straightness` | bow shortfall of loop connectors | 0.1 | | ||
| | `edge_length_cv`, `aspect_penalty` | reported, unweighted | 0 | | ||
|
|
||
| `crowding` and `sprawl` pull in opposite directions and together set a finite | ||
| optimum spacing: spread until neighbors have air, no further. A flow and the | ||
| stock or cloud its pipe attaches to are joined by construction, so their SHAPES | ||
| being close is exempt from crowding and from the declutter's overlap check -- | ||
| their labels are not. | ||
|
|
||
| ### Defects | ||
|
|
||
| `analyze_layout(view)` returns the metrics together with every defect behind | ||
| them, located on the diagram. It runs the same code as | ||
| `compute_layout_metrics` with a recording sink, so an overlay drawn from the | ||
| defects can never disagree with the score. | ||
|
|
||
| ### Calibration | ||
|
|
||
| A weight is only as good as the judgments it reproduces. The weights are | ||
| checked against two kinds of judged pairs: | ||
|
|
||
| - **Taste checks.** `layout::taste::degrade` applies edits every modeler would | ||
| call regressions -- crowd the diagram (`Cramp`), spread it (`Inflate`), | ||
| scatter free nodes (`Jitter`, `Shuffle`), park the most-used parameter across | ||
| the diagram (`Exile`), drop one node on another (`Stack`), flatten curved | ||
| links (`StraightenLinks`) -- moving only what a person drags by hand and | ||
| keeping each link's bow. The metric must charge each. The unit test | ||
| `test_metric_penalizes_every_degradation_of_the_exemplars` pins this on the | ||
| shipped default projects; the harness runs the battery over every corpus | ||
| reference and production layout. | ||
| - **Visual judgments.** Reference-versus-production pairs where a person, looking | ||
| at the renders, finds one clearly better. The reference-pair unit tests pin | ||
| the default projects' hand-drawn diagrams beating a generated layout. | ||
|
|
||
| The committed weights are rounded priors that a log-space fit against those | ||
| pairs (squared hinge on a 2% margin, anchored at `crossings = 1`) moved by under | ||
| 15%. Judgments the terms cannot reproduce at any weights point at missing | ||
| terms, not at weights -- today that is structural taste: chains laid out in | ||
| rows, parameters beside their consumers, the arrangement that makes a | ||
| hand-drawn diagram read as organized rather than merely uncluttered. | ||
| `StraightenLinks` is deliberately not required: flattening a non-loop link is | ||
| style, and only loops care. | ||
|
|
||
| ## The eval harness | ||
|
|
||
| ``` | ||
| cargo run --release -p simlin-engine --features png_render,file_io --example layout_eval | ||
| ``` | ||
|
|
||
| Knobs (environment variables): `LAYOUT_EVAL_MODELS` (corpus keys), | ||
| `LAYOUT_EVAL_TIERS` (`small,medium,large`), `LAYOUT_EVAL_EXTRA` (`key=path` ad | ||
| hoc models), `LAYOUT_EVAL_SEEDS` (default 25), `LAYOUT_EVAL_OUT` (default | ||
| `target/layout-eval`), `LAYOUT_EVAL_COMPARE` (a previous run's output dir), | ||
| `LAYOUT_EVAL_WRITE_BASELINE`, `LAYOUT_EVAL_DECLUTTER=0`, | ||
| `LAYOUT_EVAL_REPLAY_STEPS` (0 skips the replay). | ||
|
|
||
| For each corpus model it: | ||
|
|
||
| - sweeps the seeds, scoring each layout (the algorithm's quality distribution, | ||
| summarized benchstat-style in `layout::eval_stats`); | ||
| - runs `generate_best_layout` once, timed -- the layout a user gets and what it | ||
| costs; | ||
| - replays building the model from empty over a few edits | ||
| (`LAYOUT_EVAL_REPLAY_STEPS`, default 4) -- each stock-flow chain arriving | ||
| whole, then the other variables nearest the backbone first -- syncing the | ||
| diagram after every edit the way MCP `edit_model` and pysimlin's patch sync | ||
| do (`generate_best_layout` while the view is empty, `incremental_layout` | ||
| after), and scores the final diagram: what an agent or notebook user ends up | ||
| looking at, which can differ sharply from a fresh layout because incremental | ||
| layout preserves everything already placed; | ||
| - renders the hand-drawn reference, the production and incremental layouts, | ||
| and the median and worst seeds to PNG (small diagrams upscaled so labels are | ||
| legible), with `*_defects.png` overlays for the reference, production, and | ||
| incremental diagrams and a `*.view.json` of every rendered view; | ||
| - runs the taste battery on the reference and the production layout. | ||
|
|
||
| It writes `metrics.json` (per-term breakdowns, timings, taste checks), | ||
| `corpus.json` (per-seed samples), and `index.html` (a contact sheet plus the | ||
| corpus-wide taste matrix). | ||
|
|
||
| ### The corpus and its references | ||
|
|
||
| `examples/layout_eval/corpus.rs` grades how far each model's shipped diagram can | ||
| be trusted: | ||
|
|
||
| - **Curated**: one view authored in Stella or Simlin, whose drawing conventions | ||
| the renderer reproduces; its score is directly comparable to a generated | ||
| layout's. | ||
| - **Imported**: one Vensim view. Its arrangement is a trustworthy exemplar, but | ||
| Vensim draws a variable as its wrapped name, so the label geometry our renderer | ||
| imposes is not what the author saw; label-dependent terms over it are not | ||
| comparable. | ||
| - **MultiView**: several views the importer stacks into one diagram with group | ||
| boxes -- an exemplar of decomposing a large model, not one comparable diagram. | ||
| - **None**: no shipped diagram. | ||
|
|
||
| The corpus spans textbook models, the default projects, AI-built models, module | ||
| and array models, and large published models, in three size tiers. | ||
|
|
||
| ### Comparing runs | ||
|
|
||
| `LAYOUT_EVAL_COMPARE=<dir>` diffs a run against an earlier run's `corpus.json`, | ||
| re-scored under the current weights: per-model Mann-Whitney verdicts over the | ||
| seed samples, and a paired Wilcoxon signed-rank test over the models' shifted-log | ||
| median ratios for the aggregate. The contact sheet shows the earlier run's term | ||
| values beside the current ones. The committed baseline | ||
| (`examples/layout_eval_baseline.json`, see its README) is diffed the same way on | ||
| every run. | ||
|
|
||
| ## The improvement loop | ||
|
|
||
| 1. Run the harness on the current code into one directory, and on the changed | ||
| code into another with `LAYOUT_EVAL_COMPARE` pointing at the first. | ||
| 2. Read the verdicts and per-term deltas, and the production timings. | ||
| 3. Look at the production renders and their defect overlays. A defect the eye | ||
| finds that no mark covers is a blind spot in the metric; a mark over | ||
| something that reads fine is a false positive. Either means the metric needs | ||
| work before its number can be trusted. | ||
| 4. Keep a change only when the numbers improve AND the pictures do. A change | ||
| that improves the number while the picture gets worse means the metric is | ||
| wrong, not the diagram. | ||
| 5. After landing an improvement, re-seed the committed baseline. | ||
|
|
||
| The hand-drawn references are ground truth for arrangement. A generated layout | ||
| scoring better than an exemplary Curated reference is a prompt to look, not a | ||
| win: either the generator truly beat the author, or the metric is missing what | ||
| the author got right. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documented command cannot launch this example because
src/simlin-engine/Cargo.tomldeclares the target'srequired-featuresaspng_render,file_io, andlayout_eval, while this command activates only the first two. Cargo'srun --helpconfirms that--featuresselects the features to activate, so users following the new harness documentation will be told the target is unavailable; addlayout_evalto the feature list here and in the duplicate usage text.AGENTS.md reference: AGENTS.md:L122-L122
Useful? React with 馃憤聽/ 馃憥.