Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/about/file-and-program.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion docs/about/what-counts-as-public-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/howto/see-an-expansion.md
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,7 @@ the set out too.
<!-- expansion:curve:end -->
<!-- prettier-ignore-end -->

[`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.
2 changes: 1 addition & 1 deletion docs/reference/language/piecewise.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
30 changes: 5 additions & 25 deletions docs/reference/reading.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) # []
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/typeset.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion schema/math-spec.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
21 changes: 12 additions & 9 deletions src/math_spec/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
Expand Down Expand Up @@ -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'``,
Expand All @@ -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.
Expand Down
15 changes: 15 additions & 0 deletions tests/test_expand.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
2 changes: 1 addition & 1 deletion tests/test_reading_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}'
Loading