From 28fc946bbebe6bdf44c4ff5d69f6534db9f3101f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 06:28:56 +0000 Subject: [PATCH] docs: what spec.expand() returns is documented on the model writer's python api page, and reading.md keeps only which program an engine reads The Spec.expand docstring, which the Python API page in Reference renders, is now the one home for the call: the kinds and their order, the ValueError, a different model that takes the same data, itself where there is nothing to write out, and no caching. It no longer says "the same math". reading.md's section says which program an engine reads. Five links point at the API entry. The three claims reading.md checked move to tests/test_expand.py, and the page's claim count drops from 22 to 19. Schema regenerated. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01A89tReSh8rTBG9ocy7688y --- docs/about/file-and-program.md | 2 +- docs/about/what-counts-as-public-api.md | 2 +- docs/howto/see-an-expansion.md | 2 +- docs/reference/language/piecewise.md | 2 +- docs/reference/reading.md | 30 +++++-------------------- docs/reference/typeset.md | 2 +- schema/math-spec.schema.json | 2 +- src/math_spec/model.py | 21 +++++++++-------- tests/test_expand.py | 15 +++++++++++++ tests/test_reading_page.py | 2 +- 10 files changed, 39 insertions(+), 41 deletions(-) diff --git a/docs/about/file-and-program.md b/docs/about/file-and-program.md index dda0ffc1..86b6d377 100644 --- a/docs/about/file-and-program.md +++ b/docs/about/file-and-program.md @@ -39,7 +39,7 @@ kept as one declaration. - **The program keeps the model the author wrote.** A curve is one declaration to print and one to explain. Its rows are one formulation of it, so the rows are a second model, which a caller asks for with - [`spec.expand()`](../reference/reading.md#formulations-written-out). + [`spec.expand()`](../reference/api.md#math_spec.Spec.expand). - **The spec keeps the text.** A tool that rewrites a model needs the file as written: `to_yaml()` writes it back, and `expand()` rewrites it. A tree does not give the text back. diff --git a/docs/about/what-counts-as-public-api.md b/docs/about/what-counts-as-public-api.md index 140531b0..e2ba29c4 100644 --- a/docs/about/what-counts-as-public-api.md +++ b/docs/about/what-counts-as-public-api.md @@ -32,7 +32,7 @@ diff, the typesetter prints it, and an engine in another language reads it. talks about a file the language accepts, and changes nothing. - **Nothing is written out unasked.** A `piecewise:` or `sos:` block stays the block until a caller calls - [`spec.expand()`](../reference/reading.md#formulations-written-out). + [`spec.expand()`](../reference/api.md#math_spec.Spec.expand). What a solver or file format can take, how the numbers attach to the names, and which solver runs are each engine's to decide diff --git a/docs/howto/see-an-expansion.md b/docs/howto/see-an-expansion.md index 6bab0b94..8b72c2b7 100644 --- a/docs/howto/see-an-expansion.md +++ b/docs/howto/see-an-expansion.md @@ -428,7 +428,7 @@ the set out too. -[`Spec.expand()`](../reference/reading.md#formulations-written-out) lists what +[`Spec.expand()`](../reference/api.md#math_spec.Spec.expand) lists what the call accepts, and [writing a formulation out](../reference/language/piecewise.md#writing-a-formulation-out) says what each block emits. diff --git a/docs/reference/language/piecewise.md b/docs/reference/language/piecewise.md index 43b08b31..034abd2c 100644 --- a/docs/reference/language/piecewise.md +++ b/docs/reference/language/piecewise.md @@ -165,7 +165,7 @@ expansion writes that the file already declares is refused at load too. ## Writing a formulation out Writing a formulation out replaces the block with the variables and constraints -it states. [`Spec.expand()`](../reading.md#formulations-written-out) is the +it states. [`Spec.expand()`](../api.md#math_spec.Spec.expand) is the call, and [see what a curve or a set expands to](../../howto/see-an-expansion.md) shows a model before and after. diff --git a/docs/reference/reading.md b/docs/reference/reading.md index 302cae1e..42cc7395 100644 --- a/docs/reference/reading.md +++ b/docs/reference/reading.md @@ -76,31 +76,11 @@ is one the file declared. ## Formulations written out -`spec.expand(*kinds)` returns a new `Spec` with each `piecewise:` and `sos:` -block replaced by the variables and constraints it states. -[Writing a formulation out](language/piecewise.md#writing-a-formulation-out) -says what those are. - -```python -expanded = spec.expand() -expanded == spec # False -expanded.expand() is expanded # True -spec.expand('sos') is spec # True -``` - -- **The kinds are `'piecewise'` and `'sos'`, and no argument means both.** Any - other string raises `ValueError`, naming the two. Curves go first whatever - the order of the arguments, so the set a `method: sos2` curve states is - written out too. -- **The expansion is a different model.** It declares more variables and - constraints, so it does not compare equal to the model it came from. It - declares the same dimensions and parameters, so the same data attaches to both. -- **A model with nothing to write out comes back as itself.** So does an - expansion asked for the same kinds again. -- **The spec keeps no expansion.** A second call builds it again. -- **Nothing expands a model unasked.** A program holds its curves until - `expand()` writes them out. The expansion is a model like any other: - `to_yaml()` writes it, and its `program` holds the rows and no curve. +A program holds each curve and each set as one declaration until +[`Spec.expand()`](api.md#math_spec.Spec.expand) writes it out. An engine that +builds rows reads the program of `spec.expand('piecewise')` if it takes a set, +and the program of `spec.expand()` if it does not. The program of an expansion +holds no curve: ```python sorted(rows.piecewise) # [] diff --git a/docs/reference/typeset.md b/docs/reference/typeset.md index 934bdcf2..09fbe747 100644 --- a/docs/reference/typeset.md +++ b/docs/reference/typeset.md @@ -46,7 +46,7 @@ a flag. The [Python API](api.md#typesetting) gives each signature. - The model's `description:` opens the document. - A `piecewise:` block prints as one line: the curve it states, over the frame it states one curve per coordinate of. To print its rows, print - [`spec.expand()`](reading.md#formulations-written-out) or pass `--expand` + [`spec.expand()`](api.md#math_spec.Spec.expand) or pass `--expand` ([see an expansion](../howto/see-an-expansion.md)). - An [`assumptions:`](language/assumptions.md) entry prints under an **Assumptions** heading, last, beside what each curve assumes of its diff --git a/schema/math-spec.schema.json b/schema/math-spec.schema.json index f461aa8a..399403c9 100644 --- a/schema/math-spec.schema.json +++ b/schema/math-spec.schema.json @@ -681,7 +681,7 @@ }, "$schema": "https://json-schema.org/draft/2020-12/schema", "additionalProperties": false, - "description": "The declared math \u2014 one YAML file, or one dict, validated. Nothing here has seen data.\n\nA ``Spec`` that exists has passed the whole language: constructing one by\nany route \u2014 ``to_spec``, :meth:`model_validate`, the constructor \u2014 runs\nevery load-time check, expression pass included, and raises\n:class:`~math_spec.errors.LanguageError` on a model the language refuses.\nHolding one is the proof, so nothing downstream checks it again.\n\nThe API is the eleven declaration sections plus ``version`` and\n``description``, three ways back out \u2014 :meth:`to_dict` for the model as\ndata, :meth:`to_yaml` for the file a reviewer reads, :meth:`expand` for the\nsame math with its formulations written out \u2014 and :attr:`program`, the\nmodel typed, which every reader after load walks. Everything else on this\nclass is pydantic's, not a contract this package keeps.", + "description": "The declared math \u2014 one YAML file, or one dict, validated. Nothing here has seen data.\n\nA ``Spec`` that exists has passed the whole language: constructing one by\nany route \u2014 ``to_spec``, :meth:`model_validate`, the constructor \u2014 runs\nevery load-time check, expression pass included, and raises\n:class:`~math_spec.errors.LanguageError` on a model the language refuses.\nHolding one is the proof, so nothing downstream checks it again.\n\nThe API is the eleven declaration sections plus ``version`` and\n``description``, three ways back out \u2014 :meth:`to_dict` for the model as\ndata, :meth:`to_yaml` for the file a reviewer reads, :meth:`expand` for the\nmodel with its formulations written out as plain rows \u2014 and :attr:`program`, the\nmodel typed, which every reader after load walks. Everything else on this\nclass is pydantic's, not a contract this package keeps.", "properties": { "assumptions": { "additionalProperties": { diff --git a/src/math_spec/model.py b/src/math_spec/model.py index 9910cf17..dccd6feb 100644 --- a/src/math_spec/model.py +++ b/src/math_spec/model.py @@ -712,7 +712,7 @@ class Spec(_StrictBlock): The API is the eleven declaration sections plus ``version`` and ``description``, three ways back out — :meth:`to_dict` for the model as data, :meth:`to_yaml` for the file a reviewer reads, :meth:`expand` for the - same math with its formulations written out — and :attr:`program`, the + model with its formulations written out as plain rows — and :attr:`program`, the model typed, which every reader after load walks. Everything else on this class is pydantic's, not a contract this package keeps. """ @@ -825,11 +825,13 @@ def expand(self, *kinds: Formulation) -> Spec: """This model with its formulations written out as plain variables and constraints. A formulation states rows rather than being one — ``piecewise:`` states - a curve, ``sos:`` states which members of a family may be nonzero — and - expanding one writes those rows under names prefixed with the block's - own, then drops the block. The math is the same afterwards, and so is - the data attached to it: neither a set nor a curve emits a parameter, - and a curve's rows sit on ``where`` predicates over the file's own. + a curve, ``sos:`` states which members of a family may be nonzero. + Expanding one writes those rows under names prefixed with the block's + own, and drops the block. The result is a different model: it declares + more variables and constraints, so it does not compare equal to this + one. It declares the same dimensions and parameters, so the same data + attaches to both. Nothing is cached, so a second call builds the + expansion again. Args: kinds: Which formulations to write out — ``'piecewise'``, @@ -839,9 +841,10 @@ def expand(self, *kinds: Formulation) -> Spec: curve. Returns: - The model those blocks wrote out, or this one where it declares - none of them. It is a model like any other: :meth:`to_yaml` writes - it, and the same data attaches to it as to the one it came from. + The model with those blocks written out, or this same object where + it declares none of them, so an expansion asked for the same kinds + again returns itself. It is a model like any other: :meth:`to_yaml` + writes it, and :attr:`program` holds its rows. Raises: ValueError: *kinds* names something that is not a formulation. diff --git a/tests/test_expand.py b/tests/test_expand.py index 6e22e588..a04d90d6 100644 --- a/tests/test_expand.py +++ b/tests/test_expand.py @@ -161,3 +161,18 @@ def test_the_same_sources_bind_a_model_and_its_expansion(model): written_out = set(spec.expand().program.parameters) assert written_out == supplied, 'writing a formulation out asks for data the model it came from did not' + + +def test_an_expansion_is_a_different_model_and_has_nothing_left_to_write_out(): + """What `expand()` returns: a new model, which a second expansion hands back unchanged.""" + spec = schema_of(CURVE) + expanded = spec.expand() + + assert expanded != spec, 'the expansion declares more rows, so it is a different model' + assert expanded.expand() is expanded, 'an expansion has no formulation left, so it comes back as itself' + + +def test_a_model_with_no_formulation_expands_to_itself(): + spec = schema_of(DISPATCH_MODEL) + + assert spec.expand() is spec, 'nothing to write out returns the same object, not a copy' diff --git a/tests/test_reading_page.py b/tests/test_reading_page.py index 909569e0..7f932ecf 100644 --- a/tests/test_reading_page.py +++ b/tests/test_reading_page.py @@ -55,6 +55,6 @@ def test_the_page_shows_the_declarations_the_expansion_emits(tmp_path, monkeypat exec(compile(code, str(PAGE), 'exec'), namespace) claims.extend(_claims(code)) - assert len(claims) == 22, 'every `expression # value` line on the page is checked; one without one is not' + assert len(claims) == 19, 'every `expression # value` line on the page is checked; one without one is not' for expression, claimed in claims: assert eval(expression, namespace) == claimed, f'reading.md says `{expression}` is {claimed}'