-
Notifications
You must be signed in to change notification settings - Fork 2
openapi json export #30
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’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
33 commits
Select commit
Hold shift + click to select a range
fa2ab81
feat: add OpenAPI emit layer and drop the PowerSystemCaseBuilder test…
jd-lara f4fc081
test usage of the OpenAPI structs
jd-lara 7dd0109
validation readme
jd-lara 74b75f4
Salvage line shunts from switching devices declared in BRANCH data
jd-lara d1c383e
Ignore .claude/plans/ working documents
jd-lara a08051c
Remove .claude/Sienna.md in favor of the shared sienna-psy6 skill
jd-lara 13bce46
Rename .claude/claude.md to .claude/CLAUDE.md
jd-lara b33893f
test with TestData
jd-lara fa3e57d
update code for OpenAPI spec
jd-lara 52c6f7c
Rebuild OpenAPISystem over PC.SystemDocument; add topology readers
jd-lara 0f7f768
Warn on unconsumed pm dict sections; fix units.jl model drift
jd-lara 8ed18cb
Add load and generation/cost readers to the OpenAPI emit layer
jd-lara 041db62
Add branch, transformer, DC line, and shunt readers to the OpenAPI em…
jd-lara 511eaee
Fix round 1: convert non-exempt ternaries to if/else, drop dead DC_VO…
jd-lara b0103b1
Add switch/breaker and attributes readers; close pm-section ledger; f…
jd-lara bafe0ac
Replace VSC control-mode gap tests with real conversions; document kV…
jd-lara 66ac23e
Restore loud error for VSC voltage-control setpoints until reader car…
jd-lara a38218f
Simplify OpenAPI emit layer comments and structure
jd-lara ec519e8
Implement real DEVICE_BASE emission in build_openapi_system
jd-lara 886b594
Include ActivePowerChangeRate (ramp_limits) in DEVICE_BASE conversion
jd-lara db849c7
Close instance-dispatched-field hole in DEVICE_BASE classification
jd-lara 6d26bb7
Fix TransformerCircuit.controlled_quantity_limits: static :skip, not …
jd-lara fe56611
Settle transfer_setpoint DEVICE_BASE disposition: LCC exists only as …
jd-lara 7b47107
Simplify sweep: drop plan-reference comments, factor ext/optional-val…
jd-lara e81018e
Un-consolidate service association: build ServiceAssociation, not Sup…
jd-lara 45482dc
Emit base_power on FixedAdmittance/FACTSControlDevice; drop the YAML …
jd-lara f6d9cac
Emit Substation supplemental attributes, closing the unconsumed-secti…
jd-lara 726344e
Address the deferred parser code-quality review
jd-lara 454bae3
Resolve the OpenAPI models from git so CI can instantiate
jd-lara a53b479
Quality pass over the parser changes
jd-lara dd0f175
Pin the OpenAPI models in the docs environment too
jd-lara 1644ab7
Build docs on Julia 1
jd-lara d4c6654
Pin the docs job to Julia 1.12
jd-lara 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
File renamed without changes.
This file was deleted.
Oops, something went wrong.
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
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
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,30 @@ | ||
| # scripts | ||
|
|
||
| ## `validate_with_python.py` | ||
|
|
||
| Validates a JSON document written by `to_json` against the pydantic models generated from | ||
| the same SiennaSchemas, catching disagreements between the two generators. | ||
|
|
||
| ```bash | ||
| python3 -m pip install -e ../power-openapi-models # once, from a sibling checkout | ||
| python3 scripts/validate_with_python.py case.json | ||
| ``` | ||
|
|
||
| If your system Python refuses the install with `error: externally-managed-environment` | ||
| (PEP 668), install into a virtual environment instead and run the script with that | ||
| venv's interpreter: | ||
|
|
||
| ```bash | ||
| python3 -m venv /path/to/venv | ||
| /path/to/venv/bin/python3 -m pip install -e ../power-openapi-models | ||
| /path/to/venv/bin/python3 scripts/validate_with_python.py case.json | ||
| ``` | ||
|
|
||
| Run by hand. It is not part of `julia --project=test test/runtests.jl`, which stays free of | ||
| a Python dependency. | ||
|
|
||
| ## `formatter/` | ||
|
|
||
| ```bash | ||
| julia --project=scripts/formatter -e 'include("scripts/formatter/formatter_code.jl")' | ||
| ``` | ||
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,135 @@ | ||
| #!/usr/bin/env julia | ||
| # | ||
| # Parse a PSS/E case and write JSON for visual inspection. | ||
| # | ||
| # julia --project scripts/inspect_14bus_json.jl | ||
| # julia --project scripts/inspect_14bus_json.jl --case path/to/other.raw --out /tmp/look | ||
| # | ||
| # Produces three files in the output directory (default `inspection_output/`): | ||
| # | ||
| # <case>.pm.json the parsed PowerModels dict | ||
| # <case>.NATURAL_UNITS.json OpenAPI document, unit_system = NATURAL_UNITS | ||
| # <case>.DEVICE_BASE.json OpenAPI document, unit_system = DEVICE_BASE | ||
| # | ||
| # Both OpenAPI documents are built via `build_openapi_system` and carry real components — | ||
| # buses, loads, generators, branches, transformers, dc lines, shunts, plus | ||
| # `ImpedanceCorrectionData` and `DiscreteControlledACBranch`. | ||
| # `../power-openapi-models/scripts/check_json_compat.py` reads this directory's `*.json` | ||
| # (excluding `.pm.json`/`.roundtrip.json`) to validate Julia's output against the | ||
| # generated Python models. | ||
|
|
||
| import JSON | ||
| using PowerFlowFileParser | ||
|
|
||
| const DEFAULT_CASE = joinpath(@__DIR__, "..", "test", "modified_14bus_system.raw") | ||
| const UNIT_SYSTEMS = ("NATURAL_UNITS", "DEVICE_BASE") | ||
|
|
||
| function parse_args(argv) | ||
| case = DEFAULT_CASE | ||
| out = joinpath(@__DIR__, "..", "inspection_output") | ||
| i = 1 | ||
| while i <= length(argv) | ||
| arg = argv[i] | ||
| if arg == "--case" | ||
| case = argv[i + 1] | ||
| i += 2 | ||
| elseif arg == "--out" | ||
| out = argv[i + 1] | ||
| i += 2 | ||
| elseif arg in ("-h", "--help") | ||
| println(read(@__FILE__, String)[1:findfirst("import JSON", read(@__FILE__, String)).start - 1]) | ||
| exit(0) | ||
| else | ||
| error("Unrecognized argument: $arg") | ||
| end | ||
| end | ||
| return (case = abspath(case), out = abspath(out)) | ||
| end | ||
|
|
||
| """Write `data` as indented JSON, reporting the path and size.""" | ||
| function write_json(path, data) | ||
| open(path, "w") do io | ||
| JSON.print(io, data, 2) | ||
| end | ||
| println(" wrote $(relpath(path)) ($(round(filesize(path) / 1024; digits = 1)) KiB)") | ||
| return path | ||
| end | ||
|
|
||
| """Count entries per top-level PM-dict section, so the parse can be eyeballed.""" | ||
| function summarize_pm(data) | ||
| println("\nParsed sections (component counts):") | ||
| for key in sort(collect(keys(data))) | ||
| value = data[key] | ||
| if value isa Dict && !isempty(value) && all(v -> v isa Dict, values(value)) | ||
| println(" $(rpad(key, 26)) $(length(value))") | ||
| elseif value isa Vector | ||
| println(" $(rpad(key, 26)) $(length(value))") | ||
| end | ||
| end | ||
| println("\nScalars: base_power=$(get(data, "baseMVA", "?")) per_unit=$(get(data, "per_unit", "?"))") | ||
| return nothing | ||
| end | ||
|
|
||
| """ | ||
| Report the shunts, including any relocated off a switching device. | ||
|
|
||
| A BRANCH row with a '@'/'*' CKT is a breaker or switch and has no line shunt, so | ||
| GI/BI/GJ/BJ on such a row is malformed input and gets moved to the bus it was | ||
| declared on. Those carry a `branch shunt` source_id, so they are worth calling out | ||
| separately during a visual check. | ||
| """ | ||
| function summarize_shunts(data) | ||
| shunts = get(data, "shunt", Dict()) | ||
| if isempty(shunts) | ||
| println("\nNo shunts in this case.") | ||
| return nothing | ||
| end | ||
| println("\nShunts ($(length(shunts))):") | ||
| for key in sort(collect(keys(shunts))) | ||
| s = shunts[key] | ||
| source = get(s, "source_id", Any[]) | ||
| tag = isempty(source) ? "?" : string(first(source)) | ||
| marker = tag == "branch shunt" ? " <-- relocated from a switching device" : "" | ||
| println( | ||
| " bus=$(rpad(get(s, "shunt_bus", "?"), 6)) " * | ||
| "gs=$(rpad(get(s, "gs", "?"), 12)) bs=$(rpad(get(s, "bs", "?"), 12)) " * | ||
| "source=$tag$marker", | ||
| ) | ||
| end | ||
| return nothing | ||
| end | ||
|
|
||
| function main(argv) | ||
| opts = parse_args(argv) | ||
| isfile(opts.case) || error("Case not found: $(opts.case)") | ||
| mkpath(opts.out) | ||
| stem = first(splitext(basename(opts.case))) | ||
|
|
||
| println("Case: $(opts.case)") | ||
| println("Output: $(opts.out)\n") | ||
|
|
||
| println("Parsing...") | ||
| pm = PowerModelsData(opts.case) | ||
| data = pm.data | ||
|
|
||
| println("\nWriting JSON:") | ||
| write_json(joinpath(opts.out, "$stem.pm.json"), data) | ||
|
|
||
| # The OpenAPI envelope in both unit systems, built from the same parsed case via the | ||
| # full emit layer. | ||
| for unit_system in UNIT_SYSTEMS | ||
| sys = build_openapi_system(pm; unit_system = unit_system) | ||
| path = joinpath(opts.out, "$stem.$unit_system.json") | ||
| to_json(sys, path; force = true, pretty = true) | ||
| println(" wrote $(relpath(path)) ($(round(filesize(path) / 1024; digits = 1)) KiB)") | ||
| println( | ||
| " components: $(join(PowerFlowFileParser.component_type_names(sys), ", "))", | ||
| ) | ||
| end | ||
|
|
||
| summarize_pm(data) | ||
| summarize_shunts(data) | ||
| return nothing | ||
| end | ||
|
|
||
| main(ARGS) |
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,110 @@ | ||
| #!/usr/bin/env python3 | ||
| """Validate a Sienna OpenAPI JSON document against the generated pydantic models. | ||
|
|
||
| The Julia and Python model packages are generated from the same SiennaSchemas, so a | ||
| component that loads in one and not the other is a schema-level defect. Validates | ||
| components, supplemental attributes, and both association sections (supplemental attribute | ||
| and time series). Run this by hand against emitted documents; it is deliberately not wired | ||
| into the Julia test suite, which stays free of a Python dependency. | ||
|
|
||
| Usage: | ||
| python3 scripts/validate_with_python.py DOCUMENT.json | ||
| """ | ||
|
|
||
| import argparse | ||
| import importlib | ||
| import json | ||
| import sys | ||
|
|
||
| MODULES = ( | ||
| "power_openapi_models.core.models", | ||
| "power_openapi_models.operations.models", | ||
| "power_openapi_models.investments.models", | ||
| "power_openapi_models.dynamics.models", | ||
| ) | ||
|
|
||
| ASSOCIATION_SECTIONS = ( | ||
| ("supplemental_attribute_associations", "SupplementalAttributeAssociation"), | ||
| ("time_series_associations", "TimeSeriesAssociation"), | ||
| ) | ||
|
|
||
|
|
||
| def load_models(): | ||
| """Map every pydantic class name to its class, first module winning.""" | ||
| models = {} | ||
| for name in MODULES: | ||
| module = importlib.import_module(name) | ||
| for attr in dir(module): | ||
| if attr.startswith("_") or attr in models: | ||
| continue | ||
| models[attr] = getattr(module, attr) | ||
| return models | ||
|
|
||
|
|
||
| def validate_group(models, type_name, payloads, failures): | ||
| model = models.get(type_name) | ||
| if model is None: | ||
| failures.append(f"{type_name}: no pydantic class of that name in {MODULES}") | ||
| return 0 | ||
| validated = 0 | ||
| for index, payload in enumerate(payloads): | ||
| try: | ||
| model.model_validate(payload) | ||
| validated += 1 | ||
| except Exception as error: | ||
| failures.append(f"{type_name}[{index}]: {error}") | ||
| return validated | ||
|
|
||
|
|
||
| def main(): | ||
| parser = argparse.ArgumentParser(description=__doc__) | ||
| parser.add_argument("document", help="path to a JSON document written by to_json") | ||
| args = parser.parse_args() | ||
|
|
||
| with open(args.document) as handle: | ||
| document = json.load(handle) | ||
|
|
||
| models = load_models() | ||
| failures = [] | ||
| validated = 0 | ||
|
|
||
| for type_name in sorted(document.get("components", {})): | ||
| validated += validate_group( | ||
| models, type_name, document["components"][type_name], failures | ||
| ) | ||
|
|
||
| # `supplemental_attributes` is a flat list carrying no type tag of its own; the | ||
| # association table is what names each attribute's class, by `attribute_id`. An | ||
| # attribute with no association has no declarable type, which is a document defect | ||
| # rather than something to validate against a guess. | ||
| attribute_types = { | ||
| assoc["attribute_id"]: assoc["attribute_type"] | ||
| for assoc in document.get("supplemental_attribute_associations", []) | ||
| if "attribute_id" in assoc and "attribute_type" in assoc | ||
| } | ||
| for index, payload in enumerate(document.get("supplemental_attributes", [])): | ||
| type_name = attribute_types.get(payload.get("id")) | ||
| if type_name is None: | ||
| failures.append( | ||
| f"supplemental_attributes[{index}]: id={payload.get('id')} has no " | ||
| "entry in supplemental_attribute_associations, so its type is unknown" | ||
| ) | ||
| continue | ||
| validated += validate_group(models, type_name, [payload], failures) | ||
|
|
||
| for section, type_name in ASSOCIATION_SECTIONS: | ||
| validated += validate_group( | ||
| models, type_name, document.get(section, []), failures | ||
| ) | ||
|
|
||
| print(f"validated {validated} components") | ||
| if failures: | ||
| print(f"{len(failures)} failure(s):", file=sys.stderr) | ||
| for failure in failures: | ||
| print(f" {failure}", file=sys.stderr) | ||
| return 1 | ||
| return 0 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| sys.exit(main()) |
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.
If I run this validation script pointing to inspection_output/modified_14bus_system.NATURAL_UNITS.json or inspection_output/modified_14bus_system.DEVICE_BASE.json I get a bunch of failures. Is that expected?