diff --git a/docs/conf.py b/docs/conf.py index 4e376ee9..461da9a0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -74,6 +74,7 @@ ("py:class", "scanspec.core.C"), ("py:class", "scanspec.core.T"), ("py:class", "pydantic.config.ConfigDict"), + ("py:class", "numpy.float64"), ] # Both the class’ and the __init__ method’s docstring are concatenated and diff --git a/docs/explanations/technical-terms.rst b/docs/explanations/technical-terms.rst index 098691a8..f1f04948 100644 --- a/docs/explanations/technical-terms.rst +++ b/docs/explanations/technical-terms.rst @@ -13,7 +13,7 @@ documentation. Consider a 1D line scan: Axis ---- -A fixed reference that can be scanned, i.e. a motor or the `frame_` `DURATION`. +A fixed reference that can be scanned, i.e. a motor. In the diagram above, the Axis is ``x``. `Spec.axes` will return the Axes that should be scanned for a given Spec. diff --git a/docs/how-to/iterate-a-spec.rst b/docs/how-to/iterate-a-spec.rst index 24e465a5..a3b1dd0f 100644 --- a/docs/how-to/iterate-a-spec.rst +++ b/docs/how-to/iterate-a-spec.rst @@ -30,7 +30,7 @@ If you need to do a fly scan If you are conducting a fly scan then you need the frames that the motor moves through. You can get that from the lower and upper bounds of each point. If the -scan is small enough to fit in memory on the machine you can use the `Fly(spec).frames()` +scan is small enough to fit in memory on the machine you can use the `Spec.frames` method to produce a single `Dimension` object containing the entire scan: >>> from scanspec.specs import Fly diff --git a/docs/how-to/serialize-a-spec.rst b/docs/how-to/serialize-a-spec.rst index b0265779..f3bb999a 100644 --- a/docs/how-to/serialize-a-spec.rst +++ b/docs/how-to/serialize-a-spec.rst @@ -11,7 +11,7 @@ Lets start with an example `Spec`. This Spec has a `repr` that shows its parameters it was instantiated with: >>> spec -Product(outer=Line(axis='y', start=4.0, stop=5.0, num=6), inner=Line(axis='x', start=1.0, stop=2.0, num=3)) +Product(outer=Line(axis='y', start=4.0, stop=5.0, num=6), inner=Line(axis='x', start=1.0, stop=2.0, num=3), gap=True) How to Serialize @@ -20,7 +20,7 @@ How to Serialize We can recursively serialize it to a dictionary: >>> spec.serialize() -{'outer': {'axis': 'y', 'start': 4.0, 'stop': 5.0, 'num': 6, 'type': 'Line'}, 'inner': {'axis': 'x', 'start': 1.0, 'stop': 2.0, 'num': 3, 'type': 'Line'}, 'type': 'Product'} +{'outer': {'axis': 'y', 'start': 4.0, 'stop': 5.0, 'num': 6, 'type': 'Line'}, 'inner': {'axis': 'x', 'start': 1.0, 'stop': 2.0, 'num': 3, 'type': 'Line'}, 'gap': True, 'type': 'Product'} How to Deserialize ------------------ @@ -28,4 +28,4 @@ How to Deserialize We can turn this back into a spec using `Spec.deserialize`: >>> Spec.deserialize({'outer': {'axis': 'y', 'start': 4.0, 'stop': 5.0, 'num': 6, 'type': 'Line'}, 'inner': {'axis': 'x', 'start': 1.0, 'stop': 2.0, 'num': 3, 'type': 'Line'}, 'type': 'Product'}) -Product(outer=Line(axis='y', start=4.0, stop=5.0, num=6), inner=Line(axis='x', start=1.0, stop=2.0, num=3)) +Product(outer=Line(axis='y', start=4.0, stop=5.0, num=6), inner=Line(axis='x', start=1.0, stop=2.0, num=3), gap=True) diff --git a/pyproject.toml b/pyproject.toml index 194d0449..884b067b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,7 +105,7 @@ commands = pre-commit: pre-commit run --all-files --show-diff-on-failure {posargs} type-checking: pyright src tests {posargs} tests: pytest --cov=scanspec --cov-report term --cov-report xml:cov.xml {posargs} - docs: sphinx-{posargs:build -E --keep-going} -T docs build/html + docs: sphinx-{posargs:build -EW --keep-going} -T docs build/html """ [tool.ruff] diff --git a/schema.json b/schema.json index b9a529de..1c6c923e 100644 --- a/schema.json +++ b/schema.json @@ -31,6 +31,7 @@ "num": 4, "type": "Line" }, + "gap": true, "type": "Product" } ] @@ -90,6 +91,7 @@ "num": 4, "type": "Line" }, + "gap": true, "type": "Product" }, "max_frames": 1024, @@ -152,6 +154,7 @@ "num": 4, "type": "Line" }, + "gap": true, "type": "Product" }, "max_frames": 1024, @@ -213,6 +216,7 @@ "num": 4, "type": "Line" }, + "gap": true, "type": "Product" } ] @@ -271,6 +275,7 @@ "num": 4, "type": "Line" }, + "gap": true, "type": "Product" } ] @@ -505,7 +510,7 @@ "right" ], "title": "Concat", - "description": "Concatenate two Specs together, running one after the other.\n\nEach Dimension of left and right must contain the same axes. Typically\nformed using `Spec.concat`.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"x\", 1, 3, 3).concat(Line(\"x\", 4, 5, 5))" + "description": "Concatenate two Specs together, running one after the other.\n\nEach Dimension of left and right must contain the same axes. Typically\nformed using `Spec.concat`.\n\n.. example_spec::\n\n from scanspec.specs import Fly, Line\n\n spec = Fly(Line(\"x\", 1, 3, 3).concat(Line(\"x\", 4, 5, 5)))" }, "Concat_str_-Output": { "properties": { @@ -543,7 +548,7 @@ "right" ], "title": "Concat", - "description": "Concatenate two Specs together, running one after the other.\n\nEach Dimension of left and right must contain the same axes. Typically\nformed using `Spec.concat`.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"x\", 1, 3, 3).concat(Line(\"x\", 4, 5, 5))" + "description": "Concatenate two Specs together, running one after the other.\n\nEach Dimension of left and right must contain the same axes. Typically\nformed using `Spec.concat`.\n\n.. example_spec::\n\n from scanspec.specs import Fly, Line\n\n spec = Fly(Line(\"x\", 1, 3, 3).concat(Line(\"x\", 4, 5, 5)))" }, "ConstantDuration_str_-Input": { "properties": { @@ -576,7 +581,7 @@ "constant_duration" ], "title": "ConstantDuration", - "description": "A special spec used to hold information about the duration of each frame." + "description": "Apply a constant duration to every point in a Spec.\n\nTypically applied with the ``@`` modifier.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = 0.1 @ Line(\"x\", 1, 2, 3)" }, "ConstantDuration_str_-Output": { "properties": { @@ -609,7 +614,7 @@ "constant_duration" ], "title": "ConstantDuration", - "description": "A special spec used to hold information about the duration of each frame." + "description": "Apply a constant duration to every point in a Spec.\n\nTypically applied with the ``@`` modifier.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = 0.1 @ Line(\"x\", 1, 2, 3)" }, "DifferenceOf_str_-Input": { "properties": { @@ -742,7 +747,7 @@ "spec" ], "title": "Fly", - "description": "Spec that represents a fly scan." + "description": "Move through lower to upper bounds of the Spec rather than stopping.\n\nThis is commonly termed a \"fly scan\" rather than a \"step scan\"\n\n.. example_spec::\n\n from scanspec.specs import Fly, Line\n\n spec = Fly(Line(\"x\", 1, 2, 3))" }, "Fly_str_-Output": { "properties": { @@ -763,7 +768,7 @@ "spec" ], "title": "Fly", - "description": "Spec that represents a fly scan." + "description": "Move through lower to upper bounds of the Spec rather than stopping.\n\nThis is commonly termed a \"fly scan\" rather than a \"step scan\"\n\n.. example_spec::\n\n from scanspec.specs import Fly, Line\n\n spec = Fly(Line(\"x\", 1, 2, 3))" }, "GapResponse": { "properties": { @@ -887,7 +892,7 @@ "num" ], "title": "Line", - "description": "Linearly spaced frames with start and stop as first and last midpoints.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"x\", 1, 2, 5)" + "description": "Linearly spaced frames with start and stop as first and last midpoints.\n\n.. example_spec::\n\n from scanspec.specs import Fly, Line\n\n spec = Fly(Line(\"x\", 1, 2, 5))" }, "Mask_str_-Input": { "properties": { @@ -919,7 +924,7 @@ "region" ], "title": "Mask", - "description": "Restrict Spec to only midpoints that fall inside the given Region.\n\nTypically created with the ``&`` operator. It also pushes down the\n``& | ^ -`` operators to its `Region` to avoid the need for brackets on\ncombinations of Regions.\n\nIf a Region spans multiple Dimension objects, they will be squashed together.\n\n.. example_spec::\n\n from scanspec.regions import Circle\n from scanspec.specs import Line\n\n spec = Line(\"y\", 1, 3, 3) * Line(\"x\", 3, 5, 5) & Circle(\"x\", \"y\", 4, 2, 1.2)\n\nSee Also: `why-squash-can-change-path`" + "description": "Restrict Spec to only midpoints that fall inside the given Region.\n\nTypically created with the ``&`` operator. It also pushes down the\n``& | ^ -`` operators to its `Region` to avoid the need for brackets on\ncombinations of Regions.\n\nIf a Region spans multiple Dimension objects, they will be squashed together.\n\n.. example_spec::\n\n from scanspec.regions import Circle\n from scanspec.specs import Fly, Line\n\n region = Circle(\"x\", \"y\", 4, 2, 1.2)\n spec = Fly(Line(\"y\", 1, 3, 3) * Line(\"x\", 3, 5, 5) & region)\n\nSee Also: `why-squash-can-change-path`" }, "Mask_str_-Output": { "properties": { @@ -951,7 +956,7 @@ "region" ], "title": "Mask", - "description": "Restrict Spec to only midpoints that fall inside the given Region.\n\nTypically created with the ``&`` operator. It also pushes down the\n``& | ^ -`` operators to its `Region` to avoid the need for brackets on\ncombinations of Regions.\n\nIf a Region spans multiple Dimension objects, they will be squashed together.\n\n.. example_spec::\n\n from scanspec.regions import Circle\n from scanspec.specs import Line\n\n spec = Line(\"y\", 1, 3, 3) * Line(\"x\", 3, 5, 5) & Circle(\"x\", \"y\", 4, 2, 1.2)\n\nSee Also: `why-squash-can-change-path`" + "description": "Restrict Spec to only midpoints that fall inside the given Region.\n\nTypically created with the ``&`` operator. It also pushes down the\n``& | ^ -`` operators to its `Region` to avoid the need for brackets on\ncombinations of Regions.\n\nIf a Region spans multiple Dimension objects, they will be squashed together.\n\n.. example_spec::\n\n from scanspec.regions import Circle\n from scanspec.specs import Fly, Line\n\n region = Circle(\"x\", \"y\", 4, 2, 1.2)\n spec = Fly(Line(\"y\", 1, 3, 3) * Line(\"x\", 3, 5, 5) & region)\n\nSee Also: `why-squash-can-change-path`" }, "MidpointsResponse": { "properties": { @@ -1091,13 +1096,35 @@ "Product_str_-Input": { "properties": { "outer": { - "$ref": "#/components/schemas/Spec-Input", + "anyOf": [ + { + "$ref": "#/components/schemas/Spec-Input" + }, + { + "type": "integer" + } + ], + "title": "Outer", "description": "Will be executed once" }, "inner": { - "$ref": "#/components/schemas/Spec-Input", + "anyOf": [ + { + "$ref": "#/components/schemas/Spec-Input" + }, + { + "type": "integer" + } + ], + "title": "Inner", "description": "Will be executed len(outer) times" }, + "gap": { + "type": "boolean", + "title": "Gap", + "description": "If False and the outer spec is an integer and the inner spec is snaked then the end and start of consecutive iterations of inner will have no gap", + "default": true + }, "type": { "type": "string", "const": "Product", @@ -1112,18 +1139,40 @@ "inner" ], "title": "Product", - "description": "Outer product of two Specs, nesting inner within outer.\n\nThis means that inner will run in its entirety at each point in outer.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"y\", 1, 2, 3) * Line(\"x\", 3, 4, 12)" + "description": "Outer product of two Specs, nesting inner within outer.\n\nThis means that inner will run in its entirety at each point in outer.\n\n.. example_spec::\n\n from scanspec.specs import Fly, Line\n\n spec = Fly(Line(\"y\", 1, 2, 3) * Line(\"x\", 3, 4, 12))\n\nAn inner integer can be used to repeat the same point many times.\n\n.. example_spec::\n\n from scanspec.specs import Fly, Line\n\n spec = Fly(Line(\"y\", 1, 2, 3) * 2)\n\nAn outer integer can be used to repeat the same scan many times.\n\n.. example_spec::\n\n from scanspec.specs import Fly, Line\n\n spec = Fly(2 * ~Line.bounded(\"x\", 3, 4, 1))\n\nIf you want snaked axes to have no gap between iterations you can do:\n\n.. example_spec::\n\n from scanspec.specs import Fly, Line, Product\n\n spec = Fly(Product(2, ~Line.bounded(\"x\", 3, 4, 1), gap=False))\n\n.. note:: There is no turnaround arrow at x=4" }, "Product_str_-Output": { "properties": { "outer": { - "$ref": "#/components/schemas/Spec-Output", + "anyOf": [ + { + "$ref": "#/components/schemas/Spec-Output" + }, + { + "type": "integer" + } + ], + "title": "Outer", "description": "Will be executed once" }, "inner": { - "$ref": "#/components/schemas/Spec-Output", + "anyOf": [ + { + "$ref": "#/components/schemas/Spec-Output" + }, + { + "type": "integer" + } + ], + "title": "Inner", "description": "Will be executed len(outer) times" }, + "gap": { + "type": "boolean", + "title": "Gap", + "description": "If False and the outer spec is an integer and the inner spec is snaked then the end and start of consecutive iterations of inner will have no gap", + "default": true + }, "type": { "type": "string", "const": "Product", @@ -1138,7 +1187,7 @@ "inner" ], "title": "Product", - "description": "Outer product of two Specs, nesting inner within outer.\n\nThis means that inner will run in its entirety at each point in outer.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"y\", 1, 2, 3) * Line(\"x\", 3, 4, 12)" + "description": "Outer product of two Specs, nesting inner within outer.\n\nThis means that inner will run in its entirety at each point in outer.\n\n.. example_spec::\n\n from scanspec.specs import Fly, Line\n\n spec = Fly(Line(\"y\", 1, 2, 3) * Line(\"x\", 3, 4, 12))\n\nAn inner integer can be used to repeat the same point many times.\n\n.. example_spec::\n\n from scanspec.specs import Fly, Line\n\n spec = Fly(Line(\"y\", 1, 2, 3) * 2)\n\nAn outer integer can be used to repeat the same scan many times.\n\n.. example_spec::\n\n from scanspec.specs import Fly, Line\n\n spec = Fly(2 * ~Line.bounded(\"x\", 3, 4, 1))\n\nIf you want snaked axes to have no gap between iterations you can do:\n\n.. example_spec::\n\n from scanspec.specs import Fly, Line, Product\n\n spec = Fly(Product(2, ~Line.bounded(\"x\", 3, 4, 1), gap=False))\n\n.. note:: There is no turnaround arrow at x=4" }, "Range_str_": { "properties": { @@ -1330,35 +1379,6 @@ } } }, - "Repeat_str_": { - "properties": { - "num": { - "type": "integer", - "minimum": 1.0, - "title": "Num", - "description": "Number of frames to produce" - }, - "gap": { - "type": "boolean", - "title": "Gap", - "description": "If False and the slowest of the stack of Dimension is snaked then the end and start of consecutive iterations of Spec will have no gap", - "default": true - }, - "type": { - "type": "string", - "const": "Repeat", - "title": "Type", - "default": "Repeat" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "num" - ], - "title": "Repeat", - "description": "Repeat an empty frame num times.\n\nCan be used on the outside of a scan to repeat the same scan many times.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = 2 * ~Line.bounded(\"x\", 3, 4, 1)\n\nIf you want snaked axes to have no gap between iterations you can do:\n\n.. example_spec::\n\n from scanspec.specs import Line, Repeat\n\n spec = Repeat(2, gap=False) * ~Line.bounded(\"x\", 3, 4, 1)\n\n.. note:: There is no turnaround arrow at x=4" - }, "SmallestStepResponse": { "properties": { "absolute": { @@ -1402,7 +1422,7 @@ "spec" ], "title": "Snake", - "description": "Run the Spec in reverse on every other iteration when nested.\n\nTypically created with the ``~`` operator.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"y\", 1, 3, 3) * ~Line(\"x\", 3, 5, 5)" + "description": "Run the Spec in reverse on every other iteration when nested.\n\nTypically created with the ``~`` operator.\n\n.. example_spec::\n\n from scanspec.specs import Fly, Line\n\n spec = Fly(Line(\"y\", 1, 3, 3) * ~Line(\"x\", 3, 5, 5))" }, "Snake_str_-Output": { "properties": { @@ -1423,16 +1443,13 @@ "spec" ], "title": "Snake", - "description": "Run the Spec in reverse on every other iteration when nested.\n\nTypically created with the ``~`` operator.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"y\", 1, 3, 3) * ~Line(\"x\", 3, 5, 5)" + "description": "Run the Spec in reverse on every other iteration when nested.\n\nTypically created with the ``~`` operator.\n\n.. example_spec::\n\n from scanspec.specs import Fly, Line\n\n spec = Fly(Line(\"y\", 1, 3, 3) * ~Line(\"x\", 3, 5, 5))" }, "Spec-Input": { "oneOf": [ { "$ref": "#/components/schemas/Product_str_-Input" }, - { - "$ref": "#/components/schemas/Repeat_str_" - }, { "$ref": "#/components/schemas/Zip_str_-Input" }, @@ -1473,7 +1490,6 @@ "Line": "#/components/schemas/Line_str_", "Mask": "#/components/schemas/Mask_str_-Input", "Product": "#/components/schemas/Product_str_-Input", - "Repeat": "#/components/schemas/Repeat_str_", "Snake": "#/components/schemas/Snake_str_-Input", "Spiral": "#/components/schemas/Spiral_str_", "Squash": "#/components/schemas/Squash_str_-Input", @@ -1487,9 +1503,6 @@ { "$ref": "#/components/schemas/Product_str_-Output" }, - { - "$ref": "#/components/schemas/Repeat_str_" - }, { "$ref": "#/components/schemas/Zip_str_-Output" }, @@ -1530,7 +1543,6 @@ "Line": "#/components/schemas/Line_str_", "Mask": "#/components/schemas/Mask_str_-Output", "Product": "#/components/schemas/Product_str_-Output", - "Repeat": "#/components/schemas/Repeat_str_", "Snake": "#/components/schemas/Snake_str_-Output", "Spiral": "#/components/schemas/Spiral_str_", "Squash": "#/components/schemas/Squash_str_-Output", @@ -1602,7 +1614,7 @@ "num" ], "title": "Spiral", - "description": "Archimedean spiral of \"x_axis\" and \"y_axis\".\n\nStarts at centre point (\"x_start\", \"y_start\") with angle \"rotate\". Produces\n\"num\" points in a spiral spanning width of \"x_range\" and height of \"y_range\"\n\n.. example_spec::\n\n from scanspec.specs import Spiral\n\n spec = Spiral(\"x\", \"y\", 1, 5, 10, 50, 30)" + "description": "Archimedean spiral of \"x_axis\" and \"y_axis\".\n\nStarts at centre point (\"x_start\", \"y_start\") with angle \"rotate\". Produces\n\"num\" points in a spiral spanning width of \"x_range\" and height of \"y_range\"\n\n.. example_spec::\n\n from scanspec.specs import Fly, Spiral\n\n spec = Fly(Spiral(\"x\", \"y\", 1, 5, 10, 50, 30))" }, "Squash_str_-Input": { "properties": { @@ -1629,7 +1641,7 @@ "spec" ], "title": "Squash", - "description": "Squash a stack of Dimension together into a single expanded Dimension object.\n\nSee Also:\n `why-squash-can-change-path`\n\n.. example_spec::\n\n from scanspec.specs import Line, Squash\n\n spec = Squash(Line(\"y\", 1, 2, 3) * Line(\"x\", 0, 1, 4))" + "description": "Squash a stack of Dimension together into a single expanded Dimension object.\n\nSee Also:\n `why-squash-can-change-path`\n\n.. example_spec::\n\n from scanspec.specs import Fly, Line, Squash\n\n spec = Fly(Squash(Line(\"y\", 1, 2, 3) * Line(\"x\", 0, 1, 4)))" }, "Squash_str_-Output": { "properties": { @@ -1656,7 +1668,7 @@ "spec" ], "title": "Squash", - "description": "Squash a stack of Dimension together into a single expanded Dimension object.\n\nSee Also:\n `why-squash-can-change-path`\n\n.. example_spec::\n\n from scanspec.specs import Line, Squash\n\n spec = Squash(Line(\"y\", 1, 2, 3) * Line(\"x\", 0, 1, 4))" + "description": "Squash a stack of Dimension together into a single expanded Dimension object.\n\nSee Also:\n `why-squash-can-change-path`\n\n.. example_spec::\n\n from scanspec.specs import Fly, Line, Squash\n\n spec = Fly(Squash(Line(\"y\", 1, 2, 3) * Line(\"x\", 0, 1, 4)))" }, "Static_str_": { "properties": { @@ -1691,7 +1703,7 @@ "value" ], "title": "Static", - "description": "A static frame, repeated num times, with axis at value.\n\nCan be used to set axis=value at every point in a scan.\n\n.. example_spec::\n\n from scanspec.specs import Line, Static\n\n spec = Line(\"y\", 1, 2, 3).zip(Static(\"x\", 3))" + "description": "A static frame, repeated num times, with axis at value.\n\nCan be used to set axis=value at every point in a scan.\n\n.. example_spec::\n\n from scanspec.specs import Fly, Line, Static\n\n spec = Fly(Line(\"y\", 1, 2, 3).zip(Static(\"x\", 3)))" }, "SymmetricDifferenceOf_str_-Input": { "properties": { @@ -1873,7 +1885,7 @@ "right" ], "title": "Zip", - "description": "Run two Specs in parallel, merging their midpoints together.\n\nTypically formed using `Spec.zip`.\n\nStacks of Dimension are merged by:\n\n- If right creates a stack of a single Dimension object of size 1, expand it to\n the size of the fastest Dimension object created by left\n- Merge individual Dimension objects together from fastest to slowest\n\nThis means that Zipping a Spec producing stack [l2, l1] with a Spec\nproducing stack [r1] will assert len(l1)==len(r1), and produce\nstack [l2, l1.zip(r1)].\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"z\", 1, 2, 3) * Line(\"y\", 3, 4, 5).zip(Line(\"x\", 4, 5, 5))" + "description": "Run two Specs in parallel, merging their midpoints together.\n\nTypically formed using `Spec.zip`.\n\nStacks of Dimension are merged by:\n\n- If right creates a stack of a single Dimension object of size 1, expand it to\n the size of the fastest Dimension object created by left\n- Merge individual Dimension objects together from fastest to slowest\n\nThis means that Zipping a Spec producing stack [l2, l1] with a Spec\nproducing stack [r1] will assert len(l1)==len(r1), and produce\nstack [l2, l1.zip(r1)].\n\n.. example_spec::\n\n from scanspec.specs import Fly, Line\n\n spec = Fly(Line(\"z\", 1, 2, 3) * Line(\"y\", 3, 4, 5).zip(Line(\"x\", 4, 5, 5)))" }, "Zip_str_-Output": { "properties": { @@ -1899,7 +1911,7 @@ "right" ], "title": "Zip", - "description": "Run two Specs in parallel, merging their midpoints together.\n\nTypically formed using `Spec.zip`.\n\nStacks of Dimension are merged by:\n\n- If right creates a stack of a single Dimension object of size 1, expand it to\n the size of the fastest Dimension object created by left\n- Merge individual Dimension objects together from fastest to slowest\n\nThis means that Zipping a Spec producing stack [l2, l1] with a Spec\nproducing stack [r1] will assert len(l1)==len(r1), and produce\nstack [l2, l1.zip(r1)].\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"z\", 1, 2, 3) * Line(\"y\", 3, 4, 5).zip(Line(\"x\", 4, 5, 5))" + "description": "Run two Specs in parallel, merging their midpoints together.\n\nTypically formed using `Spec.zip`.\n\nStacks of Dimension are merged by:\n\n- If right creates a stack of a single Dimension object of size 1, expand it to\n the size of the fastest Dimension object created by left\n- Merge individual Dimension objects together from fastest to slowest\n\nThis means that Zipping a Spec producing stack [l2, l1] with a Spec\nproducing stack [r1] will assert len(l1)==len(r1), and produce\nstack [l2, l1.zip(r1)].\n\n.. example_spec::\n\n from scanspec.specs import Fly, Line\n\n spec = Fly(Line(\"z\", 1, 2, 3) * Line(\"y\", 3, 4, 5).zip(Line(\"x\", 4, 5, 5)))" } } } diff --git a/src/scanspec/core.py b/src/scanspec/core.py index 68fb9be2..0fbeaa88 100644 --- a/src/scanspec/core.py +++ b/src/scanspec/core.py @@ -268,7 +268,9 @@ def _make_schema( return {member.__name__: handler(member) for member in members} -def if_instance_do(x: C, cls: type[C], func: Callable[[C], T]) -> T: +def if_instance_do( + x: C, cls: type[C] | tuple[type[C], Any], func: Callable[[C], T] +) -> T: """If x is of type cls then return func(x), otherwise return NotImplemented. Used as a helper when implementing operator overloading. @@ -311,7 +313,7 @@ def __len__(self) -> int: def axes(self) -> list[Axis]: """The axes which will move during the scan. - These will be present in `midpoints`, `lower` and `upper`. + These will be present in ``midpoints``, ``lower`` and ``upper``. """ return list(self.midpoints.keys()) diff --git a/src/scanspec/service.py b/src/scanspec/service.py index 8f6115dc..1b1218da 100644 --- a/src/scanspec/service.py +++ b/src/scanspec/service.py @@ -396,5 +396,6 @@ def scanspec_schema_text() -> str: openapi_version=app.openapi_version, description=app.description, routes=app.routes, - ) + ), + indent=4, ) diff --git a/src/scanspec/specs.py b/src/scanspec/specs.py index 0ca23549..a1bc7bd0 100644 --- a/src/scanspec/specs.py +++ b/src/scanspec/specs.py @@ -9,7 +9,7 @@ import warnings from collections.abc import Callable, Mapping -from typing import Any, Generic, Literal, SupportsFloat, overload +from typing import Any, Generic, Literal, SupportsFloat import numpy as np import numpy.typing as npt @@ -35,7 +35,6 @@ "ConstantDuration", "Spec", "Product", - "Repeat", "Zip", "Mask", "Snake", @@ -47,8 +46,11 @@ "Fly", "step", "fly", + "VARIABLE_DURATION", ] +#: A string returned from `Spec.duration` to signify it produces +#: a different duration for each point VARIABLE_DURATION = "VARIABLE_DURATION" @@ -58,8 +60,8 @@ class Spec(Generic[Axis]): Abstract baseclass for the specification of a scan. Supports operators: - - ``*``: Outer `Product` of two Specs, nesting the second within the first. - If the first operand is an integer, wrap it in a `Repeat` + - ``*``: Outer `Product` of two Specs or ints, nesting the second within the first. + - ``@``: `ConstantDuration` of the Spec, setting a constant duration for each point. - ``&``: `Mask` the Spec with a `Region`, excluding midpoints outside of it - ``~``: `Snake` the Spec, reversing every other iteration of it """ @@ -107,26 +109,16 @@ def shape(self) -> tuple[int, ...]: """Return the final, simplified shape of the scan.""" return tuple(len(dim) for dim in self.calculate()) - def __rmul__(self, other: int) -> Product[Axis]: - return if_instance_do(other, int, lambda o: Product(Repeat(o), self)) - def __rmatmul__(self, other: SupportsFloat) -> ConstantDuration[Axis]: return if_instance_do( - other, - SupportsFloat, - lambda o: ConstantDuration(constant_duration=float(o), spec=self), + other, SupportsFloat, lambda o: ConstantDuration(float(o), self) ) - @overload - def __mul__(self, other: Spec[Axis]) -> Product[Axis]: ... - - @overload - def __mul__(self, other: Spec[OtherAxis]) -> Product[Axis | OtherAxis]: ... + def __rmul__(self, other: Spec[Axis] | int) -> Product[Axis]: + return if_instance_do(other, (Spec, int), lambda o: Product(o, self)) - def __mul__( - self, other: Spec[Axis] | Spec[OtherAxis] - ) -> Product[Axis] | Product[Axis | OtherAxis]: - return if_instance_do(other, Spec, lambda o: Product(self, o)) + def __mul__(self, other: Spec[Axis] | int) -> Product[Axis]: + return if_instance_do(other, (Spec, int), lambda o: Product(self, o)) def __and__(self, other: Region[Axis]) -> Mask[Axis]: return if_instance_do(other, Region, lambda o: Mask(self, o)) @@ -160,68 +152,73 @@ class Product(Spec[Axis]): .. example_spec:: - from scanspec.specs import Line - - spec = Line("y", 1, 2, 3) * Line("x", 3, 4, 12) - """ + from scanspec.specs import Fly, Line - outer: Spec[Axis] = Field(description="Will be executed once") - inner: Spec[Axis] = Field(description="Will be executed len(outer) times") + spec = Fly(Line("y", 1, 2, 3) * Line("x", 3, 4, 12)) - def axes(self) -> list[Axis]: # noqa: D102 - return self.outer.axes() + self.inner.axes() + An inner integer can be used to repeat the same point many times. - def duration(self) -> float | None | Literal["VARIABLE_DURATION"]: # noqa: D102 - outer, inner = self.outer.duration(), self.inner.duration() - if outer is not None: - raise ValueError("Outer axes defined a duration") - return inner - - def calculate( # noqa: D102 - self, bounds: bool = False, nested: bool = False - ) -> list[Dimension[Axis]]: - frames_outer = self.outer.calculate(bounds=False, nested=nested) - frames_inner = self.inner.calculate(bounds, nested=True) - return frames_outer + frames_inner + .. example_spec:: + from scanspec.specs import Fly, Line -@dataclass(config=StrictConfig) -class Repeat(Spec[Axis]): - """Repeat an empty frame num times. + spec = Fly(Line("y", 1, 2, 3) * 2) - Can be used on the outside of a scan to repeat the same scan many times. + An outer integer can be used to repeat the same scan many times. .. example_spec:: - from scanspec.specs import Line + from scanspec.specs import Fly, Line - spec = 2 * ~Line.bounded("x", 3, 4, 1) + spec = Fly(2 * ~Line.bounded("x", 3, 4, 1)) If you want snaked axes to have no gap between iterations you can do: .. example_spec:: - from scanspec.specs import Line, Repeat + from scanspec.specs import Fly, Line, Product - spec = Repeat(2, gap=False) * ~Line.bounded("x", 3, 4, 1) + spec = Fly(Product(2, ~Line.bounded("x", 3, 4, 1), gap=False)) .. note:: There is no turnaround arrow at x=4 """ - num: int = Field(ge=1, description="Number of frames to produce") + outer: Spec[Axis] | int = Field(description="Will be executed once") + inner: Spec[Axis] | int = Field(description="Will be executed len(outer) times") gap: bool = Field( - description="If False and the slowest of the stack of Dimension is snaked " - "then the end and start of consecutive iterations of Spec will have no gap", + description="If False and the outer spec is an integer and the inner spec is " + "snaked then the end and start of consecutive iterations of inner will have no " + "gap", default=True, ) def axes(self) -> list[Axis]: # noqa: D102 - return [] + outer_axes = [] if isinstance(self.outer, int) else self.outer.axes() + inner_axes = [] if isinstance(self.inner, int) else self.inner.axes() + return outer_axes + inner_axes + + def duration(self) -> float | None | Literal["VARIABLE_DURATION"]: # noqa: D102 + outer_duration = None if isinstance(self.outer, int) else self.outer.duration() + inner_duration = None if isinstance(self.inner, int) else self.inner.duration() + if outer_duration is not None: + if inner_duration is not None: + raise ValueError("Both inner and outer specs defined a duration") + return outer_duration + else: + return inner_duration def calculate( # noqa: D102 self, bounds: bool = False, nested: bool = False ) -> list[Dimension[Axis]]: - return [Dimension({}, gap=np.full(self.num, self.gap))] + if isinstance(self.outer, int): + dims_outer = [Dimension[Axis]({}, gap=np.full(self.outer, self.gap))] + else: + dims_outer = self.outer.calculate(bounds=False, nested=nested) + if isinstance(self.inner, int): + dims_inner = [Dimension[Axis]({}, gap=np.full(self.inner, False))] + else: + dims_inner = self.inner.calculate(bounds, nested=True) + return dims_outer + dims_inner @dataclass(config=StrictConfig) @@ -242,9 +239,9 @@ class Zip(Spec[Axis]): .. example_spec:: - from scanspec.specs import Line + from scanspec.specs import Fly, Line - spec = Line("z", 1, 2, 3) * Line("y", 3, 4, 5).zip(Line("x", 4, 5, 5)) + spec = Fly(Line("z", 1, 2, 3) * Line("y", 3, 4, 5).zip(Line("x", 4, 5, 5))) """ left: Spec[Axis] = Field( @@ -317,9 +314,10 @@ class Mask(Spec[Axis]): .. example_spec:: from scanspec.regions import Circle - from scanspec.specs import Line + from scanspec.specs import Fly, Line - spec = Line("y", 1, 3, 3) * Line("x", 3, 5, 5) & Circle("x", "y", 4, 2, 1.2) + region = Circle("x", "y", 4, 2, 1.2) + spec = Fly(Line("y", 1, 3, 3) * Line("x", 3, 5, 5) & region) See Also: `why-squash-can-change-path` """ @@ -385,9 +383,9 @@ class Snake(Spec[Axis]): .. example_spec:: - from scanspec.specs import Line + from scanspec.specs import Fly, Line - spec = Line("y", 1, 3, 3) * ~Line("x", 3, 5, 5) + spec = Fly(Line("y", 1, 3, 3) * ~Line("x", 3, 5, 5)) """ spec: Spec[Axis] = Field( @@ -418,9 +416,9 @@ class Concat(Spec[Axis]): .. example_spec:: - from scanspec.specs import Line + from scanspec.specs import Fly, Line - spec = Line("x", 1, 3, 3).concat(Line("x", 4, 5, 5)) + spec = Fly(Line("x", 1, 3, 3).concat(Line("x", 4, 5, 5))) """ left: Spec[Axis] = Field( @@ -479,9 +477,9 @@ class Squash(Spec[Axis]): .. example_spec:: - from scanspec.specs import Line, Squash + from scanspec.specs import Fly, Line, Squash - spec = Squash(Line("y", 1, 2, 3) * Line("x", 0, 1, 4)) + spec = Fly(Squash(Line("y", 1, 2, 3) * Line("x", 0, 1, 4))) """ @@ -538,9 +536,9 @@ class Line(Spec[Axis]): .. example_spec:: - from scanspec.specs import Line + from scanspec.specs import Fly, Line - spec = Line("x", 1, 2, 5) + spec = Fly(Line("x", 1, 2, 5)) """ axis: Axis = Field(description="An identifier for what to move") @@ -584,9 +582,9 @@ def bounded( .. example_spec:: - from scanspec.specs import Line + from scanspec.specs import Fly, Line - spec = Line.bounded("x", 1, 2, 5) + spec = Fly(Line.bounded("x", 1, 2, 5)) """ half_step = (upper - lower) / num / 2 start = lower + half_step @@ -607,7 +605,16 @@ def bounded( @dataclass(config=StrictConfig) class Fly(Spec[Axis]): - """Spec that represents a fly scan.""" + """Move through lower to upper bounds of the Spec rather than stopping. + + This is commonly termed a "fly scan" rather than a "step scan" + + .. example_spec:: + + from scanspec.specs import Fly, Line + + spec = Fly(Line("x", 1, 2, 3)) + """ spec: Spec[Axis] = Field(description="Spec contaning the path to be followed") @@ -625,7 +632,16 @@ def calculate( # noqa: D102 @dataclass(config=StrictConfig) class ConstantDuration(Spec[Axis]): - """A special spec used to hold information about the duration of each frame.""" + """Apply a constant duration to every point in a Spec. + + Typically applied with the ``@`` modifier. + + .. example_spec:: + + from scanspec.specs import Line + + spec = 0.1 @ Line("x", 1, 2, 3) + """ constant_duration: float = Field(description="The value at each point") spec: Spec[Axis] | None = Field( @@ -673,9 +689,9 @@ class Static(Spec[Axis]): .. example_spec:: - from scanspec.specs import Line, Static + from scanspec.specs import Fly, Line, Static - spec = Line("y", 1, 2, 3).zip(Static("x", 3)) + spec = Fly(Line("y", 1, 2, 3).zip(Static("x", 3))) """ axis: Axis = Field(description="An identifier for what to move") @@ -707,9 +723,9 @@ class Spiral(Spec[Axis]): .. example_spec:: - from scanspec.specs import Spiral + from scanspec.specs import Fly, Spiral - spec = Spiral("x", "y", 1, 5, 10, 50, 30) + spec = Fly(Spiral("x", "y", 1, 5, 10, 50, 30)) """ # TODO: Make use of typing.Annotated upon fix of @@ -772,9 +788,9 @@ def spaced( .. example_spec:: - from scanspec.specs import Spiral + from scanspec.specs import Fly, Spiral - spec = Spiral.spaced("x", "y", 0, 0, 10, 3) + spec = Fly(Spiral.spaced("x", "y", 0, 0, 10, 3)) """ # phi = sqrt(4 * pi * num) # and: n_rings = phi / (2 * pi) @@ -804,11 +820,9 @@ def fly(spec: Spec[Axis], duration: float) -> Spec[Axis | str]: spec: The source `Spec` to continuously move duration: How long to spend at each frame in the spec - .. example_spec:: - - from scanspec.specs import Line, fly + .. deprecated:: 1.0.0 - spec = fly(Line("x", 1, 2, 3), 0.1) + You should use `Fly` and `ConstantDuration` instead """ warnings.warn( @@ -829,11 +843,9 @@ def step(spec: Spec[Axis], duration: float, num: int = 1) -> Spec[Axis]: num: Number of frames to produce with given duration at each of frame in the spec - .. example_spec:: - - from scanspec.specs import Line, step + .. deprecated:: 1.0.0 - spec = step(Line("x", 1, 2, 3), 0.1) + You should use `ConstantDuration` instead. """ warnings.warn( diff --git a/tests/test_serialization.py b/tests/test_serialization.py index f2f73b70..effcb2d4 100644 --- a/tests/test_serialization.py +++ b/tests/test_serialization.py @@ -52,8 +52,10 @@ def test_product_lines_serializes() -> None: ob = Line("z", 4, 5, 6) * Line("y", 2, 3, 5) * Line("x", 0, 1, 4) serialized = { "type": "Product", + "gap": True, "outer": { "type": "Product", + "gap": True, "outer": { "type": "Line", "axis": "z", diff --git a/tests/test_specs.py b/tests/test_specs.py index 1600fe4b..a0eb0b7f 100644 --- a/tests/test_specs.py +++ b/tests/test_specs.py @@ -11,7 +11,7 @@ Fly, Line, Mask, - Repeat, + Product, Spec, Spiral, Squash, @@ -240,9 +240,7 @@ def test_squashed_product() -> None: def test_squashed_multiplied_snake_scan() -> None: inst = Line(z, 1, 2, 2) * Squash( - Line(y, 1, 2, 2) - * ~Line.bounded(x, 3, 7, 2) - * (9.0 @ Repeat[str](2, gap=False)) # until #177 + 9.0 @ Line(y, 1, 2, 2) * ~Line.bounded(x, 3, 7, 2) * 2 ) assert inst.axes() == [z, y, x] (dimz, dimxyt) = inst.calculate() @@ -280,7 +278,7 @@ def test_product_snaking_lines() -> None: def test_product_duration() -> None: with pytest.raises(ValueError) as msg: _ = Fly(1.0 @ Line(y, 1, 2, 3)) * Fly(1.0 @ ~Line(x, 0, 1, 2)) - assert "Outer axes defined a duration" in str(msg.value) + assert "Both inner and outer specs defined a duration" in str(msg.value) def test_concat_lines() -> None: @@ -532,7 +530,7 @@ def test_beam_selector() -> None: def test_gap_repeat() -> None: # Check that no gap propogates to dim.gap for snaked axis - spec = Repeat[str](10, gap=False) * ~Line.bounded(x, 11, 19, 1) + spec = Product(10, ~Line.bounded(x, 11, 19, 1), gap=False) dim = spec.frames(bounds=True) assert len(dim) == 10 assert dim.lower == {x: approx([11, 19, 11, 19, 11, 19, 11, 19, 11, 19])} @@ -543,7 +541,7 @@ def test_gap_repeat() -> None: def test_gap_repeat_non_snake() -> None: # Check that no gap doesn't propogate to dim.gap for non-snaked axis - spec = Repeat[str](3, gap=False) * Line.bounded(x, 11, 19, 1) + spec = Product(3, Line.bounded(x, 11, 19, 1), gap=False) dim = spec.frames(bounds=True) assert len(dim) == 3 assert dim.lower == {x: approx([11, 11, 11])} @@ -552,6 +550,17 @@ def test_gap_repeat_non_snake() -> None: assert dim.gap == ints("111") +@pytest.mark.parametrize("gap", [True, False]) +def test_gap_repeat_right_hand_side(gap: bool) -> None: + # Check that 2 repeats of each frame means a gap on each change in x, no + # matter what the setting of the gap argument + spec = Product(Line(x, 11, 19, 2), 2, gap=gap) + dim = spec.frames(bounds=True) + assert len(dim) == 4 + assert dim.lower == dim.midpoints == dim.upper == {x: approx([11, 11, 19, 19])} + assert dim.gap == ints("1010") + + def test_multiple_statics(): part_1 = Static("y", 2) * Static("z", 3) * Line("x", 0, 10, 2) part_2 = Static("y", 4) * Static("z", 5) * Line("x", 0, 10, 2)