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}'