Wrapping Generated Models

Generated Modelwright modules are calculation artifacts. They expose a small Python boundary, usually calculate(inputs=None) -> dict, where keys are workbook cell references such as "Summary!B2". That boundary is useful for validation and automation, but it is not a comfortable analyst interface by itself.

The modelwright.wrappers module provides lightweight template primitives for building custom facades around generated models. A facade can name meaningful inputs, define rectangular table views, bundle reports, and preserve provenance back to the original workbook references.

Boundary

Wrapper facades do not edit generated source code. They pass scenario input overrides to the generated model and structure the returned values.

They also do not recreate a spreadsheet UI. Formatting, merged cells, hidden rows, formulas-as-visual objects, and Excel editing remain outside this alpha API.

Minimal Example

Assume a generated model module has a calculate function:

def calculate(inputs=None):
    inputs = inputs or {}
    base = inputs.get("Inputs!B2", 100)
    growth = inputs.get("Inputs!B3", 0.1)
    return {
        "Summary!B2": base * (1 + growth),
        "Summary!C2": base * 2,
        "Summary!B3": "ok",
        "Summary!C3": base + 5,
    }

Define a wrapper facade:

from modelwright.wrappers import ModelFacade, cell, report, table

facade = ModelFacade(
    generated_model,
    cells=[
        cell("Inputs!B2", name="base", label="Base volume", role="input", unit="t"),
        cell("Inputs!B3", name="growth", label="Growth rate", role="input", unit="fraction"),
        cell("Summary!B2", name="projected", label="Projected volume", role="output", unit="t"),
    ],
    tables=[
        table(
            "summary_grid",
            sheet="Summary",
            range_ref="B2:C3",
            row_labels=["volume", "status"],
            column_labels=["primary", "secondary"],
        )
    ],
    reports=[
        report("summary", cells=["base", "projected"], tables=["summary_grid"]),
    ],
)

Run a scenario:

scenario = facade.scenario(inputs={"Inputs!B2": 50}).with_input("Inputs!B3", 0.2)
outputs = facade.calculate(scenario)

Inspect a declared cell:

projected = facade.inspect("Summary!B2", scenario)
assert projected.value == 60.0
assert projected.cell_ref == "Summary!B2"

Read a table payload:

table_payload = facade.table("summary_grid", scenario).to_dict()

assert table_payload["rows"] == ["volume", "status"]
assert table_payload["columns"] == ["primary", "secondary"]
assert table_payload["values"] == [[60.0, 100], ["ok", 55]]

Build a report payload:

summary = facade.report("summary", scenario)
assert summary["cells"]["base"]["value"] == 50
assert summary["tables"]["summary_grid"]["values"][0][0] == 60.0

Scenario Inputs

Scenario objects are copy-on-write input override sets:

baseline = facade.scenario(inputs={"Inputs!B2": 100})
high_growth = baseline.with_input("Inputs!B3", 0.25)

assert baseline.inputs == {"Inputs!B2": 100}
assert high_growth.inputs == {"Inputs!B2": 100, "Inputs!B3": 0.25}

Mutation means changing scenario inputs, not editing generated model code.

Table Declarations

The first table API is intentionally rectangular and explicit. A declaration such as sheet="Summary", range_ref="B2:C3" expands to row-major cell refs:

[["Summary!B2", "Summary!C2"], ["Summary!B3", "Summary!C3"]]

If row or column labels are supplied, their counts must match the range shape. When labels are omitted, the wrapper uses workbook row numbers and column letters as stable fallback labels.

Alpha Limits

The wrapper API is provisional. It is intended to make custom generated-model facades easier to write, not to promise complete automatic workbook semantic recovery.

Current non-goals:

  • full spreadsheet UI recreation;

  • Excel-backed execution;

  • automatic interpretation of all workbook layouts;

  • visual formatting or workbook editing;

  • stable public API compatibility before real wrapper use cases harden the design.