Skip to content

Give every load model one package layout and stop models sharing code - #15

Open
TimThacker wants to merge 6 commits into
mainfrom
refactor/model-package-layout
Open

TimThacker wants to merge 6 commits into
mainfrom
refactor/model-package-layout

Conversation

@TimThacker

@TimThacker TimThacker commented Nov 2, 2026 •

Copy link
Copy Markdown
Owner

What does this change?

Gives every built-in load model the same structure and stops the models sharing code. It is a refactor: no CON name, default, .dyr record or simulated response changes. A new golden-file test pins that.

  • One package layout. Each model is src/load_models/<id>/ with the same files and public names: cons.py (CON_NAMES, DEFAULT_CONS, DISPLAY_FIELDS), dyr.py (build_<id>_record, parse_<id>_record, parse_<id>_records, is_<id>_record), simulation.py (simulate_<id>_response, simulate_<id>_frequency_response) and presets.py (cons_for_preset, preset_gaps). __init__.py only wires MODEL. perc1_dll's preset code moves out of __init__.py into a new presets.py.
  • No shared code between models. load_models/common.py is deleted. flag_on, the preset bundle lookup and the simulator time grid are copied into each model that uses them. perc2_dll carries its own copy of the PERC1 DLL CON layout, defaults, display rows, .dyr parser and simulator, and perc2_standard carries its own copy of the PERC1 standard .dyr helpers. A model imports only ..base.
  • One file shape. Every cons/dyr/presets/simulation file opens with a docstring and ends with an exact __all__. cons.py defines CON_NAMES, then DEFAULT_CONS, then DISPLAY_FIELDS, and flag_on lives only there. Every dyr.py starts with the same header constants (FAMILY, MODEL_CODE, N_ICON, N_CON, N_STATE, N_VAR, N_RESERVED_ICON) and defines strip_comments, so the writers and parsers no longer use header literals. perc2_dll's simulator internals are renamed to match the others.
  • Renames (internal API): PERC1_CON_ORDER, PERC1_DEFAULT_CONS, PERC1_DISPLAY_FIELDS, CLOD_DISPLAY_FIELDS, build_perc1_dyr_block, parse_perc1_dyr_block, simulate_perc1_response, clod_record and their PERC2 counterparts now follow the layout above. Callers in export/, editor/, preview/, ui/, presets/ and the tests are updated.
  • Regression test. tests/test_model_regression.py compares every registered model with a golden file in tests/golden/: CON names and defaults, display rows, the .dyr record (in full for the defaults, a digest for every preset and block), and a digest of the simulated response over a grid of voltage and frequency events, for the defaults and every preset (and with momentary cessation on for the PERC2 DLL). It also compares the full .dyr file the main window exports for the four PERC models. The golden files were generated from release 1.2.1 (c5ab482), before the restructuring. UPDATE_GOLDEN=1 uv run pytest tests/test_model_regression.py regenerates them for a deliberate change.
  • Tests and docs. The one-file test model example_load_model.py is removed. The developer-loop tests (case, export, mock run, sweep, CLI) now run on every real model that can mock-run. New tests check the layout, the import rule and the file shape. ADDING_A_LOAD_MODEL.md defines the layout and rules, has a worked example (a small step-trip model, checked by a test that writes its code blocks to a temporary package), and explains the golden files. ARCHITECTURE.md, PERC2_DLL_MODEL.md and the CHANGELOG are updated.

Why?

The four PERC models and CLOD had grown different structures (different names for the same thing, perc1_dll's preset code inline in __init__.py, perc2_dll importing the PERC1 DLL's internals), so there was no pattern to follow when adding a model. PSS/E can treat a CON of the same name differently in each model, so sharing code between models risks a change for one silently changing another. Each model is now defined in full in its own files, tests enforce the layout, and the golden files guard each model's behavior.

How was it tested?

Test counts (uv run pytest -q): 909 passed, 2 skipped (the golden-file regenerator, which only runs with UPDATE_GOLDEN=1, and one skip that was already there). uv run ruff check is clean. Run on the branch rebased onto current main, with pytest 9.2.0 and ruff 0.17.3.

Model behavior is unchanged:

  • The golden-file test passes against golden files generated from c5ab482, before any of this refactoring: 5 models, 2,090 simulated responses and every preset and block.
  • I checked that the test catches breaks by temporarily changing real model code. A changed default CON, a changed .dyr header code and a changed momentary-cessation delay in the simulator each failed it and named what moved. I then reverted them.
  • 17 malformed and mixed .dyr inputs (short and long records, bad headers, placeholder and unquoted ids, PERC1 and PERC2 records together) gave the same results from the old and new perc2_dll parser. This was a one-off comparison and is not in the suite.
  • I drove the real main window offscreen, selecting every model in the IT, VFD and screening dropdowns in both modes (28 combinations). The exported .dyr equals that of c5ab482 apart from the generation timestamp. The test above keeps four of those as golden files.

This does not change model behavior, so no new PSS/E run was needed. I did not run PSS/E.

What was not done?

  • The duplicated code (mostly perc2_dll's copy of the PERC1 DLL simulator) is now kept in sync by hand. A fix to the PERC1 DLL simulator needs the same fix in perc2_dll. The golden files will show if the copies diverge in the cases they cover.
  • presets/ridethrough.py, editor/model_ui.py, editor/dyr_import.py and preview/perc2_dll.py still import PERC1 DLL pieces. They are application code, not model definitions, and the preset library is written in PERC1 DLL terms.
  • The DLL models still describe the record with DYR_LAYOUT and the standard models with ConSpec, since the vendor formats differ. validate_cons stays optional: perc1_dll and CLOD have none.
  • The golden files cover the default and preset settings on a fixed grid of events, not every possible CON combination.
  • The live charts and editor tabs were not clicked through by hand.

Checklist

  • I have read CONTRIBUTING.md
  • I have not included secrets, credentials, or unrelated changes

Each model package exposes CON_NAMES, DEFAULT_CONS and DISPLAY_FIELDS from
cons.py, build_<id>_record/parse_<id>_record/is_<id>_record from dyr.py,
simulate_<id>_response from simulation.py, and cons_for_preset/preset_gaps
from presets.py. __init__.py only wires MODEL. Adds a contract test and a
Package layout section to ADDING_A_LOAD_MODEL.md.
…wn package

Remove load_models/common.py and every cross-model import. flag_on,
preset_bundle and the simulator time grid are copied into each model that uses
them; perc2_dll carries its own PERC1-record CON layout, defaults, display
rows, .dyr parser and simulator; perc2_standard carries its own .dyr helpers.
CON names, defaults, .dyr text and simulated responses are unchanged.

Add a test that fails if a model package imports another, and update the
developer docs and CHANGELOG.
Every cons/dyr/presets/simulation file opens with a docstring and ends with an
exact __all__. cons.py defines CON_NAMES, then DEFAULT_CONS, then
DISPLAY_FIELDS; flag_on lives only in cons.py. Every dyr.py starts with the same
header constants (FAMILY, MODEL_CODE, N_ICON, N_CON, N_STATE, N_VAR,
N_RESERVED_ICON) and defines strip_comments and _SCOPE_BY_SUFFIX. perc2_dll's
simulator internals are renamed to match the others. Tests check all of it;
CON names, defaults, .dyr text and simulated responses are unchanged.
A complete step-trip model in the package layout, one block per file. The test
writes the doc's blocks to a package and runs the layout and behaviour checks on
it, so the example cannot drift from the rules.
…d-model layout

tests/fixtures/extra_models/step_trip/ has the same files, names and ..base
imports as a built-in model, supports record scopes with aliases, and is the
model the end-to-end tests register. The worked example in
ADDING_A_LOAD_MODEL.md is generated from those files, and a test fails if a
doc block differs from its file. Removes example_load_model.py.
…model

tests/test_model_regression.py compares every registered model with a golden
file in tests/golden/ (CON names and defaults, display rows, the .dyr record,
and a digest of the simulated response over a grid of voltage and frequency
events for the defaults and every preset) and the main window's exported .dyr
for four models. The golden files were generated from release 1.2.1, before the
load models were restructured; UPDATE_GOLDEN=1 regenerates them.

The developer-loop tests (case, export, mock run, sweep, CLI) now run on every
real model that can mock-run. The step-trip test fixture package is removed;
the worked example in ADDING_A_LOAD_MODEL.md stays, checked by a small test
that writes its blocks to a temporary package.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant