Skip to content

Compare two specs#

This guide shows how to diff two specification files (specs) so that the diff contains only what the two specs state differently. You need mathspec installed and two spec files that load.

Write a spec in the canonical form#

Write one spec in one fixed layout for a spec (the canonical form), to standard output (stdout), or to a file with -o canonical.yaml. Here spec.yaml is the base spec on composing specs, a least-cost dispatch spec:

python -m mathspec canonical spec.yaml
import mathspec as ms

print(ms.to_spec('spec.yaml').to_yaml(canonical=True), end='')
version: 0
dimensions:
  generator: {}
  snapshot: {dtype: int}
parameters:
  capacity: {dims: [generator]}
  cost: {dims: [generator]}
  load: {dims: [snapshot]}
variables:
  dispatch:
    dims: [generator, snapshot]
    bounds: {lower: 0.0, upper: capacity}
objective:
  expression: sum(cost * dispatch)
constraints:
  power_balance:
    dims: [snapshot]
    expression: sum(dispatch, over=generator) == load

The sections come in a fixed order, and the names within a section and the dims of dispatch are sorted. A field at its default, such as dtype: str or sense: minimize, is left out. The objective is sum(cost * dispatch), although the file has sum(dispatch * cost), because mathspec writes the factors of a product in a fixed order.

Keep this Python session open, because later steps use ms.

Diff two files#

Write both specs in the canonical form, and diff the two. Here before.yaml is the spec.yaml above, and in after.yaml the objective has twice the cost:

python -m mathspec diff before.yaml after.yaml
import difflib

before = ms.to_spec('before.yaml').to_yaml(canonical=True)
after = ms.to_spec('after.yaml').to_yaml(canonical=True)
diff = difflib.unified_diff(before.splitlines(True), after.splitlines(True), 'before.yaml', 'after.yaml')
print(''.join(diff), end='')
--- before.yaml
+++ after.yaml
@@ -11,7 +11,7 @@
     dims: [generator, snapshot]
     bounds: {lower: 0.0, upper: capacity}
 objective:
-  expression: sum(cost * dispatch)
+  expression: sum((2 * cost) * dispatch)
 constraints:
   power_balance:
     dims: [snapshot]

The one changed line is the objective, with the factor 2 in brackets. When the specs differ, the command exits with status 1, and the command line reference lists every exit status. In Python, where the specs differ, before == after is False.

Make git diff show the canonical form#

  1. Tell git which files are specs, and which command writes them out:

    echo 'specs/*.yaml diff=mathspec' >> .gitattributes
    git config diff.mathspec.textconv "python -m mathspec canonical"
    
  2. Run git diff as usual. git diff now shows no difference for a change that writes sum(dispatch * cost) as sum(cost * dispatch), and one line for a change that doubles a coefficient. After the header lines, git diff prints:

    @@ -11,7 +11,7 @@ variables:
         dims: [generator, snapshot]
         bounds: {lower: 0.0, upper: capacity}
     objective:
    -  expression: sum(cost * dispatch)
    +  expression: sum((2 * cost) * dispatch)
     constraints:
       power_balance:
         dims: [snapshot]
    

The files in the repository stay as you wrote them, with their comments, and only the diff changes.

Warning

Match only spec files in .gitattributes, because git runs the command on every file that the pattern matches. mathspec refuses a YAML file that is not a spec, so git diff shows the refusal for that file.

Check in CI that a change leaves the spec the same#

To check that a change only moves declarations or reorders the terms of a sum, run a continuous integration (CI) job. In the job, write out the file on the main branch and compare that file with the file in the change:

git show origin/main:spec.yaml > before.yaml
python -m mathspec diff before.yaml spec.yaml

The job fails when the two specs differ, and the log of the job lists the lines that changed.

See also

  • canonical, for the options of the command.
  • diff, for the options and exit status of the command.
  • Comparing two specs, for what mathspec sorts when it writes the canonical form.