modelwright 0.1.0a3__tar.gz → 0.1.0a5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- modelwright-0.1.0a5/MANIFEST.in +1 -0
- {modelwright-0.1.0a3/src/modelwright.egg-info → modelwright-0.1.0a5}/PKG-INFO +6 -2
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/README.md +1 -1
- modelwright-0.1.0a5/examples/README.md +12 -0
- modelwright-0.1.0a5/examples/fable_2020/README.md +21 -0
- modelwright-0.1.0a5/examples/fable_2020/generated_fable_2020_model.py.xz +0 -0
- modelwright-0.1.0a5/examples/fable_2020/notebook_interface.py +95 -0
- modelwright-0.1.0a5/examples/synthetic/notebook_interface.py +62 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/pyproject.toml +9 -1
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/src/modelwright/__init__.py +43 -1
- modelwright-0.1.0a5/src/modelwright/notebooks.py +246 -0
- modelwright-0.1.0a5/src/modelwright/wrappers.py +426 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5/src/modelwright.egg-info}/PKG-INFO +6 -2
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/src/modelwright.egg-info/SOURCES.txt +13 -1
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/src/modelwright.egg-info/requires.txt +5 -0
- modelwright-0.1.0a5/tests/test_examples.py +41 -0
- modelwright-0.1.0a5/tests/test_fable_wrapper_benchmark.py +111 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/tests/test_import.py +1 -1
- modelwright-0.1.0a5/tests/test_notebooks.py +211 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/tests/test_public_api.py +9 -0
- modelwright-0.1.0a5/tests/test_wrappers.py +212 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/LICENSE +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/setup.cfg +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/src/modelwright/cli.py +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/src/modelwright/conversion.py +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/src/modelwright/evaluation.py +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/src/modelwright/execution.py +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/src/modelwright/extraction.py +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/src/modelwright/formulas.py +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/src/modelwright/formulas_oracle.py +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/src/modelwright/generation.py +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/src/modelwright/graph.py +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/src/modelwright/oracle_validation.py +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/src/modelwright/oracles.py +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/src/modelwright/references.py +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/src/modelwright/validation.py +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/src/modelwright.egg-info/dependency_links.txt +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/src/modelwright.egg-info/entry_points.txt +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/src/modelwright.egg-info/top_level.txt +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/tests/test_cli.py +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/tests/test_conversion_plan.py +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/tests/test_dependency_graph.py +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/tests/test_evaluation_orchestration.py +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/tests/test_extraction_records.py +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/tests/test_formula_expressions.py +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/tests/test_formula_translation.py +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/tests/test_formulas_oracle.py +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/tests/test_generated_execution.py +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/tests/test_generation_contract.py +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/tests/test_materialize_fable_benchmarks.py +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/tests/test_openpyxl_extraction.py +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/tests/test_oracle_backed_validation.py +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/tests/test_oracle_interface.py +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/tests/test_python_generation.py +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/tests/test_references.py +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/tests/test_scalar_comparison.py +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/tests/test_supported_semantics_fixture.py +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/tests/test_synthetic_fixture.py +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/tests/test_validation.py +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/tests/test_validation_regression.py +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/tests/test_validation_report_builder.py +0 -0
- {modelwright-0.1.0a3 → modelwright-0.1.0a5}/tests/test_validation_scenario.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
recursive-include examples *.md *.py *.xz
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: modelwright
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.0a5
|
|
4
4
|
Summary: Tools for converting spreadsheet workbooks into transparent Python models.
|
|
5
5
|
Author: UBC FRESH Lab
|
|
6
6
|
License-Expression: MIT
|
|
@@ -32,11 +32,14 @@ Requires-Dist: sphinx-rtd-theme>=2; extra == "docs"
|
|
|
32
32
|
Provides-Extra: dev
|
|
33
33
|
Requires-Dist: build>=1.2; extra == "dev"
|
|
34
34
|
Requires-Dist: formulas; extra == "dev"
|
|
35
|
+
Requires-Dist: pandas>=2; extra == "dev"
|
|
35
36
|
Requires-Dist: pytest>=8; extra == "dev"
|
|
36
37
|
Requires-Dist: ruff>=0.8; extra == "dev"
|
|
37
38
|
Requires-Dist: sphinx>=7; extra == "dev"
|
|
38
39
|
Requires-Dist: sphinx-rtd-theme>=2; extra == "dev"
|
|
39
40
|
Requires-Dist: twine>=5; extra == "dev"
|
|
41
|
+
Provides-Extra: notebook
|
|
42
|
+
Requires-Dist: pandas>=2; extra == "notebook"
|
|
40
43
|
Provides-Extra: oracle
|
|
41
44
|
Requires-Dist: formulas; extra == "oracle"
|
|
42
45
|
Provides-Extra: quality
|
|
@@ -46,6 +49,7 @@ Requires-Dist: build>=1.2; extra == "release"
|
|
|
46
49
|
Requires-Dist: twine>=5; extra == "release"
|
|
47
50
|
Provides-Extra: test
|
|
48
51
|
Requires-Dist: formulas; extra == "test"
|
|
52
|
+
Requires-Dist: pandas>=2; extra == "test"
|
|
49
53
|
Requires-Dist: pytest>=8; extra == "test"
|
|
50
54
|
Dynamic: license-file
|
|
51
55
|
|
|
@@ -135,7 +139,7 @@ Restore the public external FABLE benchmark workbooks into ignored local paths:
|
|
|
135
139
|
scripts/bootstrap_dev_env.sh --benchmarks
|
|
136
140
|
```
|
|
137
141
|
|
|
138
|
-
`modelwright` is pre-release. The current alpha line is `0.1.
|
|
142
|
+
`modelwright` is pre-release. The current alpha line is `0.1.0a5`; alpha releases must not be described as full-workbook conversion guarantees.
|
|
139
143
|
|
|
140
144
|
Check release artifacts locally:
|
|
141
145
|
|
|
@@ -84,7 +84,7 @@ Restore the public external FABLE benchmark workbooks into ignored local paths:
|
|
|
84
84
|
scripts/bootstrap_dev_env.sh --benchmarks
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
`modelwright` is pre-release. The current alpha line is `0.1.
|
|
87
|
+
`modelwright` is pre-release. The current alpha line is `0.1.0a5`; alpha releases must not be described as full-workbook conversion guarantees.
|
|
88
88
|
|
|
89
89
|
Check release artifacts locally:
|
|
90
90
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Modelwright Examples
|
|
2
|
+
|
|
3
|
+
These examples show how generated Modelwright Python models can be wrapped with analyst-facing
|
|
4
|
+
facades and notebook-friendly DataFrame helpers.
|
|
5
|
+
|
|
6
|
+
- `synthetic/`: a tiny generated-model example based on the tracked synthetic fixture shape.
|
|
7
|
+
- `fable_2020/`: a production-size example using a compressed generated Python model converted from
|
|
8
|
+
the public 2020 FABLE Calculator benchmark workbook.
|
|
9
|
+
|
|
10
|
+
The original FABLE workbook is not tracked here. The tracked FABLE example contains Modelwright's
|
|
11
|
+
generated Python output, compressed as `generated_fable_2020_model.py.xz` because the uncompressed
|
|
12
|
+
module is larger than ordinary GitHub per-file limits.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# 2020 FABLE Generated Model Example
|
|
2
|
+
|
|
3
|
+
This directory contains a compressed generated Python model produced by Modelwright from the public
|
|
4
|
+
2020 FABLE Calculator benchmark workbook.
|
|
5
|
+
|
|
6
|
+
The original workbook is not tracked in this repository. The generated model is tracked as
|
|
7
|
+
`generated_fable_2020_model.py.xz` because the uncompressed Python module is about 117 MiB, which is
|
|
8
|
+
larger than ordinary GitHub per-file limits. The example script decompresses it into ignored `tmp/`
|
|
9
|
+
working space before importing it.
|
|
10
|
+
|
|
11
|
+
The generated model preserves the P26 full-validation evidence boundary:
|
|
12
|
+
|
|
13
|
+
- comparable cached outputs: 281,741;
|
|
14
|
+
- matches: 281,741;
|
|
15
|
+
- mismatches: 0.
|
|
16
|
+
|
|
17
|
+
Run from the repository root:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
python examples/fable_2020/notebook_interface.py
|
|
21
|
+
```
|
|
Binary file
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Notebook-interface example for the generated 2020 FABLE benchmark model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.util
|
|
6
|
+
import lzma
|
|
7
|
+
import shutil
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from types import ModuleType
|
|
11
|
+
|
|
12
|
+
from modelwright.notebooks import outputs_frame, report_frames, table_frame
|
|
13
|
+
from modelwright.wrappers import ModelFacade, cell, report, table
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
EXAMPLE_DIR = Path(__file__).resolve().parent
|
|
17
|
+
ARCHIVE_PATH = EXAMPLE_DIR / "generated_fable_2020_model.py.xz"
|
|
18
|
+
WORK_DIR = Path("tmp/examples/fable_2020")
|
|
19
|
+
MODEL_PATH = WORK_DIR / "generated_fable_2020_model.py"
|
|
20
|
+
|
|
21
|
+
FABLE_SCENARIO_OUTPUTS = {
|
|
22
|
+
"SCENARIOS selection!D20": 2.146115426018433,
|
|
23
|
+
"SCENARIOS selection!D21": 1.8982220554032356,
|
|
24
|
+
"SCENARIOS selection!D22": 1.462761288724012,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def materialize_generated_model() -> Path:
|
|
29
|
+
"""Decompress the tracked generated model into ignored local working space."""
|
|
30
|
+
|
|
31
|
+
WORK_DIR.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
if not MODEL_PATH.exists():
|
|
33
|
+
with lzma.open(ARCHIVE_PATH, "rb") as source:
|
|
34
|
+
with MODEL_PATH.open("wb") as target:
|
|
35
|
+
shutil.copyfileobj(source, target)
|
|
36
|
+
return MODEL_PATH
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load_generated_model(path: Path | None = None) -> ModuleType:
|
|
40
|
+
model_path = path or materialize_generated_model()
|
|
41
|
+
spec = importlib.util.spec_from_file_location("modelwright_example_fable_2020", model_path)
|
|
42
|
+
if spec is None or spec.loader is None:
|
|
43
|
+
raise RuntimeError(f"could not load generated model from {model_path}")
|
|
44
|
+
module = importlib.util.module_from_spec(spec)
|
|
45
|
+
sys.modules[spec.name] = module
|
|
46
|
+
spec.loader.exec_module(module)
|
|
47
|
+
return module
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def build_facade(module: ModuleType | None = None) -> ModelFacade:
|
|
51
|
+
generated_model = module or load_generated_model()
|
|
52
|
+
return ModelFacade(
|
|
53
|
+
generated_model,
|
|
54
|
+
cells=[
|
|
55
|
+
cell("SCENARIOS selection!D20", name="scenario_metric_1", role="output"),
|
|
56
|
+
cell("SCENARIOS selection!D21", name="scenario_metric_2", role="output"),
|
|
57
|
+
cell("SCENARIOS selection!D22", name="scenario_metric_3", role="output"),
|
|
58
|
+
],
|
|
59
|
+
tables=[
|
|
60
|
+
table(
|
|
61
|
+
"scenario_selection_slice",
|
|
62
|
+
sheet="SCENARIOS selection",
|
|
63
|
+
range_ref="D20:D22",
|
|
64
|
+
row_labels=["d20", "d21", "d22"],
|
|
65
|
+
column_labels=["value"],
|
|
66
|
+
)
|
|
67
|
+
],
|
|
68
|
+
reports=[
|
|
69
|
+
report(
|
|
70
|
+
"scenario_selection",
|
|
71
|
+
cells=["scenario_metric_1", "scenario_metric_2", "scenario_metric_3"],
|
|
72
|
+
tables=["scenario_selection_slice"],
|
|
73
|
+
)
|
|
74
|
+
],
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def run_example():
|
|
79
|
+
facade = build_facade()
|
|
80
|
+
values = facade.calculate()
|
|
81
|
+
for cell_ref, expected in FABLE_SCENARIO_OUTPUTS.items():
|
|
82
|
+
observed = values[cell_ref]
|
|
83
|
+
if observed != expected:
|
|
84
|
+
raise RuntimeError(f"{cell_ref} expected {expected!r}, observed {observed!r}")
|
|
85
|
+
return {
|
|
86
|
+
"outputs": outputs_frame(facade),
|
|
87
|
+
"scenario_selection_slice": table_frame(facade, "scenario_selection_slice"),
|
|
88
|
+
"report": report_frames(facade, "scenario_selection"),
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
if __name__ == "__main__":
|
|
93
|
+
frames = run_example()
|
|
94
|
+
print(frames["outputs"])
|
|
95
|
+
print(frames["scenario_selection_slice"])
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Tiny notebook-interface example for a generated Modelwright model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from modelwright.notebooks import compare_scenarios_frame, outputs_frame, table_frame
|
|
6
|
+
from modelwright.wrappers import ModelFacade, cell, report, table
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def calculate(inputs=None):
|
|
10
|
+
"""Small stand-in for generated Modelwright Python output."""
|
|
11
|
+
|
|
12
|
+
inputs = inputs or {}
|
|
13
|
+
base = inputs.get("Inputs!B2", 100)
|
|
14
|
+
growth = inputs.get("Inputs!B3", 0.1)
|
|
15
|
+
return {
|
|
16
|
+
"Summary!B2": base * (1 + growth),
|
|
17
|
+
"Summary!C2": base * 2,
|
|
18
|
+
"Summary!B3": "ok",
|
|
19
|
+
"Summary!C3": base + 5,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def build_facade() -> ModelFacade:
|
|
24
|
+
return ModelFacade(
|
|
25
|
+
calculate,
|
|
26
|
+
cells=[
|
|
27
|
+
cell("Inputs!B2", name="base", label="Base volume", role="input", unit="t"),
|
|
28
|
+
cell("Inputs!B3", name="growth", label="Growth rate", role="input", unit="fraction"),
|
|
29
|
+
cell("Summary!B2", name="projected", label="Projected volume", role="output", unit="t"),
|
|
30
|
+
cell("Summary!B3", name="status", label="Status", role="output"),
|
|
31
|
+
],
|
|
32
|
+
tables=[
|
|
33
|
+
table(
|
|
34
|
+
"summary_grid",
|
|
35
|
+
sheet="Summary",
|
|
36
|
+
range_ref="B2:C3",
|
|
37
|
+
row_labels=["volume", "status"],
|
|
38
|
+
column_labels=["primary", "secondary"],
|
|
39
|
+
)
|
|
40
|
+
],
|
|
41
|
+
reports=[
|
|
42
|
+
report("summary", cells=["base", "projected", "status"], tables=["summary_grid"]),
|
|
43
|
+
],
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def run_example():
|
|
48
|
+
facade = build_facade()
|
|
49
|
+
baseline = facade.scenario(name="baseline", inputs={"Inputs!B2": 100, "Inputs!B3": 0.1})
|
|
50
|
+
shock = baseline.with_input("Inputs!B2", 120)
|
|
51
|
+
return {
|
|
52
|
+
"outputs": outputs_frame(facade, shock),
|
|
53
|
+
"summary_grid": table_frame(facade, "summary_grid", shock),
|
|
54
|
+
"comparison": compare_scenarios_frame(facade, baseline, shock),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
if __name__ == "__main__":
|
|
59
|
+
frames = run_example()
|
|
60
|
+
for name, frame in frames.items():
|
|
61
|
+
print(f"\n{name}")
|
|
62
|
+
print(frame)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "modelwright"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.0a5"
|
|
8
8
|
description = "Tools for converting spreadsheet workbooks into transparent Python models."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -56,12 +56,16 @@ docs = [
|
|
|
56
56
|
dev = [
|
|
57
57
|
"build>=1.2",
|
|
58
58
|
"formulas",
|
|
59
|
+
"pandas>=2",
|
|
59
60
|
"pytest>=8",
|
|
60
61
|
"ruff>=0.8",
|
|
61
62
|
"sphinx>=7",
|
|
62
63
|
"sphinx-rtd-theme>=2",
|
|
63
64
|
"twine>=5"
|
|
64
65
|
]
|
|
66
|
+
notebook = [
|
|
67
|
+
"pandas>=2"
|
|
68
|
+
]
|
|
65
69
|
oracle = [
|
|
66
70
|
"formulas"
|
|
67
71
|
]
|
|
@@ -74,6 +78,7 @@ release = [
|
|
|
74
78
|
]
|
|
75
79
|
test = [
|
|
76
80
|
"formulas",
|
|
81
|
+
"pandas>=2",
|
|
77
82
|
"pytest>=8"
|
|
78
83
|
]
|
|
79
84
|
|
|
@@ -82,6 +87,9 @@ where = ["src"]
|
|
|
82
87
|
|
|
83
88
|
[tool.pytest.ini_options]
|
|
84
89
|
testpaths = ["tests"]
|
|
90
|
+
markers = [
|
|
91
|
+
"benchmark: tests that require local benchmark artifacts and are skipped unless explicitly enabled"
|
|
92
|
+
]
|
|
85
93
|
|
|
86
94
|
[tool.ruff]
|
|
87
95
|
line-length = 120
|
|
@@ -54,6 +54,15 @@ from modelwright.graph import (
|
|
|
54
54
|
DependencyGraph,
|
|
55
55
|
build_dependency_graph,
|
|
56
56
|
)
|
|
57
|
+
from modelwright.notebooks import (
|
|
58
|
+
NotebookDependencyError,
|
|
59
|
+
compare_scenarios_frame,
|
|
60
|
+
inputs_frame,
|
|
61
|
+
outputs_frame,
|
|
62
|
+
report_frames,
|
|
63
|
+
scenario_frame,
|
|
64
|
+
table_frame,
|
|
65
|
+
)
|
|
57
66
|
from modelwright.oracles import (
|
|
58
67
|
OracleDiagnostic,
|
|
59
68
|
OracleRequest,
|
|
@@ -80,11 +89,27 @@ from modelwright.validation import (
|
|
|
80
89
|
compare_scalar_output,
|
|
81
90
|
load_validation_scenario,
|
|
82
91
|
)
|
|
92
|
+
from modelwright.wrappers import (
|
|
93
|
+
CellRef,
|
|
94
|
+
CellView,
|
|
95
|
+
ModelFacade,
|
|
96
|
+
ReportRef,
|
|
97
|
+
Scenario,
|
|
98
|
+
TableRef,
|
|
99
|
+
TableView,
|
|
100
|
+
WrapperDeclarationError,
|
|
101
|
+
WrapperExecutionError,
|
|
102
|
+
cell,
|
|
103
|
+
report,
|
|
104
|
+
table,
|
|
105
|
+
)
|
|
83
106
|
|
|
84
|
-
__version__ = "0.1.
|
|
107
|
+
__version__ = "0.1.0a5"
|
|
85
108
|
|
|
86
109
|
__all__ = [
|
|
87
110
|
"CellRecord",
|
|
111
|
+
"CellRef",
|
|
112
|
+
"CellView",
|
|
88
113
|
"ComparisonResult",
|
|
89
114
|
"ComparisonRules",
|
|
90
115
|
"ConversionPlan",
|
|
@@ -109,7 +134,9 @@ __all__ = [
|
|
|
109
134
|
"GenerationDiagnostic",
|
|
110
135
|
"GenerationResult",
|
|
111
136
|
"MISSING_VALUE",
|
|
137
|
+
"ModelFacade",
|
|
112
138
|
"NamedRangeRecord",
|
|
139
|
+
"NotebookDependencyError",
|
|
113
140
|
"OracleConfig",
|
|
114
141
|
"OracleDiagnostic",
|
|
115
142
|
"OracleRequest",
|
|
@@ -117,10 +144,14 @@ __all__ = [
|
|
|
117
144
|
"PlanRecommendation",
|
|
118
145
|
"PrivacyReview",
|
|
119
146
|
"ResidualBlocker",
|
|
147
|
+
"ReportRef",
|
|
148
|
+
"Scenario",
|
|
120
149
|
"ScenarioInput",
|
|
121
150
|
"ScenarioOutput",
|
|
122
151
|
"SheetRecord",
|
|
152
|
+
"TableRef",
|
|
123
153
|
"TableRecord",
|
|
154
|
+
"TableView",
|
|
124
155
|
"ValidationReport",
|
|
125
156
|
"ValidationEvaluationResult",
|
|
126
157
|
"ValidationScenario",
|
|
@@ -129,20 +160,31 @@ __all__ = [
|
|
|
129
160
|
"WorkbookReference",
|
|
130
161
|
"WorkbookRecord",
|
|
131
162
|
"WorkflowStatus",
|
|
163
|
+
"WrapperDeclarationError",
|
|
164
|
+
"WrapperExecutionError",
|
|
132
165
|
"__version__",
|
|
133
166
|
"build_dependency_graph",
|
|
134
167
|
"build_conversion_plan",
|
|
135
168
|
"build_formula_reference_index",
|
|
136
169
|
"build_oracle_validation_report",
|
|
137
170
|
"build_validation_report",
|
|
171
|
+
"cell",
|
|
138
172
|
"compare_scalar_output",
|
|
173
|
+
"compare_scenarios_frame",
|
|
139
174
|
"execute_generated_model",
|
|
140
175
|
"evaluate_generated_model",
|
|
141
176
|
"extract_workbook",
|
|
142
177
|
"generate_python_module",
|
|
143
178
|
"infer_generated_module_contract",
|
|
179
|
+
"inputs_frame",
|
|
144
180
|
"load_validation_scenario",
|
|
145
181
|
"normalize_cell_reference",
|
|
146
182
|
"normalize_reference",
|
|
183
|
+
"outputs_frame",
|
|
184
|
+
"report",
|
|
185
|
+
"report_frames",
|
|
186
|
+
"scenario_frame",
|
|
187
|
+
"table",
|
|
188
|
+
"table_frame",
|
|
147
189
|
"translate_formula_cell",
|
|
148
190
|
]
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""Notebook-friendly DataFrame helpers for wrapped generated models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable, Mapping
|
|
6
|
+
from numbers import Real
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
from modelwright.references import normalize_cell_reference
|
|
10
|
+
from modelwright.wrappers import CellRef, ModelFacade, Scenario, WrapperDeclarationError
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
import pandas as pd
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NotebookDependencyError(RuntimeError):
|
|
17
|
+
"""Raised when notebook helpers need an optional dependency that is not installed."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def inputs_frame(facade: ModelFacade, scenario: Scenario | None = None) -> "pd.DataFrame":
|
|
21
|
+
"""Return declared facade inputs as a tidy pandas DataFrame."""
|
|
22
|
+
|
|
23
|
+
values = _values_for(facade, scenario)
|
|
24
|
+
return _cell_frame(facade.inputs().values(), values)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def outputs_frame(facade: ModelFacade, scenario: Scenario | None = None) -> "pd.DataFrame":
|
|
28
|
+
"""Return declared facade outputs as a tidy pandas DataFrame."""
|
|
29
|
+
|
|
30
|
+
values = _values_for(facade, scenario)
|
|
31
|
+
return _cell_frame(facade.outputs().values(), values)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def scenario_frame(scenario: Scenario) -> "pd.DataFrame":
|
|
35
|
+
"""Return scenario input overrides as a tidy pandas DataFrame."""
|
|
36
|
+
|
|
37
|
+
pd = _load_pandas()
|
|
38
|
+
return pd.DataFrame(
|
|
39
|
+
[
|
|
40
|
+
{
|
|
41
|
+
"scenario": scenario.name,
|
|
42
|
+
"cell_ref": cell_ref,
|
|
43
|
+
"value": value,
|
|
44
|
+
}
|
|
45
|
+
for cell_ref, value in scenario.inputs.items()
|
|
46
|
+
],
|
|
47
|
+
columns=["scenario", "cell_ref", "value"],
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def table_frame(facade: ModelFacade, name: str, scenario: Scenario | None = None) -> "pd.DataFrame":
|
|
52
|
+
"""Return a declared facade table as a pandas DataFrame."""
|
|
53
|
+
|
|
54
|
+
pd = _load_pandas()
|
|
55
|
+
table = facade.table(name, scenario=scenario)
|
|
56
|
+
frame = pd.DataFrame(table.values, index=list(table.rows), columns=list(table.columns))
|
|
57
|
+
frame.index.name = "row"
|
|
58
|
+
frame.attrs.update(
|
|
59
|
+
{
|
|
60
|
+
"name": table.name,
|
|
61
|
+
"label": table.label,
|
|
62
|
+
"description": table.description,
|
|
63
|
+
"sheet": table.sheet,
|
|
64
|
+
"range_ref": table.range_ref,
|
|
65
|
+
"cell_refs": [list(row) for row in table.cell_refs],
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
return frame
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def report_frames(facade: ModelFacade, name: str, scenario: Scenario | None = None) -> dict[str, Any]:
|
|
72
|
+
"""Return DataFrame payloads for a declared facade report."""
|
|
73
|
+
|
|
74
|
+
report = facade.reports.get(name)
|
|
75
|
+
if report is None:
|
|
76
|
+
raise WrapperDeclarationError(f"unknown report declaration {name!r}")
|
|
77
|
+
|
|
78
|
+
values = _values_for(facade, scenario)
|
|
79
|
+
cells = [facade.cells[cell_name] for cell_name in report.cells]
|
|
80
|
+
return {
|
|
81
|
+
"name": report.name,
|
|
82
|
+
"label": report.label,
|
|
83
|
+
"description": report.description,
|
|
84
|
+
"cells": _cell_frame(cells, values),
|
|
85
|
+
"tables": {
|
|
86
|
+
table_name: table_frame(facade, table_name, scenario=scenario)
|
|
87
|
+
for table_name in report.tables
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def compare_scenarios_frame(
|
|
93
|
+
facade: ModelFacade,
|
|
94
|
+
baseline: Scenario,
|
|
95
|
+
scenario: Scenario,
|
|
96
|
+
*,
|
|
97
|
+
cells: Iterable[str] | None = None,
|
|
98
|
+
) -> "pd.DataFrame":
|
|
99
|
+
"""Compare declared output cells between two scenarios as a tidy DataFrame."""
|
|
100
|
+
|
|
101
|
+
baseline_values = _values_for(facade, baseline)
|
|
102
|
+
scenario_values = _values_for(facade, scenario)
|
|
103
|
+
declarations = _comparison_cells(facade, cells)
|
|
104
|
+
|
|
105
|
+
rows = []
|
|
106
|
+
for declaration in declarations:
|
|
107
|
+
baseline_value = baseline_values.get(declaration.cell_ref)
|
|
108
|
+
scenario_value = scenario_values.get(declaration.cell_ref)
|
|
109
|
+
absolute_change = _absolute_change(baseline_value, scenario_value)
|
|
110
|
+
rows.append(
|
|
111
|
+
{
|
|
112
|
+
"name": declaration.name,
|
|
113
|
+
"label": declaration.label,
|
|
114
|
+
"cell_ref": declaration.cell_ref,
|
|
115
|
+
"baseline_value": baseline_value,
|
|
116
|
+
"scenario_value": scenario_value,
|
|
117
|
+
"absolute_change": absolute_change,
|
|
118
|
+
"percent_change": _percent_change(baseline_value, absolute_change),
|
|
119
|
+
"unit": declaration.unit,
|
|
120
|
+
"role": declaration.role,
|
|
121
|
+
"description": declaration.description,
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
pd = _load_pandas()
|
|
126
|
+
return pd.DataFrame(
|
|
127
|
+
rows,
|
|
128
|
+
columns=[
|
|
129
|
+
"name",
|
|
130
|
+
"label",
|
|
131
|
+
"cell_ref",
|
|
132
|
+
"baseline_value",
|
|
133
|
+
"scenario_value",
|
|
134
|
+
"absolute_change",
|
|
135
|
+
"percent_change",
|
|
136
|
+
"unit",
|
|
137
|
+
"role",
|
|
138
|
+
"description",
|
|
139
|
+
],
|
|
140
|
+
dtype=object,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _cell_frame(cells: Iterable[CellRef], values: Mapping[str, object]) -> "pd.DataFrame":
|
|
145
|
+
pd = _load_pandas()
|
|
146
|
+
return pd.DataFrame(
|
|
147
|
+
[
|
|
148
|
+
{
|
|
149
|
+
"name": declaration.name,
|
|
150
|
+
"label": declaration.label,
|
|
151
|
+
"cell_ref": declaration.cell_ref,
|
|
152
|
+
"role": declaration.role,
|
|
153
|
+
"unit": declaration.unit,
|
|
154
|
+
"description": declaration.description,
|
|
155
|
+
"value": values.get(declaration.cell_ref),
|
|
156
|
+
"has_value": declaration.cell_ref in values,
|
|
157
|
+
}
|
|
158
|
+
for declaration in cells
|
|
159
|
+
],
|
|
160
|
+
columns=[
|
|
161
|
+
"name",
|
|
162
|
+
"label",
|
|
163
|
+
"cell_ref",
|
|
164
|
+
"role",
|
|
165
|
+
"unit",
|
|
166
|
+
"description",
|
|
167
|
+
"value",
|
|
168
|
+
"has_value",
|
|
169
|
+
],
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _comparison_cells(facade: ModelFacade, cells: Iterable[str] | None) -> list[CellRef]:
|
|
174
|
+
declarations_by_name = facade.cells
|
|
175
|
+
if cells is None:
|
|
176
|
+
return list(facade.outputs().values())
|
|
177
|
+
|
|
178
|
+
declarations = []
|
|
179
|
+
for selector in cells:
|
|
180
|
+
declaration = declarations_by_name.get(selector)
|
|
181
|
+
if declaration is not None:
|
|
182
|
+
declarations.append(declaration)
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
normalized = _normalize_cell_ref(selector)
|
|
186
|
+
for candidate in declarations_by_name.values():
|
|
187
|
+
if candidate.cell_ref == normalized:
|
|
188
|
+
declarations.append(candidate)
|
|
189
|
+
break
|
|
190
|
+
else:
|
|
191
|
+
declarations.append(CellRef(cell_ref=normalized, name=normalized))
|
|
192
|
+
return declarations
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _values_for(facade: ModelFacade, scenario: Scenario | None) -> dict[str, object]:
|
|
196
|
+
facade_values_for = getattr(facade, "_values_for", None)
|
|
197
|
+
if callable(facade_values_for):
|
|
198
|
+
return dict(facade_values_for(scenario))
|
|
199
|
+
|
|
200
|
+
active_scenario = scenario or facade.scenario()
|
|
201
|
+
values = facade.calculate(active_scenario)
|
|
202
|
+
return {**active_scenario.inputs, **values}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _normalize_cell_ref(cell_ref: str) -> str:
|
|
206
|
+
normalized = normalize_cell_reference(cell_ref)
|
|
207
|
+
if normalized.kind != "cell" or normalized.sheet is None:
|
|
208
|
+
raise WrapperDeclarationError(f"expected a full cell reference like 'Sheet!A1', got {cell_ref!r}")
|
|
209
|
+
return normalized.normalized
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _absolute_change(baseline_value: object, scenario_value: object) -> float | int | None:
|
|
213
|
+
if _is_number(baseline_value) and _is_number(scenario_value):
|
|
214
|
+
return scenario_value - baseline_value
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _percent_change(baseline_value: object, absolute_change: object) -> float | None:
|
|
219
|
+
if _is_number(baseline_value) and baseline_value != 0 and _is_number(absolute_change):
|
|
220
|
+
return absolute_change / baseline_value
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _is_number(value: object) -> bool:
|
|
225
|
+
return isinstance(value, Real) and not isinstance(value, bool)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _load_pandas() -> Any:
|
|
229
|
+
try:
|
|
230
|
+
import pandas as pd
|
|
231
|
+
except ImportError as error:
|
|
232
|
+
raise NotebookDependencyError(
|
|
233
|
+
"Install modelwright[notebook] to use pandas-backed notebook helpers."
|
|
234
|
+
) from error
|
|
235
|
+
return pd
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
__all__ = [
|
|
239
|
+
"NotebookDependencyError",
|
|
240
|
+
"compare_scenarios_frame",
|
|
241
|
+
"inputs_frame",
|
|
242
|
+
"outputs_frame",
|
|
243
|
+
"report_frames",
|
|
244
|
+
"scenario_frame",
|
|
245
|
+
"table_frame",
|
|
246
|
+
]
|