API#
++ | Top level API. |
+
diff --git a/0.7.5/.buildinfo b/0.7.5/.buildinfo new file mode 100644 index 00000000..29a7dbfc --- /dev/null +++ b/0.7.5/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file records the configuration used when building these files. When it is not found, a full rebuild will be done. +config: 6a98a1844afd97517a90e0191ea0c5a4 +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/0.7.5/.doctrees/_api.doctree b/0.7.5/.doctrees/_api.doctree new file mode 100644 index 00000000..c0283d08 Binary files /dev/null and b/0.7.5/.doctrees/_api.doctree differ diff --git a/0.7.5/.doctrees/_api/scanspec.core.doctree b/0.7.5/.doctrees/_api/scanspec.core.doctree new file mode 100644 index 00000000..016a62ed Binary files /dev/null and b/0.7.5/.doctrees/_api/scanspec.core.doctree differ diff --git a/0.7.5/.doctrees/_api/scanspec.doctree b/0.7.5/.doctrees/_api/scanspec.doctree new file mode 100644 index 00000000..79a89a76 Binary files /dev/null and b/0.7.5/.doctrees/_api/scanspec.doctree differ diff --git a/0.7.5/.doctrees/_api/scanspec.plot.doctree b/0.7.5/.doctrees/_api/scanspec.plot.doctree new file mode 100644 index 00000000..f630d7a6 Binary files /dev/null and b/0.7.5/.doctrees/_api/scanspec.plot.doctree differ diff --git a/0.7.5/.doctrees/_api/scanspec.regions.doctree b/0.7.5/.doctrees/_api/scanspec.regions.doctree new file mode 100644 index 00000000..3b34577b Binary files /dev/null and b/0.7.5/.doctrees/_api/scanspec.regions.doctree differ diff --git a/0.7.5/.doctrees/_api/scanspec.specs.doctree b/0.7.5/.doctrees/_api/scanspec.specs.doctree new file mode 100644 index 00000000..0a1e3027 Binary files /dev/null and b/0.7.5/.doctrees/_api/scanspec.specs.doctree differ diff --git a/0.7.5/.doctrees/explanations.doctree b/0.7.5/.doctrees/explanations.doctree new file mode 100644 index 00000000..18293956 Binary files /dev/null and b/0.7.5/.doctrees/explanations.doctree differ diff --git a/0.7.5/.doctrees/explanations/decisions.doctree b/0.7.5/.doctrees/explanations/decisions.doctree new file mode 100644 index 00000000..f1656e72 Binary files /dev/null and b/0.7.5/.doctrees/explanations/decisions.doctree differ diff --git a/0.7.5/.doctrees/explanations/decisions/0001-record-architecture-decisions.doctree b/0.7.5/.doctrees/explanations/decisions/0001-record-architecture-decisions.doctree new file mode 100644 index 00000000..edd68666 Binary files /dev/null and b/0.7.5/.doctrees/explanations/decisions/0001-record-architecture-decisions.doctree differ diff --git a/0.7.5/.doctrees/explanations/decisions/0002-switched-to-python-copier-template.doctree b/0.7.5/.doctrees/explanations/decisions/0002-switched-to-python-copier-template.doctree new file mode 100644 index 00000000..45bc27f6 Binary files /dev/null and b/0.7.5/.doctrees/explanations/decisions/0002-switched-to-python-copier-template.doctree differ diff --git a/0.7.5/.doctrees/explanations/technical-terms.doctree b/0.7.5/.doctrees/explanations/technical-terms.doctree new file mode 100644 index 00000000..ee9ddaa2 Binary files /dev/null and b/0.7.5/.doctrees/explanations/technical-terms.doctree differ diff --git a/0.7.5/.doctrees/explanations/why-squash-can-change-path.doctree b/0.7.5/.doctrees/explanations/why-squash-can-change-path.doctree new file mode 100644 index 00000000..de62a98d Binary files /dev/null and b/0.7.5/.doctrees/explanations/why-squash-can-change-path.doctree differ diff --git a/0.7.5/.doctrees/explanations/why-stack-frames.doctree b/0.7.5/.doctrees/explanations/why-stack-frames.doctree new file mode 100644 index 00000000..9f2f1986 Binary files /dev/null and b/0.7.5/.doctrees/explanations/why-stack-frames.doctree differ diff --git a/0.7.5/.doctrees/genindex.doctree b/0.7.5/.doctrees/genindex.doctree new file mode 100644 index 00000000..148feca7 Binary files /dev/null and b/0.7.5/.doctrees/genindex.doctree differ diff --git a/0.7.5/.doctrees/how-to.doctree b/0.7.5/.doctrees/how-to.doctree new file mode 100644 index 00000000..da1cdc52 Binary files /dev/null and b/0.7.5/.doctrees/how-to.doctree differ diff --git a/0.7.5/.doctrees/how-to/contribute.doctree b/0.7.5/.doctrees/how-to/contribute.doctree new file mode 100644 index 00000000..92a08d4c Binary files /dev/null and b/0.7.5/.doctrees/how-to/contribute.doctree differ diff --git a/0.7.5/.doctrees/how-to/iterate-a-spec.doctree b/0.7.5/.doctrees/how-to/iterate-a-spec.doctree new file mode 100644 index 00000000..e43cf3f7 Binary files /dev/null and b/0.7.5/.doctrees/how-to/iterate-a-spec.doctree differ diff --git a/0.7.5/.doctrees/how-to/run-container.doctree b/0.7.5/.doctrees/how-to/run-container.doctree new file mode 100644 index 00000000..9b6974c6 Binary files /dev/null and b/0.7.5/.doctrees/how-to/run-container.doctree differ diff --git a/0.7.5/.doctrees/how-to/serialize-a-spec.doctree b/0.7.5/.doctrees/how-to/serialize-a-spec.doctree new file mode 100644 index 00000000..fed2ac42 Binary files /dev/null and b/0.7.5/.doctrees/how-to/serialize-a-spec.doctree differ diff --git a/0.7.5/.doctrees/index.doctree b/0.7.5/.doctrees/index.doctree new file mode 100644 index 00000000..561bf7f3 Binary files /dev/null and b/0.7.5/.doctrees/index.doctree differ diff --git a/0.7.5/.doctrees/reference.doctree b/0.7.5/.doctrees/reference.doctree new file mode 100644 index 00000000..7dfeadbd Binary files /dev/null and b/0.7.5/.doctrees/reference.doctree differ diff --git a/0.7.5/.doctrees/reference/rest_api.doctree b/0.7.5/.doctrees/reference/rest_api.doctree new file mode 100644 index 00000000..311c7932 Binary files /dev/null and b/0.7.5/.doctrees/reference/rest_api.doctree differ diff --git a/0.7.5/.doctrees/tutorials.doctree b/0.7.5/.doctrees/tutorials.doctree new file mode 100644 index 00000000..7288d4d6 Binary files /dev/null and b/0.7.5/.doctrees/tutorials.doctree differ diff --git a/0.7.5/.doctrees/tutorials/creating-a-spec.doctree b/0.7.5/.doctrees/tutorials/creating-a-spec.doctree new file mode 100644 index 00000000..2f6b6ae5 Binary files /dev/null and b/0.7.5/.doctrees/tutorials/creating-a-spec.doctree differ diff --git a/0.7.5/.doctrees/tutorials/installation.doctree b/0.7.5/.doctrees/tutorials/installation.doctree new file mode 100644 index 00000000..aa523e68 Binary files /dev/null and b/0.7.5/.doctrees/tutorials/installation.doctree differ diff --git a/0.7.5/.doctrees/tutorials/rest-service.doctree b/0.7.5/.doctrees/tutorials/rest-service.doctree new file mode 100644 index 00000000..c76ec141 Binary files /dev/null and b/0.7.5/.doctrees/tutorials/rest-service.doctree differ diff --git a/0.7.5/_api.html b/0.7.5/_api.html new file mode 100644 index 00000000..5ea61722 --- /dev/null +++ b/0.7.5/_api.html @@ -0,0 +1,566 @@ + + + + + + +
+ + + +scanspec.core
#Core classes like Frames
and Path
.
Members
++ | A type variable for an Axis that can be specified for a scan |
+
+ | Alternative axis variable to be used when two are required in the same type binding |
+
+ | If x is of type cls then return func(x), otherwise return NotImplemented. |
+
+ | Map of axes to float ndarray of points E.g. |
+
+ | Represents a series of scan frames along a number of axes. |
+
+ | Like a |
+
+ | Is there a gap between end of frames1 and start of frames2. |
+
+ | Squash a stack of nested Frames into a single one. |
+
+ | A consumable route through a stack of Frames, representing a scan path. |
+
+ | Convenience iterable that produces the scan midpoints for each axis. |
+
+ | Add all subclasses of super_cls to a discriminated union. |
+
+ | Used to ensure pydantic dataclasses error if given extra arguments |
+
A type variable for an Axis that can be specified for a scan
+alias of TypeVar(‘Axis’, covariant=True)
+Alternative axis variable to be used when two are required in the same type binding
+alias of TypeVar(‘OtherAxis’)
+If x is of type cls then return func(x), otherwise return NotImplemented.
+Used as a helper when implementing operator overloading.
+Map of axes to float ndarray of points +E.g. {xmotor: array([0, 1, 2]), ymotor: array([2, 2, 2])}
+ +Represents a series of scan frames along a number of axes.
+During a scan each axis will traverse lower-midpoint-upper for each frame.
+midpoints – The midpoints of scan frames for each axis
lower – Lower bounds of scan frames if different from midpoints
upper – Upper bounds of scan frames if different from midpoints
gap – If supplied, define if there is a gap between frame and previous +otherwise it is calculated by looking at lower and upper bounds
Typically used in two ways:
+A list of Frames objects returned from Spec.calculate
represents a scan
+as a linear stack of frames. Interpreted as nested from slowest moving to
+fastest moving, so each faster Frames object will iterate once per
+position of the slower Frames object. It is passed to a Path
for
+calculation of the actual scan path.
A single Frames object returned from Path.consume
represents a chunk of
+frames forming part of a scan path, for interpretation by the code
+that will actually perform the scan.
See also
+ +The midpoints of scan frames for each axis
+The lower bounds of each scan frame in each axis for fly-scanning
+The upper bounds of each scan frame in each axis for fly-scanning
+Whether there is a gap between this frame and the previous. First +element is whether there is a gap between the last frame and the first
+Return a new Frames object restricted to the indices provided.
+indices – The indices of the frames to extract, modulo scan length
calculate_gap – If True then recalculate the gap from upper and lower
>>> frames = Frames({"x": np.array([1, 2, 3])})
+>>> frames.extract(np.array([1, 0, 1])).midpoints
+{'x': array([2, 1, 2])}
+
Return a new Frames object concatenating self and other.
+Requires both Frames objects to have the same axes, but not necessarily in +the same order. The order is inherited from self, so other may be reordered.
+other – The Frames to concatenate to self
gap – Whether to force a gap between the two Frames objects
>>> frames = Frames({"x": np.array([1, 2, 3]), "y": np.array([6, 5, 4])})
+>>> frames2 = Frames({"y": np.array([3, 2, 1]), "x": np.array([4, 5, 6])})
+>>> frames.concat(frames2).midpoints
+{'x': array([1, 2, 3, 4, 5, 6]), 'y': array([6, 5, 4, 3, 2, 1])}
+
Return a new Frames object merging self and other.
+Require both Frames objects to not share axes.
+>>> fx = Frames({"x": np.array([1, 2, 3])})
+>>> fy = Frames({"y": np.array([5, 6, 7])})
+>>> fx.zip(fy).midpoints
+{'x': array([1, 2, 3]), 'y': array([5, 6, 7])}
+
Like a Frames
object, but each alternate repetition will run in reverse.
Create a snaked version of a Frames
object.
Return a new Frames object restricted to the indices provided.
+indices – The indices of the frames to extract, can extend past len(self)
calculate_gap – If True then recalculate the gap from upper and lower
>>> frames = SnakedFrames({"x": np.array([1, 2, 3])})
+>>> frames.extract(np.array([0, 1, 2, 3, 4, 5])).midpoints
+{'x': array([1, 2, 3, 3, 2, 1])}
+
Is there a gap between end of frames1 and start of frames2.
+Squash a stack of nested Frames into a single one.
+stack – The Frames stack to squash, from slowest to fastest moving
check_path_changes – If True then check that nesting the output +Frames object within others will provide the same path +as nesting the input Frames stack within others
See also
+ +>>> fx = SnakedFrames({"x": np.array([1, 2])})
+>>> fy = Frames({"y": np.array([3, 4])})
+>>> squash_frames([fy, fx]).midpoints
+{'y': array([3, 3, 4, 4]), 'x': array([1, 2, 2, 1])}
+
A consumable route through a stack of Frames, representing a scan path.
+stack – The Frames stack describing the scan, from slowest to fastest +moving
start – The index of where in the Path to start
num – The number of scan frames to produce after start. None means up to +the end
See also
+ +The Frames stack describing the scan, from slowest to fastest moving
+Index that is next to be consumed
+The lengths of all the stack
+Index of the end frame, one more than the last index that will be +produced
+Consume at most num frames from the Path and return as a Frames object.
+>>> fx = SnakedFrames({"x": np.array([1, 2])})
+>>> fy = Frames({"y": np.array([3, 4])})
+>>> path = Path([fy, fx])
+>>> path.consume(3).midpoints
+{'y': array([3, 3, 4]), 'x': array([1, 2, 2])}
+>>> path.consume(3).midpoints
+{'y': array([4]), 'x': array([1])}
+>>> path.consume(3).midpoints
+{'y': array([], dtype=int64), 'x': array([], dtype=int64)}
+
Convenience iterable that produces the scan midpoints for each axis.
+For better performance, consume from a Path
instead.
stack – The stack of Frames describing the scan, from slowest to fastest +moving
+See also
+ +>>> fx = SnakedFrames({"x": np.array([1, 2])})
+>>> fy = Frames({"y": np.array([3, 4])})
+>>> mp = Midpoints([fy, fx])
+>>> for p in mp: print(p)
+{'y': np.int64(3), 'x': np.int64(1)}
+{'y': np.int64(3), 'x': np.int64(2)}
+{'y': np.int64(4), 'x': np.int64(2)}
+{'y': np.int64(4), 'x': np.int64(1)}
+
The stack of Frames describing the scan, from slowest to fastest moving
+Add all subclasses of super_cls to a discriminated union.
+For all subclasses of super_cls, add a discriminator field to identify +the type. Raw JSON should look like {<discriminator>: <type name>, params for +<type name>…}.
+Subclasses that extend this class must be Pydantic dataclasses, and types that +need their schema to be updated when a new type that extends super_cls is +created must be either Pydantic dataclasses or BaseModels.
+Example:
+@discriminated_union_of_subclasses
+class Expression(ABC):
+ @abstractmethod
+ def calculate(self) -> int:
+ ...
+
+
+@dataclass
+class Add(Expression):
+ left: Expression
+ right: Expression
+
+ def calculate(self) -> int:
+ return self.left.calculate() + self.right.calculate()
+
+
+@dataclass
+class Subtract(Expression):
+ left: Expression
+ right: Expression
+
+ def calculate(self) -> int:
+ return self.left.calculate() - self.right.calculate()
+
+
+@dataclass
+class IntLiteral(Expression):
+ value: int
+
+ def calculate(self) -> int:
+ return self.value
+
+
+my_sum = Add(IntLiteral(5), Subtract(IntLiteral(10), IntLiteral(2)))
+assert my_sum.calculate() == 13
+
+assert my_sum == parse_obj_as(
+ Expression,
+ {
+ "type": "Add",
+ "left": {"type": "IntLiteral", "value": 5},
+ "right": {
+ "type": "Subtract",
+ "left": {"type": "IntLiteral", "value": 10},
+ "right": {"type": "IntLiteral", "value": 2},
+ },
+ },
+)
+
super_cls – The superclass of the union, Expression in the above example
discriminator – The discriminator that will be inserted into the +serialized documents for type determination. Defaults to “type”.
to its discriminated union for deserialization
+Type
+Used to ensure pydantic dataclasses error if given extra arguments
+scanspec
#Top level API.
+Version number as calculated by pypa/setuptools_scm
+Submodules
+ +scanspec.plot
#plot_spec
to visualize a scan.
Members
++ | Plot a spec, drawing the path taken through the scan. |
+
Plot a spec, drawing the path taken through the scan.
+Uses a different colour for each frame, grey for the turnarounds, and +marks the midpoints with a filled circle if there are less than 200 of +them. If the scan is 2D then 2D regions are shown in black.
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.specs import Line
+from scanspec.regions import Circle
+
+cube = Line("z", 1, 3, 3) * Line("y", 1, 3, 10) * ~Line("x", 0, 2, 10)
+spec = cube & Circle("x", "y", 1, 2, 0.9)
+plot_spec(spec)
+
(Source code
, png
, hires.png
, pdf
)
scanspec.regions
#Region
and its subclasses.
Members
++ | + |
+ | Return a mask of the points inside the region. |
+
+ | Recursively yield Regions from obj and its children. |
+
Abstract baseclass for a Region that can Mask
a Spec
.
Supports operators:
+|
: UnionOf
two Regions, midpoints present in either
&
: IntersectionOf
two Regions, midpoints present in both
-
: DifferenceOf
two Regions, midpoints present in first not second
^
: SymmetricDifferenceOf
two Regions, midpoints present in one not both
Produce the non-overlapping sets of axes this region spans.
+Return a mask of the points inside the region.
+If there is an overlap of axes of region and points return a +mask of the points in the region, otherwise return all ones
+Abstract baseclass for a combination of two regions, left and right.
+Show JSON schema
{
+ "$defs": {
+ "Circle": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within an xy circle of given radius.\n\n.. example_spec::\n\n from scanspec.regions import Circle\n from scanspec.specs import Line\n\n grid = Line(\"y\", 1, 3, 10) * ~Line(\"x\", 0, 2, 10)\n spec = grid & Circle(\"x\", \"y\", 1, 2, 0.9)",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_middle": {
+ "description": "The central x point of the circle",
+ "title": "X Middle",
+ "type": "number"
+ },
+ "y_middle": {
+ "description": "The central y point of the circle",
+ "title": "Y Middle",
+ "type": "number"
+ },
+ "radius": {
+ "description": "Radius of the circle",
+ "exclusiveMinimum": 0.0,
+ "title": "Radius",
+ "type": "number"
+ },
+ "type": {
+ "const": "Circle",
+ "default": "Circle",
+ "enum": [
+ "Circle"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_middle",
+ "y_middle",
+ "radius"
+ ],
+ "title": "Circle",
+ "type": "object"
+ },
+ "CombinationOf": {
+ "additionalProperties": false,
+ "description": "Abstract baseclass for a combination of two regions, left and right.",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "CombinationOf",
+ "default": "CombinationOf",
+ "enum": [
+ "CombinationOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "CombinationOf",
+ "type": "object"
+ },
+ "DifferenceOf": {
+ "additionalProperties": false,
+ "description": "A point is in DifferenceOf(a, b) if in a and not in b.\n\nTypically created with the ``-`` operator.\n\n>>> r = Range(\"x\", 0.5, 2.5) - Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, False, False, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "DifferenceOf",
+ "default": "DifferenceOf",
+ "enum": [
+ "DifferenceOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "DifferenceOf",
+ "type": "object"
+ },
+ "Ellipse": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within an xy ellipse of given radius.\n\n.. example_spec::\n\n from scanspec.regions import Ellipse\n from scanspec.specs import Line\n\n grid = Line(\"y\", 3, 8, 10) * ~Line(\"x\", 1 ,8, 10)\n spec = grid & Ellipse(\"x\", \"y\", 5, 5, 2, 3, 75)",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_middle": {
+ "description": "The central x point of the ellipse",
+ "title": "X Middle",
+ "type": "number"
+ },
+ "y_middle": {
+ "description": "The central y point of the ellipse",
+ "title": "Y Middle",
+ "type": "number"
+ },
+ "x_radius": {
+ "description": "The radius along the x axis of the ellipse",
+ "exclusiveMinimum": 0.0,
+ "title": "X Radius",
+ "type": "number"
+ },
+ "y_radius": {
+ "description": "The radius along the y axis of the ellipse",
+ "exclusiveMinimum": 0.0,
+ "title": "Y Radius",
+ "type": "number"
+ },
+ "angle": {
+ "default": 0.0,
+ "description": "The angle of the ellipse (degrees)",
+ "title": "Angle",
+ "type": "number"
+ },
+ "type": {
+ "const": "Ellipse",
+ "default": "Ellipse",
+ "enum": [
+ "Ellipse"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_middle",
+ "y_middle",
+ "x_radius",
+ "y_radius"
+ ],
+ "title": "Ellipse",
+ "type": "object"
+ },
+ "IntersectionOf": {
+ "additionalProperties": false,
+ "description": "A point is in IntersectionOf(a, b) if in both a and b.\n\nTypically created with the ``&`` operator.\n\n>>> r = Range(\"x\", 0.5, 2.5) & Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, False, True, False, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "IntersectionOf",
+ "default": "IntersectionOf",
+ "enum": [
+ "IntersectionOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "IntersectionOf",
+ "type": "object"
+ },
+ "Polygon": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within a rotated xy polygon.\n\n.. example_spec::\n\n from scanspec.regions import Polygon\n from scanspec.specs import Line\n\n grid = Line(\"y\", 3, 8, 10) * ~Line(\"x\", 1 ,8, 10)\n spec = grid & Polygon(\"x\", \"y\", [1.0, 6.0, 8.0, 2.0], [4.0, 10.0, 6.0, 1.0])",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_verts": {
+ "description": "The Nx1 x coordinates of the polygons vertices",
+ "items": {
+ "type": "number"
+ },
+ "minItems": 3,
+ "title": "X Verts",
+ "type": "array"
+ },
+ "y_verts": {
+ "description": "The Nx1 y coordinates of the polygons vertices",
+ "items": {
+ "type": "number"
+ },
+ "minItems": 3,
+ "title": "Y Verts",
+ "type": "array"
+ },
+ "type": {
+ "const": "Polygon",
+ "default": "Polygon",
+ "enum": [
+ "Polygon"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_verts",
+ "y_verts"
+ ],
+ "title": "Polygon",
+ "type": "object"
+ },
+ "Range": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis >= min and <= max.\n\n>>> r = Range(\"x\", 1, 2)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, True, False, False])",
+ "properties": {
+ "axis": {
+ "description": "The name matching the axis to mask in spec",
+ "title": "Axis"
+ },
+ "min": {
+ "description": "The minimum inclusive value in the region",
+ "title": "Min",
+ "type": "number"
+ },
+ "max": {
+ "description": "The minimum inclusive value in the region",
+ "title": "Max",
+ "type": "number"
+ },
+ "type": {
+ "const": "Range",
+ "default": "Range",
+ "enum": [
+ "Range"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "axis",
+ "min",
+ "max"
+ ],
+ "title": "Range",
+ "type": "object"
+ },
+ "Rectangle": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within a rotated xy rectangle.\n\n.. example_spec::\n\n from scanspec.regions import Rectangle\n from scanspec.specs import Line\n\n grid = Line(\"y\", 1, 3, 10) * ~Line(\"x\", 0, 2, 10)\n spec = grid & Rectangle(\"x\", \"y\", 0, 1.1, 1.5, 2.1, 30)",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_min": {
+ "description": "Minimum inclusive x value in the region",
+ "title": "X Min",
+ "type": "number"
+ },
+ "y_min": {
+ "description": "Minimum inclusive y value in the region",
+ "title": "Y Min",
+ "type": "number"
+ },
+ "x_max": {
+ "description": "Maximum inclusive x value in the region",
+ "title": "X Max",
+ "type": "number"
+ },
+ "y_max": {
+ "description": "Maximum inclusive y value in the region",
+ "title": "Y Max",
+ "type": "number"
+ },
+ "angle": {
+ "default": 0.0,
+ "description": "Clockwise rotation angle of the rectangle",
+ "title": "Angle",
+ "type": "number"
+ },
+ "type": {
+ "const": "Rectangle",
+ "default": "Rectangle",
+ "enum": [
+ "Rectangle"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_min",
+ "y_min",
+ "x_max",
+ "y_max"
+ ],
+ "title": "Rectangle",
+ "type": "object"
+ },
+ "Region": {
+ "discriminator": {
+ "mapping": {
+ "Circle": "#/$defs/Circle",
+ "CombinationOf": "#/$defs/CombinationOf",
+ "DifferenceOf": "#/$defs/DifferenceOf",
+ "Ellipse": "#/$defs/Ellipse",
+ "IntersectionOf": "#/$defs/IntersectionOf",
+ "Polygon": "#/$defs/Polygon",
+ "Range": "#/$defs/Range",
+ "Rectangle": "#/$defs/Rectangle",
+ "SymmetricDifferenceOf": "#/$defs/SymmetricDifferenceOf",
+ "UnionOf": "#/$defs/UnionOf"
+ },
+ "propertyName": "type"
+ },
+ "oneOf": [
+ {
+ "$ref": "#/$defs/CombinationOf"
+ },
+ {
+ "$ref": "#/$defs/UnionOf"
+ },
+ {
+ "$ref": "#/$defs/IntersectionOf"
+ },
+ {
+ "$ref": "#/$defs/DifferenceOf"
+ },
+ {
+ "$ref": "#/$defs/SymmetricDifferenceOf"
+ },
+ {
+ "$ref": "#/$defs/Range"
+ },
+ {
+ "$ref": "#/$defs/Rectangle"
+ },
+ {
+ "$ref": "#/$defs/Polygon"
+ },
+ {
+ "$ref": "#/$defs/Circle"
+ },
+ {
+ "$ref": "#/$defs/Ellipse"
+ }
+ ]
+ },
+ "SymmetricDifferenceOf": {
+ "additionalProperties": false,
+ "description": "A point is in SymmetricDifferenceOf(a, b) if in either a or b, but not both.\n\nTypically created with the ``^`` operator.\n\n>>> r = Range(\"x\", 0.5, 2.5) ^ Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, False, True, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "SymmetricDifferenceOf",
+ "default": "SymmetricDifferenceOf",
+ "enum": [
+ "SymmetricDifferenceOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "SymmetricDifferenceOf",
+ "type": "object"
+ },
+ "UnionOf": {
+ "additionalProperties": false,
+ "description": "A point is in UnionOf(a, b) if in either a or b.\n\nTypically created with the ``|`` operator\n\n>>> r = Range(\"x\", 0.5, 2.5) | Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, True, True, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "UnionOf",
+ "default": "UnionOf",
+ "enum": [
+ "UnionOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "UnionOf",
+ "type": "object"
+ }
+ },
+ "$ref": "#/$defs/CombinationOf"
+}
+
A point is in UnionOf(a, b) if in either a or b.
+Typically created with the |
operator
>>> r = Range("x", 0.5, 2.5) | Range("x", 1.5, 3.5)
+>>> r.mask({"x": np.array([0, 1, 2, 3, 4])})
+array([False, True, True, True, False])
+
Show JSON schema
{
+ "$defs": {
+ "Circle": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within an xy circle of given radius.\n\n.. example_spec::\n\n from scanspec.regions import Circle\n from scanspec.specs import Line\n\n grid = Line(\"y\", 1, 3, 10) * ~Line(\"x\", 0, 2, 10)\n spec = grid & Circle(\"x\", \"y\", 1, 2, 0.9)",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_middle": {
+ "description": "The central x point of the circle",
+ "title": "X Middle",
+ "type": "number"
+ },
+ "y_middle": {
+ "description": "The central y point of the circle",
+ "title": "Y Middle",
+ "type": "number"
+ },
+ "radius": {
+ "description": "Radius of the circle",
+ "exclusiveMinimum": 0.0,
+ "title": "Radius",
+ "type": "number"
+ },
+ "type": {
+ "const": "Circle",
+ "default": "Circle",
+ "enum": [
+ "Circle"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_middle",
+ "y_middle",
+ "radius"
+ ],
+ "title": "Circle",
+ "type": "object"
+ },
+ "CombinationOf": {
+ "additionalProperties": false,
+ "description": "Abstract baseclass for a combination of two regions, left and right.",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "CombinationOf",
+ "default": "CombinationOf",
+ "enum": [
+ "CombinationOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "CombinationOf",
+ "type": "object"
+ },
+ "DifferenceOf": {
+ "additionalProperties": false,
+ "description": "A point is in DifferenceOf(a, b) if in a and not in b.\n\nTypically created with the ``-`` operator.\n\n>>> r = Range(\"x\", 0.5, 2.5) - Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, False, False, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "DifferenceOf",
+ "default": "DifferenceOf",
+ "enum": [
+ "DifferenceOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "DifferenceOf",
+ "type": "object"
+ },
+ "Ellipse": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within an xy ellipse of given radius.\n\n.. example_spec::\n\n from scanspec.regions import Ellipse\n from scanspec.specs import Line\n\n grid = Line(\"y\", 3, 8, 10) * ~Line(\"x\", 1 ,8, 10)\n spec = grid & Ellipse(\"x\", \"y\", 5, 5, 2, 3, 75)",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_middle": {
+ "description": "The central x point of the ellipse",
+ "title": "X Middle",
+ "type": "number"
+ },
+ "y_middle": {
+ "description": "The central y point of the ellipse",
+ "title": "Y Middle",
+ "type": "number"
+ },
+ "x_radius": {
+ "description": "The radius along the x axis of the ellipse",
+ "exclusiveMinimum": 0.0,
+ "title": "X Radius",
+ "type": "number"
+ },
+ "y_radius": {
+ "description": "The radius along the y axis of the ellipse",
+ "exclusiveMinimum": 0.0,
+ "title": "Y Radius",
+ "type": "number"
+ },
+ "angle": {
+ "default": 0.0,
+ "description": "The angle of the ellipse (degrees)",
+ "title": "Angle",
+ "type": "number"
+ },
+ "type": {
+ "const": "Ellipse",
+ "default": "Ellipse",
+ "enum": [
+ "Ellipse"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_middle",
+ "y_middle",
+ "x_radius",
+ "y_radius"
+ ],
+ "title": "Ellipse",
+ "type": "object"
+ },
+ "IntersectionOf": {
+ "additionalProperties": false,
+ "description": "A point is in IntersectionOf(a, b) if in both a and b.\n\nTypically created with the ``&`` operator.\n\n>>> r = Range(\"x\", 0.5, 2.5) & Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, False, True, False, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "IntersectionOf",
+ "default": "IntersectionOf",
+ "enum": [
+ "IntersectionOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "IntersectionOf",
+ "type": "object"
+ },
+ "Polygon": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within a rotated xy polygon.\n\n.. example_spec::\n\n from scanspec.regions import Polygon\n from scanspec.specs import Line\n\n grid = Line(\"y\", 3, 8, 10) * ~Line(\"x\", 1 ,8, 10)\n spec = grid & Polygon(\"x\", \"y\", [1.0, 6.0, 8.0, 2.0], [4.0, 10.0, 6.0, 1.0])",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_verts": {
+ "description": "The Nx1 x coordinates of the polygons vertices",
+ "items": {
+ "type": "number"
+ },
+ "minItems": 3,
+ "title": "X Verts",
+ "type": "array"
+ },
+ "y_verts": {
+ "description": "The Nx1 y coordinates of the polygons vertices",
+ "items": {
+ "type": "number"
+ },
+ "minItems": 3,
+ "title": "Y Verts",
+ "type": "array"
+ },
+ "type": {
+ "const": "Polygon",
+ "default": "Polygon",
+ "enum": [
+ "Polygon"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_verts",
+ "y_verts"
+ ],
+ "title": "Polygon",
+ "type": "object"
+ },
+ "Range": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis >= min and <= max.\n\n>>> r = Range(\"x\", 1, 2)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, True, False, False])",
+ "properties": {
+ "axis": {
+ "description": "The name matching the axis to mask in spec",
+ "title": "Axis"
+ },
+ "min": {
+ "description": "The minimum inclusive value in the region",
+ "title": "Min",
+ "type": "number"
+ },
+ "max": {
+ "description": "The minimum inclusive value in the region",
+ "title": "Max",
+ "type": "number"
+ },
+ "type": {
+ "const": "Range",
+ "default": "Range",
+ "enum": [
+ "Range"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "axis",
+ "min",
+ "max"
+ ],
+ "title": "Range",
+ "type": "object"
+ },
+ "Rectangle": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within a rotated xy rectangle.\n\n.. example_spec::\n\n from scanspec.regions import Rectangle\n from scanspec.specs import Line\n\n grid = Line(\"y\", 1, 3, 10) * ~Line(\"x\", 0, 2, 10)\n spec = grid & Rectangle(\"x\", \"y\", 0, 1.1, 1.5, 2.1, 30)",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_min": {
+ "description": "Minimum inclusive x value in the region",
+ "title": "X Min",
+ "type": "number"
+ },
+ "y_min": {
+ "description": "Minimum inclusive y value in the region",
+ "title": "Y Min",
+ "type": "number"
+ },
+ "x_max": {
+ "description": "Maximum inclusive x value in the region",
+ "title": "X Max",
+ "type": "number"
+ },
+ "y_max": {
+ "description": "Maximum inclusive y value in the region",
+ "title": "Y Max",
+ "type": "number"
+ },
+ "angle": {
+ "default": 0.0,
+ "description": "Clockwise rotation angle of the rectangle",
+ "title": "Angle",
+ "type": "number"
+ },
+ "type": {
+ "const": "Rectangle",
+ "default": "Rectangle",
+ "enum": [
+ "Rectangle"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_min",
+ "y_min",
+ "x_max",
+ "y_max"
+ ],
+ "title": "Rectangle",
+ "type": "object"
+ },
+ "Region": {
+ "discriminator": {
+ "mapping": {
+ "Circle": "#/$defs/Circle",
+ "CombinationOf": "#/$defs/CombinationOf",
+ "DifferenceOf": "#/$defs/DifferenceOf",
+ "Ellipse": "#/$defs/Ellipse",
+ "IntersectionOf": "#/$defs/IntersectionOf",
+ "Polygon": "#/$defs/Polygon",
+ "Range": "#/$defs/Range",
+ "Rectangle": "#/$defs/Rectangle",
+ "SymmetricDifferenceOf": "#/$defs/SymmetricDifferenceOf",
+ "UnionOf": "#/$defs/UnionOf"
+ },
+ "propertyName": "type"
+ },
+ "oneOf": [
+ {
+ "$ref": "#/$defs/CombinationOf"
+ },
+ {
+ "$ref": "#/$defs/UnionOf"
+ },
+ {
+ "$ref": "#/$defs/IntersectionOf"
+ },
+ {
+ "$ref": "#/$defs/DifferenceOf"
+ },
+ {
+ "$ref": "#/$defs/SymmetricDifferenceOf"
+ },
+ {
+ "$ref": "#/$defs/Range"
+ },
+ {
+ "$ref": "#/$defs/Rectangle"
+ },
+ {
+ "$ref": "#/$defs/Polygon"
+ },
+ {
+ "$ref": "#/$defs/Circle"
+ },
+ {
+ "$ref": "#/$defs/Ellipse"
+ }
+ ]
+ },
+ "SymmetricDifferenceOf": {
+ "additionalProperties": false,
+ "description": "A point is in SymmetricDifferenceOf(a, b) if in either a or b, but not both.\n\nTypically created with the ``^`` operator.\n\n>>> r = Range(\"x\", 0.5, 2.5) ^ Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, False, True, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "SymmetricDifferenceOf",
+ "default": "SymmetricDifferenceOf",
+ "enum": [
+ "SymmetricDifferenceOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "SymmetricDifferenceOf",
+ "type": "object"
+ },
+ "UnionOf": {
+ "additionalProperties": false,
+ "description": "A point is in UnionOf(a, b) if in either a or b.\n\nTypically created with the ``|`` operator\n\n>>> r = Range(\"x\", 0.5, 2.5) | Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, True, True, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "UnionOf",
+ "default": "UnionOf",
+ "enum": [
+ "UnionOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "UnionOf",
+ "type": "object"
+ }
+ },
+ "$ref": "#/$defs/UnionOf"
+}
+
A point is in IntersectionOf(a, b) if in both a and b.
+Typically created with the &
operator.
>>> r = Range("x", 0.5, 2.5) & Range("x", 1.5, 3.5)
+>>> r.mask({"x": np.array([0, 1, 2, 3, 4])})
+array([False, False, True, False, False])
+
Show JSON schema
{
+ "$defs": {
+ "Circle": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within an xy circle of given radius.\n\n.. example_spec::\n\n from scanspec.regions import Circle\n from scanspec.specs import Line\n\n grid = Line(\"y\", 1, 3, 10) * ~Line(\"x\", 0, 2, 10)\n spec = grid & Circle(\"x\", \"y\", 1, 2, 0.9)",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_middle": {
+ "description": "The central x point of the circle",
+ "title": "X Middle",
+ "type": "number"
+ },
+ "y_middle": {
+ "description": "The central y point of the circle",
+ "title": "Y Middle",
+ "type": "number"
+ },
+ "radius": {
+ "description": "Radius of the circle",
+ "exclusiveMinimum": 0.0,
+ "title": "Radius",
+ "type": "number"
+ },
+ "type": {
+ "const": "Circle",
+ "default": "Circle",
+ "enum": [
+ "Circle"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_middle",
+ "y_middle",
+ "radius"
+ ],
+ "title": "Circle",
+ "type": "object"
+ },
+ "CombinationOf": {
+ "additionalProperties": false,
+ "description": "Abstract baseclass for a combination of two regions, left and right.",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "CombinationOf",
+ "default": "CombinationOf",
+ "enum": [
+ "CombinationOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "CombinationOf",
+ "type": "object"
+ },
+ "DifferenceOf": {
+ "additionalProperties": false,
+ "description": "A point is in DifferenceOf(a, b) if in a and not in b.\n\nTypically created with the ``-`` operator.\n\n>>> r = Range(\"x\", 0.5, 2.5) - Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, False, False, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "DifferenceOf",
+ "default": "DifferenceOf",
+ "enum": [
+ "DifferenceOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "DifferenceOf",
+ "type": "object"
+ },
+ "Ellipse": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within an xy ellipse of given radius.\n\n.. example_spec::\n\n from scanspec.regions import Ellipse\n from scanspec.specs import Line\n\n grid = Line(\"y\", 3, 8, 10) * ~Line(\"x\", 1 ,8, 10)\n spec = grid & Ellipse(\"x\", \"y\", 5, 5, 2, 3, 75)",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_middle": {
+ "description": "The central x point of the ellipse",
+ "title": "X Middle",
+ "type": "number"
+ },
+ "y_middle": {
+ "description": "The central y point of the ellipse",
+ "title": "Y Middle",
+ "type": "number"
+ },
+ "x_radius": {
+ "description": "The radius along the x axis of the ellipse",
+ "exclusiveMinimum": 0.0,
+ "title": "X Radius",
+ "type": "number"
+ },
+ "y_radius": {
+ "description": "The radius along the y axis of the ellipse",
+ "exclusiveMinimum": 0.0,
+ "title": "Y Radius",
+ "type": "number"
+ },
+ "angle": {
+ "default": 0.0,
+ "description": "The angle of the ellipse (degrees)",
+ "title": "Angle",
+ "type": "number"
+ },
+ "type": {
+ "const": "Ellipse",
+ "default": "Ellipse",
+ "enum": [
+ "Ellipse"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_middle",
+ "y_middle",
+ "x_radius",
+ "y_radius"
+ ],
+ "title": "Ellipse",
+ "type": "object"
+ },
+ "IntersectionOf": {
+ "additionalProperties": false,
+ "description": "A point is in IntersectionOf(a, b) if in both a and b.\n\nTypically created with the ``&`` operator.\n\n>>> r = Range(\"x\", 0.5, 2.5) & Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, False, True, False, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "IntersectionOf",
+ "default": "IntersectionOf",
+ "enum": [
+ "IntersectionOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "IntersectionOf",
+ "type": "object"
+ },
+ "Polygon": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within a rotated xy polygon.\n\n.. example_spec::\n\n from scanspec.regions import Polygon\n from scanspec.specs import Line\n\n grid = Line(\"y\", 3, 8, 10) * ~Line(\"x\", 1 ,8, 10)\n spec = grid & Polygon(\"x\", \"y\", [1.0, 6.0, 8.0, 2.0], [4.0, 10.0, 6.0, 1.0])",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_verts": {
+ "description": "The Nx1 x coordinates of the polygons vertices",
+ "items": {
+ "type": "number"
+ },
+ "minItems": 3,
+ "title": "X Verts",
+ "type": "array"
+ },
+ "y_verts": {
+ "description": "The Nx1 y coordinates of the polygons vertices",
+ "items": {
+ "type": "number"
+ },
+ "minItems": 3,
+ "title": "Y Verts",
+ "type": "array"
+ },
+ "type": {
+ "const": "Polygon",
+ "default": "Polygon",
+ "enum": [
+ "Polygon"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_verts",
+ "y_verts"
+ ],
+ "title": "Polygon",
+ "type": "object"
+ },
+ "Range": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis >= min and <= max.\n\n>>> r = Range(\"x\", 1, 2)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, True, False, False])",
+ "properties": {
+ "axis": {
+ "description": "The name matching the axis to mask in spec",
+ "title": "Axis"
+ },
+ "min": {
+ "description": "The minimum inclusive value in the region",
+ "title": "Min",
+ "type": "number"
+ },
+ "max": {
+ "description": "The minimum inclusive value in the region",
+ "title": "Max",
+ "type": "number"
+ },
+ "type": {
+ "const": "Range",
+ "default": "Range",
+ "enum": [
+ "Range"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "axis",
+ "min",
+ "max"
+ ],
+ "title": "Range",
+ "type": "object"
+ },
+ "Rectangle": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within a rotated xy rectangle.\n\n.. example_spec::\n\n from scanspec.regions import Rectangle\n from scanspec.specs import Line\n\n grid = Line(\"y\", 1, 3, 10) * ~Line(\"x\", 0, 2, 10)\n spec = grid & Rectangle(\"x\", \"y\", 0, 1.1, 1.5, 2.1, 30)",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_min": {
+ "description": "Minimum inclusive x value in the region",
+ "title": "X Min",
+ "type": "number"
+ },
+ "y_min": {
+ "description": "Minimum inclusive y value in the region",
+ "title": "Y Min",
+ "type": "number"
+ },
+ "x_max": {
+ "description": "Maximum inclusive x value in the region",
+ "title": "X Max",
+ "type": "number"
+ },
+ "y_max": {
+ "description": "Maximum inclusive y value in the region",
+ "title": "Y Max",
+ "type": "number"
+ },
+ "angle": {
+ "default": 0.0,
+ "description": "Clockwise rotation angle of the rectangle",
+ "title": "Angle",
+ "type": "number"
+ },
+ "type": {
+ "const": "Rectangle",
+ "default": "Rectangle",
+ "enum": [
+ "Rectangle"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_min",
+ "y_min",
+ "x_max",
+ "y_max"
+ ],
+ "title": "Rectangle",
+ "type": "object"
+ },
+ "Region": {
+ "discriminator": {
+ "mapping": {
+ "Circle": "#/$defs/Circle",
+ "CombinationOf": "#/$defs/CombinationOf",
+ "DifferenceOf": "#/$defs/DifferenceOf",
+ "Ellipse": "#/$defs/Ellipse",
+ "IntersectionOf": "#/$defs/IntersectionOf",
+ "Polygon": "#/$defs/Polygon",
+ "Range": "#/$defs/Range",
+ "Rectangle": "#/$defs/Rectangle",
+ "SymmetricDifferenceOf": "#/$defs/SymmetricDifferenceOf",
+ "UnionOf": "#/$defs/UnionOf"
+ },
+ "propertyName": "type"
+ },
+ "oneOf": [
+ {
+ "$ref": "#/$defs/CombinationOf"
+ },
+ {
+ "$ref": "#/$defs/UnionOf"
+ },
+ {
+ "$ref": "#/$defs/IntersectionOf"
+ },
+ {
+ "$ref": "#/$defs/DifferenceOf"
+ },
+ {
+ "$ref": "#/$defs/SymmetricDifferenceOf"
+ },
+ {
+ "$ref": "#/$defs/Range"
+ },
+ {
+ "$ref": "#/$defs/Rectangle"
+ },
+ {
+ "$ref": "#/$defs/Polygon"
+ },
+ {
+ "$ref": "#/$defs/Circle"
+ },
+ {
+ "$ref": "#/$defs/Ellipse"
+ }
+ ]
+ },
+ "SymmetricDifferenceOf": {
+ "additionalProperties": false,
+ "description": "A point is in SymmetricDifferenceOf(a, b) if in either a or b, but not both.\n\nTypically created with the ``^`` operator.\n\n>>> r = Range(\"x\", 0.5, 2.5) ^ Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, False, True, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "SymmetricDifferenceOf",
+ "default": "SymmetricDifferenceOf",
+ "enum": [
+ "SymmetricDifferenceOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "SymmetricDifferenceOf",
+ "type": "object"
+ },
+ "UnionOf": {
+ "additionalProperties": false,
+ "description": "A point is in UnionOf(a, b) if in either a or b.\n\nTypically created with the ``|`` operator\n\n>>> r = Range(\"x\", 0.5, 2.5) | Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, True, True, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "UnionOf",
+ "default": "UnionOf",
+ "enum": [
+ "UnionOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "UnionOf",
+ "type": "object"
+ }
+ },
+ "$ref": "#/$defs/IntersectionOf"
+}
+
A point is in DifferenceOf(a, b) if in a and not in b.
+Typically created with the -
operator.
>>> r = Range("x", 0.5, 2.5) - Range("x", 1.5, 3.5)
+>>> r.mask({"x": np.array([0, 1, 2, 3, 4])})
+array([False, True, False, False, False])
+
Show JSON schema
{
+ "$defs": {
+ "Circle": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within an xy circle of given radius.\n\n.. example_spec::\n\n from scanspec.regions import Circle\n from scanspec.specs import Line\n\n grid = Line(\"y\", 1, 3, 10) * ~Line(\"x\", 0, 2, 10)\n spec = grid & Circle(\"x\", \"y\", 1, 2, 0.9)",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_middle": {
+ "description": "The central x point of the circle",
+ "title": "X Middle",
+ "type": "number"
+ },
+ "y_middle": {
+ "description": "The central y point of the circle",
+ "title": "Y Middle",
+ "type": "number"
+ },
+ "radius": {
+ "description": "Radius of the circle",
+ "exclusiveMinimum": 0.0,
+ "title": "Radius",
+ "type": "number"
+ },
+ "type": {
+ "const": "Circle",
+ "default": "Circle",
+ "enum": [
+ "Circle"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_middle",
+ "y_middle",
+ "radius"
+ ],
+ "title": "Circle",
+ "type": "object"
+ },
+ "CombinationOf": {
+ "additionalProperties": false,
+ "description": "Abstract baseclass for a combination of two regions, left and right.",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "CombinationOf",
+ "default": "CombinationOf",
+ "enum": [
+ "CombinationOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "CombinationOf",
+ "type": "object"
+ },
+ "DifferenceOf": {
+ "additionalProperties": false,
+ "description": "A point is in DifferenceOf(a, b) if in a and not in b.\n\nTypically created with the ``-`` operator.\n\n>>> r = Range(\"x\", 0.5, 2.5) - Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, False, False, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "DifferenceOf",
+ "default": "DifferenceOf",
+ "enum": [
+ "DifferenceOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "DifferenceOf",
+ "type": "object"
+ },
+ "Ellipse": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within an xy ellipse of given radius.\n\n.. example_spec::\n\n from scanspec.regions import Ellipse\n from scanspec.specs import Line\n\n grid = Line(\"y\", 3, 8, 10) * ~Line(\"x\", 1 ,8, 10)\n spec = grid & Ellipse(\"x\", \"y\", 5, 5, 2, 3, 75)",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_middle": {
+ "description": "The central x point of the ellipse",
+ "title": "X Middle",
+ "type": "number"
+ },
+ "y_middle": {
+ "description": "The central y point of the ellipse",
+ "title": "Y Middle",
+ "type": "number"
+ },
+ "x_radius": {
+ "description": "The radius along the x axis of the ellipse",
+ "exclusiveMinimum": 0.0,
+ "title": "X Radius",
+ "type": "number"
+ },
+ "y_radius": {
+ "description": "The radius along the y axis of the ellipse",
+ "exclusiveMinimum": 0.0,
+ "title": "Y Radius",
+ "type": "number"
+ },
+ "angle": {
+ "default": 0.0,
+ "description": "The angle of the ellipse (degrees)",
+ "title": "Angle",
+ "type": "number"
+ },
+ "type": {
+ "const": "Ellipse",
+ "default": "Ellipse",
+ "enum": [
+ "Ellipse"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_middle",
+ "y_middle",
+ "x_radius",
+ "y_radius"
+ ],
+ "title": "Ellipse",
+ "type": "object"
+ },
+ "IntersectionOf": {
+ "additionalProperties": false,
+ "description": "A point is in IntersectionOf(a, b) if in both a and b.\n\nTypically created with the ``&`` operator.\n\n>>> r = Range(\"x\", 0.5, 2.5) & Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, False, True, False, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "IntersectionOf",
+ "default": "IntersectionOf",
+ "enum": [
+ "IntersectionOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "IntersectionOf",
+ "type": "object"
+ },
+ "Polygon": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within a rotated xy polygon.\n\n.. example_spec::\n\n from scanspec.regions import Polygon\n from scanspec.specs import Line\n\n grid = Line(\"y\", 3, 8, 10) * ~Line(\"x\", 1 ,8, 10)\n spec = grid & Polygon(\"x\", \"y\", [1.0, 6.0, 8.0, 2.0], [4.0, 10.0, 6.0, 1.0])",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_verts": {
+ "description": "The Nx1 x coordinates of the polygons vertices",
+ "items": {
+ "type": "number"
+ },
+ "minItems": 3,
+ "title": "X Verts",
+ "type": "array"
+ },
+ "y_verts": {
+ "description": "The Nx1 y coordinates of the polygons vertices",
+ "items": {
+ "type": "number"
+ },
+ "minItems": 3,
+ "title": "Y Verts",
+ "type": "array"
+ },
+ "type": {
+ "const": "Polygon",
+ "default": "Polygon",
+ "enum": [
+ "Polygon"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_verts",
+ "y_verts"
+ ],
+ "title": "Polygon",
+ "type": "object"
+ },
+ "Range": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis >= min and <= max.\n\n>>> r = Range(\"x\", 1, 2)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, True, False, False])",
+ "properties": {
+ "axis": {
+ "description": "The name matching the axis to mask in spec",
+ "title": "Axis"
+ },
+ "min": {
+ "description": "The minimum inclusive value in the region",
+ "title": "Min",
+ "type": "number"
+ },
+ "max": {
+ "description": "The minimum inclusive value in the region",
+ "title": "Max",
+ "type": "number"
+ },
+ "type": {
+ "const": "Range",
+ "default": "Range",
+ "enum": [
+ "Range"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "axis",
+ "min",
+ "max"
+ ],
+ "title": "Range",
+ "type": "object"
+ },
+ "Rectangle": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within a rotated xy rectangle.\n\n.. example_spec::\n\n from scanspec.regions import Rectangle\n from scanspec.specs import Line\n\n grid = Line(\"y\", 1, 3, 10) * ~Line(\"x\", 0, 2, 10)\n spec = grid & Rectangle(\"x\", \"y\", 0, 1.1, 1.5, 2.1, 30)",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_min": {
+ "description": "Minimum inclusive x value in the region",
+ "title": "X Min",
+ "type": "number"
+ },
+ "y_min": {
+ "description": "Minimum inclusive y value in the region",
+ "title": "Y Min",
+ "type": "number"
+ },
+ "x_max": {
+ "description": "Maximum inclusive x value in the region",
+ "title": "X Max",
+ "type": "number"
+ },
+ "y_max": {
+ "description": "Maximum inclusive y value in the region",
+ "title": "Y Max",
+ "type": "number"
+ },
+ "angle": {
+ "default": 0.0,
+ "description": "Clockwise rotation angle of the rectangle",
+ "title": "Angle",
+ "type": "number"
+ },
+ "type": {
+ "const": "Rectangle",
+ "default": "Rectangle",
+ "enum": [
+ "Rectangle"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_min",
+ "y_min",
+ "x_max",
+ "y_max"
+ ],
+ "title": "Rectangle",
+ "type": "object"
+ },
+ "Region": {
+ "discriminator": {
+ "mapping": {
+ "Circle": "#/$defs/Circle",
+ "CombinationOf": "#/$defs/CombinationOf",
+ "DifferenceOf": "#/$defs/DifferenceOf",
+ "Ellipse": "#/$defs/Ellipse",
+ "IntersectionOf": "#/$defs/IntersectionOf",
+ "Polygon": "#/$defs/Polygon",
+ "Range": "#/$defs/Range",
+ "Rectangle": "#/$defs/Rectangle",
+ "SymmetricDifferenceOf": "#/$defs/SymmetricDifferenceOf",
+ "UnionOf": "#/$defs/UnionOf"
+ },
+ "propertyName": "type"
+ },
+ "oneOf": [
+ {
+ "$ref": "#/$defs/CombinationOf"
+ },
+ {
+ "$ref": "#/$defs/UnionOf"
+ },
+ {
+ "$ref": "#/$defs/IntersectionOf"
+ },
+ {
+ "$ref": "#/$defs/DifferenceOf"
+ },
+ {
+ "$ref": "#/$defs/SymmetricDifferenceOf"
+ },
+ {
+ "$ref": "#/$defs/Range"
+ },
+ {
+ "$ref": "#/$defs/Rectangle"
+ },
+ {
+ "$ref": "#/$defs/Polygon"
+ },
+ {
+ "$ref": "#/$defs/Circle"
+ },
+ {
+ "$ref": "#/$defs/Ellipse"
+ }
+ ]
+ },
+ "SymmetricDifferenceOf": {
+ "additionalProperties": false,
+ "description": "A point is in SymmetricDifferenceOf(a, b) if in either a or b, but not both.\n\nTypically created with the ``^`` operator.\n\n>>> r = Range(\"x\", 0.5, 2.5) ^ Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, False, True, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "SymmetricDifferenceOf",
+ "default": "SymmetricDifferenceOf",
+ "enum": [
+ "SymmetricDifferenceOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "SymmetricDifferenceOf",
+ "type": "object"
+ },
+ "UnionOf": {
+ "additionalProperties": false,
+ "description": "A point is in UnionOf(a, b) if in either a or b.\n\nTypically created with the ``|`` operator\n\n>>> r = Range(\"x\", 0.5, 2.5) | Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, True, True, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "UnionOf",
+ "default": "UnionOf",
+ "enum": [
+ "UnionOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "UnionOf",
+ "type": "object"
+ }
+ },
+ "$ref": "#/$defs/DifferenceOf"
+}
+
A point is in SymmetricDifferenceOf(a, b) if in either a or b, but not both.
+Typically created with the ^
operator.
>>> r = Range("x", 0.5, 2.5) ^ Range("x", 1.5, 3.5)
+>>> r.mask({"x": np.array([0, 1, 2, 3, 4])})
+array([False, True, False, True, False])
+
Show JSON schema
{
+ "$defs": {
+ "Circle": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within an xy circle of given radius.\n\n.. example_spec::\n\n from scanspec.regions import Circle\n from scanspec.specs import Line\n\n grid = Line(\"y\", 1, 3, 10) * ~Line(\"x\", 0, 2, 10)\n spec = grid & Circle(\"x\", \"y\", 1, 2, 0.9)",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_middle": {
+ "description": "The central x point of the circle",
+ "title": "X Middle",
+ "type": "number"
+ },
+ "y_middle": {
+ "description": "The central y point of the circle",
+ "title": "Y Middle",
+ "type": "number"
+ },
+ "radius": {
+ "description": "Radius of the circle",
+ "exclusiveMinimum": 0.0,
+ "title": "Radius",
+ "type": "number"
+ },
+ "type": {
+ "const": "Circle",
+ "default": "Circle",
+ "enum": [
+ "Circle"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_middle",
+ "y_middle",
+ "radius"
+ ],
+ "title": "Circle",
+ "type": "object"
+ },
+ "CombinationOf": {
+ "additionalProperties": false,
+ "description": "Abstract baseclass for a combination of two regions, left and right.",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "CombinationOf",
+ "default": "CombinationOf",
+ "enum": [
+ "CombinationOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "CombinationOf",
+ "type": "object"
+ },
+ "DifferenceOf": {
+ "additionalProperties": false,
+ "description": "A point is in DifferenceOf(a, b) if in a and not in b.\n\nTypically created with the ``-`` operator.\n\n>>> r = Range(\"x\", 0.5, 2.5) - Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, False, False, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "DifferenceOf",
+ "default": "DifferenceOf",
+ "enum": [
+ "DifferenceOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "DifferenceOf",
+ "type": "object"
+ },
+ "Ellipse": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within an xy ellipse of given radius.\n\n.. example_spec::\n\n from scanspec.regions import Ellipse\n from scanspec.specs import Line\n\n grid = Line(\"y\", 3, 8, 10) * ~Line(\"x\", 1 ,8, 10)\n spec = grid & Ellipse(\"x\", \"y\", 5, 5, 2, 3, 75)",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_middle": {
+ "description": "The central x point of the ellipse",
+ "title": "X Middle",
+ "type": "number"
+ },
+ "y_middle": {
+ "description": "The central y point of the ellipse",
+ "title": "Y Middle",
+ "type": "number"
+ },
+ "x_radius": {
+ "description": "The radius along the x axis of the ellipse",
+ "exclusiveMinimum": 0.0,
+ "title": "X Radius",
+ "type": "number"
+ },
+ "y_radius": {
+ "description": "The radius along the y axis of the ellipse",
+ "exclusiveMinimum": 0.0,
+ "title": "Y Radius",
+ "type": "number"
+ },
+ "angle": {
+ "default": 0.0,
+ "description": "The angle of the ellipse (degrees)",
+ "title": "Angle",
+ "type": "number"
+ },
+ "type": {
+ "const": "Ellipse",
+ "default": "Ellipse",
+ "enum": [
+ "Ellipse"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_middle",
+ "y_middle",
+ "x_radius",
+ "y_radius"
+ ],
+ "title": "Ellipse",
+ "type": "object"
+ },
+ "IntersectionOf": {
+ "additionalProperties": false,
+ "description": "A point is in IntersectionOf(a, b) if in both a and b.\n\nTypically created with the ``&`` operator.\n\n>>> r = Range(\"x\", 0.5, 2.5) & Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, False, True, False, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "IntersectionOf",
+ "default": "IntersectionOf",
+ "enum": [
+ "IntersectionOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "IntersectionOf",
+ "type": "object"
+ },
+ "Polygon": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within a rotated xy polygon.\n\n.. example_spec::\n\n from scanspec.regions import Polygon\n from scanspec.specs import Line\n\n grid = Line(\"y\", 3, 8, 10) * ~Line(\"x\", 1 ,8, 10)\n spec = grid & Polygon(\"x\", \"y\", [1.0, 6.0, 8.0, 2.0], [4.0, 10.0, 6.0, 1.0])",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_verts": {
+ "description": "The Nx1 x coordinates of the polygons vertices",
+ "items": {
+ "type": "number"
+ },
+ "minItems": 3,
+ "title": "X Verts",
+ "type": "array"
+ },
+ "y_verts": {
+ "description": "The Nx1 y coordinates of the polygons vertices",
+ "items": {
+ "type": "number"
+ },
+ "minItems": 3,
+ "title": "Y Verts",
+ "type": "array"
+ },
+ "type": {
+ "const": "Polygon",
+ "default": "Polygon",
+ "enum": [
+ "Polygon"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_verts",
+ "y_verts"
+ ],
+ "title": "Polygon",
+ "type": "object"
+ },
+ "Range": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis >= min and <= max.\n\n>>> r = Range(\"x\", 1, 2)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, True, False, False])",
+ "properties": {
+ "axis": {
+ "description": "The name matching the axis to mask in spec",
+ "title": "Axis"
+ },
+ "min": {
+ "description": "The minimum inclusive value in the region",
+ "title": "Min",
+ "type": "number"
+ },
+ "max": {
+ "description": "The minimum inclusive value in the region",
+ "title": "Max",
+ "type": "number"
+ },
+ "type": {
+ "const": "Range",
+ "default": "Range",
+ "enum": [
+ "Range"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "axis",
+ "min",
+ "max"
+ ],
+ "title": "Range",
+ "type": "object"
+ },
+ "Rectangle": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within a rotated xy rectangle.\n\n.. example_spec::\n\n from scanspec.regions import Rectangle\n from scanspec.specs import Line\n\n grid = Line(\"y\", 1, 3, 10) * ~Line(\"x\", 0, 2, 10)\n spec = grid & Rectangle(\"x\", \"y\", 0, 1.1, 1.5, 2.1, 30)",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_min": {
+ "description": "Minimum inclusive x value in the region",
+ "title": "X Min",
+ "type": "number"
+ },
+ "y_min": {
+ "description": "Minimum inclusive y value in the region",
+ "title": "Y Min",
+ "type": "number"
+ },
+ "x_max": {
+ "description": "Maximum inclusive x value in the region",
+ "title": "X Max",
+ "type": "number"
+ },
+ "y_max": {
+ "description": "Maximum inclusive y value in the region",
+ "title": "Y Max",
+ "type": "number"
+ },
+ "angle": {
+ "default": 0.0,
+ "description": "Clockwise rotation angle of the rectangle",
+ "title": "Angle",
+ "type": "number"
+ },
+ "type": {
+ "const": "Rectangle",
+ "default": "Rectangle",
+ "enum": [
+ "Rectangle"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_min",
+ "y_min",
+ "x_max",
+ "y_max"
+ ],
+ "title": "Rectangle",
+ "type": "object"
+ },
+ "Region": {
+ "discriminator": {
+ "mapping": {
+ "Circle": "#/$defs/Circle",
+ "CombinationOf": "#/$defs/CombinationOf",
+ "DifferenceOf": "#/$defs/DifferenceOf",
+ "Ellipse": "#/$defs/Ellipse",
+ "IntersectionOf": "#/$defs/IntersectionOf",
+ "Polygon": "#/$defs/Polygon",
+ "Range": "#/$defs/Range",
+ "Rectangle": "#/$defs/Rectangle",
+ "SymmetricDifferenceOf": "#/$defs/SymmetricDifferenceOf",
+ "UnionOf": "#/$defs/UnionOf"
+ },
+ "propertyName": "type"
+ },
+ "oneOf": [
+ {
+ "$ref": "#/$defs/CombinationOf"
+ },
+ {
+ "$ref": "#/$defs/UnionOf"
+ },
+ {
+ "$ref": "#/$defs/IntersectionOf"
+ },
+ {
+ "$ref": "#/$defs/DifferenceOf"
+ },
+ {
+ "$ref": "#/$defs/SymmetricDifferenceOf"
+ },
+ {
+ "$ref": "#/$defs/Range"
+ },
+ {
+ "$ref": "#/$defs/Rectangle"
+ },
+ {
+ "$ref": "#/$defs/Polygon"
+ },
+ {
+ "$ref": "#/$defs/Circle"
+ },
+ {
+ "$ref": "#/$defs/Ellipse"
+ }
+ ]
+ },
+ "SymmetricDifferenceOf": {
+ "additionalProperties": false,
+ "description": "A point is in SymmetricDifferenceOf(a, b) if in either a or b, but not both.\n\nTypically created with the ``^`` operator.\n\n>>> r = Range(\"x\", 0.5, 2.5) ^ Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, False, True, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "SymmetricDifferenceOf",
+ "default": "SymmetricDifferenceOf",
+ "enum": [
+ "SymmetricDifferenceOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "SymmetricDifferenceOf",
+ "type": "object"
+ },
+ "UnionOf": {
+ "additionalProperties": false,
+ "description": "A point is in UnionOf(a, b) if in either a or b.\n\nTypically created with the ``|`` operator\n\n>>> r = Range(\"x\", 0.5, 2.5) | Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, True, True, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "UnionOf",
+ "default": "UnionOf",
+ "enum": [
+ "UnionOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "UnionOf",
+ "type": "object"
+ }
+ },
+ "$ref": "#/$defs/SymmetricDifferenceOf"
+}
+
Mask contains points of axis >= min and <= max.
+>>> r = Range("x", 1, 2)
+>>> r.mask({"x": np.array([0, 1, 2, 3, 4])})
+array([False, True, True, False, False])
+
Show JSON schema
{
+ "title": "Range",
+ "description": "Mask contains points of axis >= min and <= max.\n\n>>> r = Range(\"x\", 1, 2)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, True, False, False])",
+ "type": "object",
+ "properties": {
+ "axis": {
+ "description": "The name matching the axis to mask in spec",
+ "title": "Axis"
+ },
+ "min": {
+ "description": "The minimum inclusive value in the region",
+ "title": "Min",
+ "type": "number"
+ },
+ "max": {
+ "description": "The minimum inclusive value in the region",
+ "title": "Max",
+ "type": "number"
+ },
+ "type": {
+ "const": "Range",
+ "default": "Range",
+ "enum": [
+ "Range"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "axis",
+ "min",
+ "max"
+ ]
+}
+
Mask contains points of axis within a rotated xy rectangle.
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.regions import Rectangle
+from scanspec.specs import Line
+
+grid = Line("y", 1, 3, 10) * ~Line("x", 0, 2, 10)
+spec = grid & Rectangle("x", "y", 0, 1.1, 1.5, 2.1, 30)
+plot_spec(spec)
+
(Source code
, png
, hires.png
, pdf
)
Show JSON schema
{
+ "title": "Rectangle",
+ "description": "Mask contains points of axis within a rotated xy rectangle.\n\n.. example_spec::\n\n from scanspec.regions import Rectangle\n from scanspec.specs import Line\n\n grid = Line(\"y\", 1, 3, 10) * ~Line(\"x\", 0, 2, 10)\n spec = grid & Rectangle(\"x\", \"y\", 0, 1.1, 1.5, 2.1, 30)",
+ "type": "object",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_min": {
+ "description": "Minimum inclusive x value in the region",
+ "title": "X Min",
+ "type": "number"
+ },
+ "y_min": {
+ "description": "Minimum inclusive y value in the region",
+ "title": "Y Min",
+ "type": "number"
+ },
+ "x_max": {
+ "description": "Maximum inclusive x value in the region",
+ "title": "X Max",
+ "type": "number"
+ },
+ "y_max": {
+ "description": "Maximum inclusive y value in the region",
+ "title": "Y Max",
+ "type": "number"
+ },
+ "angle": {
+ "default": 0.0,
+ "description": "Clockwise rotation angle of the rectangle",
+ "title": "Angle",
+ "type": "number"
+ },
+ "type": {
+ "const": "Rectangle",
+ "default": "Rectangle",
+ "enum": [
+ "Rectangle"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_min",
+ "y_min",
+ "x_max",
+ "y_max"
+ ]
+}
+
Mask contains points of axis within a rotated xy polygon.
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.regions import Polygon
+from scanspec.specs import Line
+
+grid = Line("y", 3, 8, 10) * ~Line("x", 1 ,8, 10)
+spec = grid & Polygon("x", "y", [1.0, 6.0, 8.0, 2.0], [4.0, 10.0, 6.0, 1.0])
+plot_spec(spec)
+
(Source code
, png
, hires.png
, pdf
)
Show JSON schema
{
+ "title": "Polygon",
+ "description": "Mask contains points of axis within a rotated xy polygon.\n\n.. example_spec::\n\n from scanspec.regions import Polygon\n from scanspec.specs import Line\n\n grid = Line(\"y\", 3, 8, 10) * ~Line(\"x\", 1 ,8, 10)\n spec = grid & Polygon(\"x\", \"y\", [1.0, 6.0, 8.0, 2.0], [4.0, 10.0, 6.0, 1.0])",
+ "type": "object",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_verts": {
+ "description": "The Nx1 x coordinates of the polygons vertices",
+ "items": {
+ "type": "number"
+ },
+ "minItems": 3,
+ "title": "X Verts",
+ "type": "array"
+ },
+ "y_verts": {
+ "description": "The Nx1 y coordinates of the polygons vertices",
+ "items": {
+ "type": "number"
+ },
+ "minItems": 3,
+ "title": "Y Verts",
+ "type": "array"
+ },
+ "type": {
+ "const": "Polygon",
+ "default": "Polygon",
+ "enum": [
+ "Polygon"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_verts",
+ "y_verts"
+ ]
+}
+
The Nx1 x coordinates of the polygons vertices
+min_length = 3
The Nx1 y coordinates of the polygons vertices
+min_length = 3
Mask contains points of axis within an xy circle of given radius.
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.regions import Circle
+from scanspec.specs import Line
+
+grid = Line("y", 1, 3, 10) * ~Line("x", 0, 2, 10)
+spec = grid & Circle("x", "y", 1, 2, 0.9)
+plot_spec(spec)
+
(Source code
, png
, hires.png
, pdf
)
Show JSON schema
{
+ "title": "Circle",
+ "description": "Mask contains points of axis within an xy circle of given radius.\n\n.. example_spec::\n\n from scanspec.regions import Circle\n from scanspec.specs import Line\n\n grid = Line(\"y\", 1, 3, 10) * ~Line(\"x\", 0, 2, 10)\n spec = grid & Circle(\"x\", \"y\", 1, 2, 0.9)",
+ "type": "object",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_middle": {
+ "description": "The central x point of the circle",
+ "title": "X Middle",
+ "type": "number"
+ },
+ "y_middle": {
+ "description": "The central y point of the circle",
+ "title": "Y Middle",
+ "type": "number"
+ },
+ "radius": {
+ "description": "Radius of the circle",
+ "exclusiveMinimum": 0.0,
+ "title": "Radius",
+ "type": "number"
+ },
+ "type": {
+ "const": "Circle",
+ "default": "Circle",
+ "enum": [
+ "Circle"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_middle",
+ "y_middle",
+ "radius"
+ ]
+}
+
Mask contains points of axis within an xy ellipse of given radius.
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.regions import Ellipse
+from scanspec.specs import Line
+
+grid = Line("y", 3, 8, 10) * ~Line("x", 1 ,8, 10)
+spec = grid & Ellipse("x", "y", 5, 5, 2, 3, 75)
+plot_spec(spec)
+
(Source code
, png
, hires.png
, pdf
)
Show JSON schema
{
+ "title": "Ellipse",
+ "description": "Mask contains points of axis within an xy ellipse of given radius.\n\n.. example_spec::\n\n from scanspec.regions import Ellipse\n from scanspec.specs import Line\n\n grid = Line(\"y\", 3, 8, 10) * ~Line(\"x\", 1 ,8, 10)\n spec = grid & Ellipse(\"x\", \"y\", 5, 5, 2, 3, 75)",
+ "type": "object",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_middle": {
+ "description": "The central x point of the ellipse",
+ "title": "X Middle",
+ "type": "number"
+ },
+ "y_middle": {
+ "description": "The central y point of the ellipse",
+ "title": "Y Middle",
+ "type": "number"
+ },
+ "x_radius": {
+ "description": "The radius along the x axis of the ellipse",
+ "exclusiveMinimum": 0.0,
+ "title": "X Radius",
+ "type": "number"
+ },
+ "y_radius": {
+ "description": "The radius along the y axis of the ellipse",
+ "exclusiveMinimum": 0.0,
+ "title": "Y Radius",
+ "type": "number"
+ },
+ "angle": {
+ "default": 0.0,
+ "description": "The angle of the ellipse (degrees)",
+ "title": "Angle",
+ "type": "number"
+ },
+ "type": {
+ "const": "Ellipse",
+ "default": "Ellipse",
+ "enum": [
+ "Ellipse"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_middle",
+ "y_middle",
+ "x_radius",
+ "y_radius"
+ ]
+}
+
The radius along the x axis of the ellipse
+gt = 0.0
The radius along the y axis of the ellipse
+gt = 0.0
scanspec.specs
#Spec
and its subclasses.
Members
++ | Can be used as a special key to indicate how long each point should be |
+
+ | A serializable representation of the type and parameters of a scan. |
+
+ | Flyscan, zipping with fixed duration for every frame. |
+
+ | Step scan, with num frames of given duration at each frame in the spec. |
+
Can be used as a special key to indicate how long each point should be
+A serializable representation of the type and parameters of a scan.
+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
&
: Mask
the Spec with a Region
, excluding midpoints outside of it
~
: Snake
the Spec, reversing every other iteration of it
Return the list of axes that are present in the scan.
+Ordered from slowest moving to fastest moving.
+Produce a stack of nested Frames
that form the scan.
Ordered from slowest moving to fastest moving.
+Zip
the Spec with another, iterating in tandem.
Outer product of two Specs, nesting inner within outer.
+This means that inner will run in its entirety at each point in outer.
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.specs import Line
+
+spec = Line("y", 1, 2, 3) * Line("x", 3, 4, 12)
+plot_spec(spec)
+
(Source code
, png
, hires.png
, pdf
)
Show JSON schema
{
+ "$defs": {
+ "Circle": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within an xy circle of given radius.\n\n.. example_spec::\n\n from scanspec.regions import Circle\n from scanspec.specs import Line\n\n grid = Line(\"y\", 1, 3, 10) * ~Line(\"x\", 0, 2, 10)\n spec = grid & Circle(\"x\", \"y\", 1, 2, 0.9)",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_middle": {
+ "description": "The central x point of the circle",
+ "title": "X Middle",
+ "type": "number"
+ },
+ "y_middle": {
+ "description": "The central y point of the circle",
+ "title": "Y Middle",
+ "type": "number"
+ },
+ "radius": {
+ "description": "Radius of the circle",
+ "exclusiveMinimum": 0.0,
+ "title": "Radius",
+ "type": "number"
+ },
+ "type": {
+ "const": "Circle",
+ "default": "Circle",
+ "enum": [
+ "Circle"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_middle",
+ "y_middle",
+ "radius"
+ ],
+ "title": "Circle",
+ "type": "object"
+ },
+ "CombinationOf": {
+ "additionalProperties": false,
+ "description": "Abstract baseclass for a combination of two regions, left and right.",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "CombinationOf",
+ "default": "CombinationOf",
+ "enum": [
+ "CombinationOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "CombinationOf",
+ "type": "object"
+ },
+ "Concat": {
+ "additionalProperties": false,
+ "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))",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Spec",
+ "description": "The left-hand Spec to Concat, midpoints will appear earlier"
+ },
+ "right": {
+ "$ref": "#/$defs/Spec",
+ "description": "The right-hand Spec to Concat, midpoints will appear later"
+ },
+ "gap": {
+ "default": false,
+ "description": "If True, force a gap in the output at the join",
+ "title": "Gap",
+ "type": "boolean"
+ },
+ "check_path_changes": {
+ "default": true,
+ "description": "If True path through scan will not be modified by squash",
+ "title": "Check Path Changes",
+ "type": "boolean"
+ },
+ "type": {
+ "const": "Concat",
+ "default": "Concat",
+ "enum": [
+ "Concat"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "Concat",
+ "type": "object"
+ },
+ "DifferenceOf": {
+ "additionalProperties": false,
+ "description": "A point is in DifferenceOf(a, b) if in a and not in b.\n\nTypically created with the ``-`` operator.\n\n>>> r = Range(\"x\", 0.5, 2.5) - Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, False, False, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "DifferenceOf",
+ "default": "DifferenceOf",
+ "enum": [
+ "DifferenceOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "DifferenceOf",
+ "type": "object"
+ },
+ "Ellipse": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within an xy ellipse of given radius.\n\n.. example_spec::\n\n from scanspec.regions import Ellipse\n from scanspec.specs import Line\n\n grid = Line(\"y\", 3, 8, 10) * ~Line(\"x\", 1 ,8, 10)\n spec = grid & Ellipse(\"x\", \"y\", 5, 5, 2, 3, 75)",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_middle": {
+ "description": "The central x point of the ellipse",
+ "title": "X Middle",
+ "type": "number"
+ },
+ "y_middle": {
+ "description": "The central y point of the ellipse",
+ "title": "Y Middle",
+ "type": "number"
+ },
+ "x_radius": {
+ "description": "The radius along the x axis of the ellipse",
+ "exclusiveMinimum": 0.0,
+ "title": "X Radius",
+ "type": "number"
+ },
+ "y_radius": {
+ "description": "The radius along the y axis of the ellipse",
+ "exclusiveMinimum": 0.0,
+ "title": "Y Radius",
+ "type": "number"
+ },
+ "angle": {
+ "default": 0.0,
+ "description": "The angle of the ellipse (degrees)",
+ "title": "Angle",
+ "type": "number"
+ },
+ "type": {
+ "const": "Ellipse",
+ "default": "Ellipse",
+ "enum": [
+ "Ellipse"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_middle",
+ "y_middle",
+ "x_radius",
+ "y_radius"
+ ],
+ "title": "Ellipse",
+ "type": "object"
+ },
+ "IntersectionOf": {
+ "additionalProperties": false,
+ "description": "A point is in IntersectionOf(a, b) if in both a and b.\n\nTypically created with the ``&`` operator.\n\n>>> r = Range(\"x\", 0.5, 2.5) & Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, False, True, False, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "IntersectionOf",
+ "default": "IntersectionOf",
+ "enum": [
+ "IntersectionOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "IntersectionOf",
+ "type": "object"
+ },
+ "Line": {
+ "additionalProperties": false,
+ "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)",
+ "properties": {
+ "axis": {
+ "description": "An identifier for what to move",
+ "title": "Axis"
+ },
+ "start": {
+ "description": "Midpoint of the first point of the line",
+ "title": "Start",
+ "type": "number"
+ },
+ "stop": {
+ "description": "Midpoint of the last point of the line",
+ "title": "Stop",
+ "type": "number"
+ },
+ "num": {
+ "description": "Number of frames to produce",
+ "minimum": 1,
+ "title": "Num",
+ "type": "integer"
+ },
+ "type": {
+ "const": "Line",
+ "default": "Line",
+ "enum": [
+ "Line"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "axis",
+ "start",
+ "stop",
+ "num"
+ ],
+ "title": "Line",
+ "type": "object"
+ },
+ "Mask": {
+ "additionalProperties": false,
+ "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 Frames 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`",
+ "properties": {
+ "spec": {
+ "$ref": "#/$defs/Spec",
+ "description": "The Spec containing the source midpoints"
+ },
+ "region": {
+ "$ref": "#/$defs/Region",
+ "description": "The Region that midpoints will be inside"
+ },
+ "check_path_changes": {
+ "default": true,
+ "description": "If True path through scan will not be modified by squash",
+ "title": "Check Path Changes",
+ "type": "boolean"
+ },
+ "type": {
+ "const": "Mask",
+ "default": "Mask",
+ "enum": [
+ "Mask"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "spec",
+ "region"
+ ],
+ "title": "Mask",
+ "type": "object"
+ },
+ "Polygon": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within a rotated xy polygon.\n\n.. example_spec::\n\n from scanspec.regions import Polygon\n from scanspec.specs import Line\n\n grid = Line(\"y\", 3, 8, 10) * ~Line(\"x\", 1 ,8, 10)\n spec = grid & Polygon(\"x\", \"y\", [1.0, 6.0, 8.0, 2.0], [4.0, 10.0, 6.0, 1.0])",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_verts": {
+ "description": "The Nx1 x coordinates of the polygons vertices",
+ "items": {
+ "type": "number"
+ },
+ "minItems": 3,
+ "title": "X Verts",
+ "type": "array"
+ },
+ "y_verts": {
+ "description": "The Nx1 y coordinates of the polygons vertices",
+ "items": {
+ "type": "number"
+ },
+ "minItems": 3,
+ "title": "Y Verts",
+ "type": "array"
+ },
+ "type": {
+ "const": "Polygon",
+ "default": "Polygon",
+ "enum": [
+ "Polygon"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_verts",
+ "y_verts"
+ ],
+ "title": "Polygon",
+ "type": "object"
+ },
+ "Product": {
+ "additionalProperties": false,
+ "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)",
+ "properties": {
+ "outer": {
+ "$ref": "#/$defs/Spec",
+ "description": "Will be executed once"
+ },
+ "inner": {
+ "$ref": "#/$defs/Spec",
+ "description": "Will be executed len(outer) times"
+ },
+ "type": {
+ "const": "Product",
+ "default": "Product",
+ "enum": [
+ "Product"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "outer",
+ "inner"
+ ],
+ "title": "Product",
+ "type": "object"
+ },
+ "Range": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis >= min and <= max.\n\n>>> r = Range(\"x\", 1, 2)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, True, False, False])",
+ "properties": {
+ "axis": {
+ "description": "The name matching the axis to mask in spec",
+ "title": "Axis"
+ },
+ "min": {
+ "description": "The minimum inclusive value in the region",
+ "title": "Min",
+ "type": "number"
+ },
+ "max": {
+ "description": "The minimum inclusive value in the region",
+ "title": "Max",
+ "type": "number"
+ },
+ "type": {
+ "const": "Range",
+ "default": "Range",
+ "enum": [
+ "Range"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "axis",
+ "min",
+ "max"
+ ],
+ "title": "Range",
+ "type": "object"
+ },
+ "Rectangle": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within a rotated xy rectangle.\n\n.. example_spec::\n\n from scanspec.regions import Rectangle\n from scanspec.specs import Line\n\n grid = Line(\"y\", 1, 3, 10) * ~Line(\"x\", 0, 2, 10)\n spec = grid & Rectangle(\"x\", \"y\", 0, 1.1, 1.5, 2.1, 30)",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_min": {
+ "description": "Minimum inclusive x value in the region",
+ "title": "X Min",
+ "type": "number"
+ },
+ "y_min": {
+ "description": "Minimum inclusive y value in the region",
+ "title": "Y Min",
+ "type": "number"
+ },
+ "x_max": {
+ "description": "Maximum inclusive x value in the region",
+ "title": "X Max",
+ "type": "number"
+ },
+ "y_max": {
+ "description": "Maximum inclusive y value in the region",
+ "title": "Y Max",
+ "type": "number"
+ },
+ "angle": {
+ "default": 0.0,
+ "description": "Clockwise rotation angle of the rectangle",
+ "title": "Angle",
+ "type": "number"
+ },
+ "type": {
+ "const": "Rectangle",
+ "default": "Rectangle",
+ "enum": [
+ "Rectangle"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_min",
+ "y_min",
+ "x_max",
+ "y_max"
+ ],
+ "title": "Rectangle",
+ "type": "object"
+ },
+ "Region": {
+ "discriminator": {
+ "mapping": {
+ "Circle": "#/$defs/Circle",
+ "CombinationOf": "#/$defs/CombinationOf",
+ "DifferenceOf": "#/$defs/DifferenceOf",
+ "Ellipse": "#/$defs/Ellipse",
+ "IntersectionOf": "#/$defs/IntersectionOf",
+ "Polygon": "#/$defs/Polygon",
+ "Range": "#/$defs/Range",
+ "Rectangle": "#/$defs/Rectangle",
+ "SymmetricDifferenceOf": "#/$defs/SymmetricDifferenceOf",
+ "UnionOf": "#/$defs/UnionOf"
+ },
+ "propertyName": "type"
+ },
+ "oneOf": [
+ {
+ "$ref": "#/$defs/CombinationOf"
+ },
+ {
+ "$ref": "#/$defs/UnionOf"
+ },
+ {
+ "$ref": "#/$defs/IntersectionOf"
+ },
+ {
+ "$ref": "#/$defs/DifferenceOf"
+ },
+ {
+ "$ref": "#/$defs/SymmetricDifferenceOf"
+ },
+ {
+ "$ref": "#/$defs/Range"
+ },
+ {
+ "$ref": "#/$defs/Rectangle"
+ },
+ {
+ "$ref": "#/$defs/Polygon"
+ },
+ {
+ "$ref": "#/$defs/Circle"
+ },
+ {
+ "$ref": "#/$defs/Ellipse"
+ }
+ ]
+ },
+ "Repeat": {
+ "additionalProperties": false,
+ "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",
+ "properties": {
+ "num": {
+ "description": "Number of frames to produce",
+ "minimum": 1,
+ "title": "Num",
+ "type": "integer"
+ },
+ "gap": {
+ "default": true,
+ "description": "If False and the slowest of the stack of Frames is snaked then the end and start of consecutive iterations of Spec will have no gap",
+ "title": "Gap",
+ "type": "boolean"
+ },
+ "type": {
+ "const": "Repeat",
+ "default": "Repeat",
+ "enum": [
+ "Repeat"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "num"
+ ],
+ "title": "Repeat",
+ "type": "object"
+ },
+ "Snake": {
+ "additionalProperties": false,
+ "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)",
+ "properties": {
+ "spec": {
+ "$ref": "#/$defs/Spec",
+ "description": "The Spec to run in reverse every other iteration"
+ },
+ "type": {
+ "const": "Snake",
+ "default": "Snake",
+ "enum": [
+ "Snake"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "spec"
+ ],
+ "title": "Snake",
+ "type": "object"
+ },
+ "Spec": {
+ "discriminator": {
+ "mapping": {
+ "Concat": "#/$defs/Concat",
+ "Line": "#/$defs/Line",
+ "Mask": "#/$defs/Mask",
+ "Product": "#/$defs/Product",
+ "Repeat": "#/$defs/Repeat",
+ "Snake": "#/$defs/Snake",
+ "Spiral": "#/$defs/Spiral",
+ "Squash": "#/$defs/Squash",
+ "Static": "#/$defs/Static",
+ "Zip": "#/$defs/Zip"
+ },
+ "propertyName": "type"
+ },
+ "oneOf": [
+ {
+ "$ref": "#/$defs/Product"
+ },
+ {
+ "$ref": "#/$defs/Repeat"
+ },
+ {
+ "$ref": "#/$defs/Zip"
+ },
+ {
+ "$ref": "#/$defs/Mask"
+ },
+ {
+ "$ref": "#/$defs/Snake"
+ },
+ {
+ "$ref": "#/$defs/Concat"
+ },
+ {
+ "$ref": "#/$defs/Squash"
+ },
+ {
+ "$ref": "#/$defs/Line"
+ },
+ {
+ "$ref": "#/$defs/Static"
+ },
+ {
+ "$ref": "#/$defs/Spiral"
+ }
+ ]
+ },
+ "Spiral": {
+ "additionalProperties": false,
+ "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)",
+ "properties": {
+ "x_axis": {
+ "description": "An identifier for what to move for x",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "An identifier for what to move for y",
+ "title": "Y Axis"
+ },
+ "x_start": {
+ "description": "x centre of the spiral",
+ "title": "X Start",
+ "type": "number"
+ },
+ "y_start": {
+ "description": "y centre of the spiral",
+ "title": "Y Start",
+ "type": "number"
+ },
+ "x_range": {
+ "description": "x width of the spiral",
+ "title": "X Range",
+ "type": "number"
+ },
+ "y_range": {
+ "description": "y width of the spiral",
+ "title": "Y Range",
+ "type": "number"
+ },
+ "num": {
+ "description": "Number of frames to produce",
+ "minimum": 1,
+ "title": "Num",
+ "type": "integer"
+ },
+ "rotate": {
+ "default": 0.0,
+ "description": "How much to rotate the angle of the spiral",
+ "title": "Rotate",
+ "type": "number"
+ },
+ "type": {
+ "const": "Spiral",
+ "default": "Spiral",
+ "enum": [
+ "Spiral"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_start",
+ "y_start",
+ "x_range",
+ "y_range",
+ "num"
+ ],
+ "title": "Spiral",
+ "type": "object"
+ },
+ "Squash": {
+ "additionalProperties": false,
+ "description": "Squash a stack of Frames together into a single expanded Frames 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))",
+ "properties": {
+ "spec": {
+ "$ref": "#/$defs/Spec",
+ "description": "The Spec to squash the dimensions of"
+ },
+ "check_path_changes": {
+ "default": true,
+ "description": "If True path through scan will not be modified by squash",
+ "title": "Check Path Changes",
+ "type": "boolean"
+ },
+ "type": {
+ "const": "Squash",
+ "default": "Squash",
+ "enum": [
+ "Squash"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "spec"
+ ],
+ "title": "Squash",
+ "type": "object"
+ },
+ "Static": {
+ "additionalProperties": false,
+ "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))",
+ "properties": {
+ "axis": {
+ "description": "An identifier for what to move",
+ "title": "Axis"
+ },
+ "value": {
+ "description": "The value at each point",
+ "title": "Value",
+ "type": "number"
+ },
+ "num": {
+ "default": 1,
+ "description": "Number of frames to produce",
+ "minimum": 1,
+ "title": "Num",
+ "type": "integer"
+ },
+ "type": {
+ "const": "Static",
+ "default": "Static",
+ "enum": [
+ "Static"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "axis",
+ "value"
+ ],
+ "title": "Static",
+ "type": "object"
+ },
+ "SymmetricDifferenceOf": {
+ "additionalProperties": false,
+ "description": "A point is in SymmetricDifferenceOf(a, b) if in either a or b, but not both.\n\nTypically created with the ``^`` operator.\n\n>>> r = Range(\"x\", 0.5, 2.5) ^ Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, False, True, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "SymmetricDifferenceOf",
+ "default": "SymmetricDifferenceOf",
+ "enum": [
+ "SymmetricDifferenceOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "SymmetricDifferenceOf",
+ "type": "object"
+ },
+ "UnionOf": {
+ "additionalProperties": false,
+ "description": "A point is in UnionOf(a, b) if in either a or b.\n\nTypically created with the ``|`` operator\n\n>>> r = Range(\"x\", 0.5, 2.5) | Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, True, True, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "UnionOf",
+ "default": "UnionOf",
+ "enum": [
+ "UnionOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "UnionOf",
+ "type": "object"
+ },
+ "Zip": {
+ "additionalProperties": false,
+ "description": "Run two Specs in parallel, merging their midpoints together.\n\nTypically formed using `Spec.zip`.\n\nStacks of Frames are merged by:\n\n- If right creates a stack of a single Frames object of size 1, expand it to\n the size of the fastest Frames object created by left\n- Merge individual Frames 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))",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Spec",
+ "description": "The left-hand Spec to Zip, will appear earlier in axes"
+ },
+ "right": {
+ "$ref": "#/$defs/Spec",
+ "description": "The right-hand Spec to Zip, will appear later in axes"
+ },
+ "type": {
+ "const": "Zip",
+ "default": "Zip",
+ "enum": [
+ "Zip"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "Zip",
+ "type": "object"
+ }
+ },
+ "$ref": "#/$defs/Product"
+}
+
Repeat an empty frame num times.
+Can be used on the outside of a scan to repeat the same scan many times.
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.specs import Line
+
+spec = 2 * ~Line.bounded("x", 3, 4, 1)
+plot_spec(spec)
+
(Source code
, png
, hires.png
, pdf
)
If you want snaked axes to have no gap between iterations you can do:
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.specs import Line, Repeat
+
+spec = Repeat(2, gap=False) * ~Line.bounded("x", 3, 4, 1)
+plot_spec(spec)
+
(Source code
, png
, hires.png
, pdf
)
Note
+There is no turnaround arrow at x=4
+Show JSON schema
{
+ "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",
+ "type": "object",
+ "properties": {
+ "num": {
+ "description": "Number of frames to produce",
+ "minimum": 1,
+ "title": "Num",
+ "type": "integer"
+ },
+ "gap": {
+ "default": true,
+ "description": "If False and the slowest of the stack of Frames is snaked then the end and start of consecutive iterations of Spec will have no gap",
+ "title": "Gap",
+ "type": "boolean"
+ },
+ "type": {
+ "const": "Repeat",
+ "default": "Repeat",
+ "enum": [
+ "Repeat"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "num"
+ ]
+}
+
If False and the slowest of the stack of Frames is snaked then the end and start of consecutive iterations of Spec will have no gap
+Run two Specs in parallel, merging their midpoints together.
+Typically formed using Spec.zip
.
Stacks of Frames are merged by:
+If right creates a stack of a single Frames object of size 1, expand it to +the size of the fastest Frames object created by left
Merge individual Frames objects together from fastest to slowest
This means that Zipping a Spec producing stack [l2, l1] with a Spec +producing stack [r1] will assert len(l1)==len(r1), and produce +stack [l2, l1.zip(r1)].
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.specs import Line
+
+spec = Line("z", 1, 2, 3) * Line("y", 3, 4, 5).zip(Line("x", 4, 5, 5))
+plot_spec(spec)
+
(Source code
, png
, hires.png
, pdf
)
Show JSON schema
{
+ "$defs": {
+ "Circle": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within an xy circle of given radius.\n\n.. example_spec::\n\n from scanspec.regions import Circle\n from scanspec.specs import Line\n\n grid = Line(\"y\", 1, 3, 10) * ~Line(\"x\", 0, 2, 10)\n spec = grid & Circle(\"x\", \"y\", 1, 2, 0.9)",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_middle": {
+ "description": "The central x point of the circle",
+ "title": "X Middle",
+ "type": "number"
+ },
+ "y_middle": {
+ "description": "The central y point of the circle",
+ "title": "Y Middle",
+ "type": "number"
+ },
+ "radius": {
+ "description": "Radius of the circle",
+ "exclusiveMinimum": 0.0,
+ "title": "Radius",
+ "type": "number"
+ },
+ "type": {
+ "const": "Circle",
+ "default": "Circle",
+ "enum": [
+ "Circle"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_middle",
+ "y_middle",
+ "radius"
+ ],
+ "title": "Circle",
+ "type": "object"
+ },
+ "CombinationOf": {
+ "additionalProperties": false,
+ "description": "Abstract baseclass for a combination of two regions, left and right.",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "CombinationOf",
+ "default": "CombinationOf",
+ "enum": [
+ "CombinationOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "CombinationOf",
+ "type": "object"
+ },
+ "Concat": {
+ "additionalProperties": false,
+ "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))",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Spec",
+ "description": "The left-hand Spec to Concat, midpoints will appear earlier"
+ },
+ "right": {
+ "$ref": "#/$defs/Spec",
+ "description": "The right-hand Spec to Concat, midpoints will appear later"
+ },
+ "gap": {
+ "default": false,
+ "description": "If True, force a gap in the output at the join",
+ "title": "Gap",
+ "type": "boolean"
+ },
+ "check_path_changes": {
+ "default": true,
+ "description": "If True path through scan will not be modified by squash",
+ "title": "Check Path Changes",
+ "type": "boolean"
+ },
+ "type": {
+ "const": "Concat",
+ "default": "Concat",
+ "enum": [
+ "Concat"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "Concat",
+ "type": "object"
+ },
+ "DifferenceOf": {
+ "additionalProperties": false,
+ "description": "A point is in DifferenceOf(a, b) if in a and not in b.\n\nTypically created with the ``-`` operator.\n\n>>> r = Range(\"x\", 0.5, 2.5) - Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, False, False, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "DifferenceOf",
+ "default": "DifferenceOf",
+ "enum": [
+ "DifferenceOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "DifferenceOf",
+ "type": "object"
+ },
+ "Ellipse": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within an xy ellipse of given radius.\n\n.. example_spec::\n\n from scanspec.regions import Ellipse\n from scanspec.specs import Line\n\n grid = Line(\"y\", 3, 8, 10) * ~Line(\"x\", 1 ,8, 10)\n spec = grid & Ellipse(\"x\", \"y\", 5, 5, 2, 3, 75)",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_middle": {
+ "description": "The central x point of the ellipse",
+ "title": "X Middle",
+ "type": "number"
+ },
+ "y_middle": {
+ "description": "The central y point of the ellipse",
+ "title": "Y Middle",
+ "type": "number"
+ },
+ "x_radius": {
+ "description": "The radius along the x axis of the ellipse",
+ "exclusiveMinimum": 0.0,
+ "title": "X Radius",
+ "type": "number"
+ },
+ "y_radius": {
+ "description": "The radius along the y axis of the ellipse",
+ "exclusiveMinimum": 0.0,
+ "title": "Y Radius",
+ "type": "number"
+ },
+ "angle": {
+ "default": 0.0,
+ "description": "The angle of the ellipse (degrees)",
+ "title": "Angle",
+ "type": "number"
+ },
+ "type": {
+ "const": "Ellipse",
+ "default": "Ellipse",
+ "enum": [
+ "Ellipse"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_middle",
+ "y_middle",
+ "x_radius",
+ "y_radius"
+ ],
+ "title": "Ellipse",
+ "type": "object"
+ },
+ "IntersectionOf": {
+ "additionalProperties": false,
+ "description": "A point is in IntersectionOf(a, b) if in both a and b.\n\nTypically created with the ``&`` operator.\n\n>>> r = Range(\"x\", 0.5, 2.5) & Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, False, True, False, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "IntersectionOf",
+ "default": "IntersectionOf",
+ "enum": [
+ "IntersectionOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "IntersectionOf",
+ "type": "object"
+ },
+ "Line": {
+ "additionalProperties": false,
+ "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)",
+ "properties": {
+ "axis": {
+ "description": "An identifier for what to move",
+ "title": "Axis"
+ },
+ "start": {
+ "description": "Midpoint of the first point of the line",
+ "title": "Start",
+ "type": "number"
+ },
+ "stop": {
+ "description": "Midpoint of the last point of the line",
+ "title": "Stop",
+ "type": "number"
+ },
+ "num": {
+ "description": "Number of frames to produce",
+ "minimum": 1,
+ "title": "Num",
+ "type": "integer"
+ },
+ "type": {
+ "const": "Line",
+ "default": "Line",
+ "enum": [
+ "Line"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "axis",
+ "start",
+ "stop",
+ "num"
+ ],
+ "title": "Line",
+ "type": "object"
+ },
+ "Mask": {
+ "additionalProperties": false,
+ "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 Frames 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`",
+ "properties": {
+ "spec": {
+ "$ref": "#/$defs/Spec",
+ "description": "The Spec containing the source midpoints"
+ },
+ "region": {
+ "$ref": "#/$defs/Region",
+ "description": "The Region that midpoints will be inside"
+ },
+ "check_path_changes": {
+ "default": true,
+ "description": "If True path through scan will not be modified by squash",
+ "title": "Check Path Changes",
+ "type": "boolean"
+ },
+ "type": {
+ "const": "Mask",
+ "default": "Mask",
+ "enum": [
+ "Mask"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "spec",
+ "region"
+ ],
+ "title": "Mask",
+ "type": "object"
+ },
+ "Polygon": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within a rotated xy polygon.\n\n.. example_spec::\n\n from scanspec.regions import Polygon\n from scanspec.specs import Line\n\n grid = Line(\"y\", 3, 8, 10) * ~Line(\"x\", 1 ,8, 10)\n spec = grid & Polygon(\"x\", \"y\", [1.0, 6.0, 8.0, 2.0], [4.0, 10.0, 6.0, 1.0])",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_verts": {
+ "description": "The Nx1 x coordinates of the polygons vertices",
+ "items": {
+ "type": "number"
+ },
+ "minItems": 3,
+ "title": "X Verts",
+ "type": "array"
+ },
+ "y_verts": {
+ "description": "The Nx1 y coordinates of the polygons vertices",
+ "items": {
+ "type": "number"
+ },
+ "minItems": 3,
+ "title": "Y Verts",
+ "type": "array"
+ },
+ "type": {
+ "const": "Polygon",
+ "default": "Polygon",
+ "enum": [
+ "Polygon"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_verts",
+ "y_verts"
+ ],
+ "title": "Polygon",
+ "type": "object"
+ },
+ "Product": {
+ "additionalProperties": false,
+ "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)",
+ "properties": {
+ "outer": {
+ "$ref": "#/$defs/Spec",
+ "description": "Will be executed once"
+ },
+ "inner": {
+ "$ref": "#/$defs/Spec",
+ "description": "Will be executed len(outer) times"
+ },
+ "type": {
+ "const": "Product",
+ "default": "Product",
+ "enum": [
+ "Product"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "outer",
+ "inner"
+ ],
+ "title": "Product",
+ "type": "object"
+ },
+ "Range": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis >= min and <= max.\n\n>>> r = Range(\"x\", 1, 2)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, True, False, False])",
+ "properties": {
+ "axis": {
+ "description": "The name matching the axis to mask in spec",
+ "title": "Axis"
+ },
+ "min": {
+ "description": "The minimum inclusive value in the region",
+ "title": "Min",
+ "type": "number"
+ },
+ "max": {
+ "description": "The minimum inclusive value in the region",
+ "title": "Max",
+ "type": "number"
+ },
+ "type": {
+ "const": "Range",
+ "default": "Range",
+ "enum": [
+ "Range"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "axis",
+ "min",
+ "max"
+ ],
+ "title": "Range",
+ "type": "object"
+ },
+ "Rectangle": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within a rotated xy rectangle.\n\n.. example_spec::\n\n from scanspec.regions import Rectangle\n from scanspec.specs import Line\n\n grid = Line(\"y\", 1, 3, 10) * ~Line(\"x\", 0, 2, 10)\n spec = grid & Rectangle(\"x\", \"y\", 0, 1.1, 1.5, 2.1, 30)",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_min": {
+ "description": "Minimum inclusive x value in the region",
+ "title": "X Min",
+ "type": "number"
+ },
+ "y_min": {
+ "description": "Minimum inclusive y value in the region",
+ "title": "Y Min",
+ "type": "number"
+ },
+ "x_max": {
+ "description": "Maximum inclusive x value in the region",
+ "title": "X Max",
+ "type": "number"
+ },
+ "y_max": {
+ "description": "Maximum inclusive y value in the region",
+ "title": "Y Max",
+ "type": "number"
+ },
+ "angle": {
+ "default": 0.0,
+ "description": "Clockwise rotation angle of the rectangle",
+ "title": "Angle",
+ "type": "number"
+ },
+ "type": {
+ "const": "Rectangle",
+ "default": "Rectangle",
+ "enum": [
+ "Rectangle"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_min",
+ "y_min",
+ "x_max",
+ "y_max"
+ ],
+ "title": "Rectangle",
+ "type": "object"
+ },
+ "Region": {
+ "discriminator": {
+ "mapping": {
+ "Circle": "#/$defs/Circle",
+ "CombinationOf": "#/$defs/CombinationOf",
+ "DifferenceOf": "#/$defs/DifferenceOf",
+ "Ellipse": "#/$defs/Ellipse",
+ "IntersectionOf": "#/$defs/IntersectionOf",
+ "Polygon": "#/$defs/Polygon",
+ "Range": "#/$defs/Range",
+ "Rectangle": "#/$defs/Rectangle",
+ "SymmetricDifferenceOf": "#/$defs/SymmetricDifferenceOf",
+ "UnionOf": "#/$defs/UnionOf"
+ },
+ "propertyName": "type"
+ },
+ "oneOf": [
+ {
+ "$ref": "#/$defs/CombinationOf"
+ },
+ {
+ "$ref": "#/$defs/UnionOf"
+ },
+ {
+ "$ref": "#/$defs/IntersectionOf"
+ },
+ {
+ "$ref": "#/$defs/DifferenceOf"
+ },
+ {
+ "$ref": "#/$defs/SymmetricDifferenceOf"
+ },
+ {
+ "$ref": "#/$defs/Range"
+ },
+ {
+ "$ref": "#/$defs/Rectangle"
+ },
+ {
+ "$ref": "#/$defs/Polygon"
+ },
+ {
+ "$ref": "#/$defs/Circle"
+ },
+ {
+ "$ref": "#/$defs/Ellipse"
+ }
+ ]
+ },
+ "Repeat": {
+ "additionalProperties": false,
+ "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",
+ "properties": {
+ "num": {
+ "description": "Number of frames to produce",
+ "minimum": 1,
+ "title": "Num",
+ "type": "integer"
+ },
+ "gap": {
+ "default": true,
+ "description": "If False and the slowest of the stack of Frames is snaked then the end and start of consecutive iterations of Spec will have no gap",
+ "title": "Gap",
+ "type": "boolean"
+ },
+ "type": {
+ "const": "Repeat",
+ "default": "Repeat",
+ "enum": [
+ "Repeat"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "num"
+ ],
+ "title": "Repeat",
+ "type": "object"
+ },
+ "Snake": {
+ "additionalProperties": false,
+ "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)",
+ "properties": {
+ "spec": {
+ "$ref": "#/$defs/Spec",
+ "description": "The Spec to run in reverse every other iteration"
+ },
+ "type": {
+ "const": "Snake",
+ "default": "Snake",
+ "enum": [
+ "Snake"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "spec"
+ ],
+ "title": "Snake",
+ "type": "object"
+ },
+ "Spec": {
+ "discriminator": {
+ "mapping": {
+ "Concat": "#/$defs/Concat",
+ "Line": "#/$defs/Line",
+ "Mask": "#/$defs/Mask",
+ "Product": "#/$defs/Product",
+ "Repeat": "#/$defs/Repeat",
+ "Snake": "#/$defs/Snake",
+ "Spiral": "#/$defs/Spiral",
+ "Squash": "#/$defs/Squash",
+ "Static": "#/$defs/Static",
+ "Zip": "#/$defs/Zip"
+ },
+ "propertyName": "type"
+ },
+ "oneOf": [
+ {
+ "$ref": "#/$defs/Product"
+ },
+ {
+ "$ref": "#/$defs/Repeat"
+ },
+ {
+ "$ref": "#/$defs/Zip"
+ },
+ {
+ "$ref": "#/$defs/Mask"
+ },
+ {
+ "$ref": "#/$defs/Snake"
+ },
+ {
+ "$ref": "#/$defs/Concat"
+ },
+ {
+ "$ref": "#/$defs/Squash"
+ },
+ {
+ "$ref": "#/$defs/Line"
+ },
+ {
+ "$ref": "#/$defs/Static"
+ },
+ {
+ "$ref": "#/$defs/Spiral"
+ }
+ ]
+ },
+ "Spiral": {
+ "additionalProperties": false,
+ "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)",
+ "properties": {
+ "x_axis": {
+ "description": "An identifier for what to move for x",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "An identifier for what to move for y",
+ "title": "Y Axis"
+ },
+ "x_start": {
+ "description": "x centre of the spiral",
+ "title": "X Start",
+ "type": "number"
+ },
+ "y_start": {
+ "description": "y centre of the spiral",
+ "title": "Y Start",
+ "type": "number"
+ },
+ "x_range": {
+ "description": "x width of the spiral",
+ "title": "X Range",
+ "type": "number"
+ },
+ "y_range": {
+ "description": "y width of the spiral",
+ "title": "Y Range",
+ "type": "number"
+ },
+ "num": {
+ "description": "Number of frames to produce",
+ "minimum": 1,
+ "title": "Num",
+ "type": "integer"
+ },
+ "rotate": {
+ "default": 0.0,
+ "description": "How much to rotate the angle of the spiral",
+ "title": "Rotate",
+ "type": "number"
+ },
+ "type": {
+ "const": "Spiral",
+ "default": "Spiral",
+ "enum": [
+ "Spiral"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_start",
+ "y_start",
+ "x_range",
+ "y_range",
+ "num"
+ ],
+ "title": "Spiral",
+ "type": "object"
+ },
+ "Squash": {
+ "additionalProperties": false,
+ "description": "Squash a stack of Frames together into a single expanded Frames 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))",
+ "properties": {
+ "spec": {
+ "$ref": "#/$defs/Spec",
+ "description": "The Spec to squash the dimensions of"
+ },
+ "check_path_changes": {
+ "default": true,
+ "description": "If True path through scan will not be modified by squash",
+ "title": "Check Path Changes",
+ "type": "boolean"
+ },
+ "type": {
+ "const": "Squash",
+ "default": "Squash",
+ "enum": [
+ "Squash"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "spec"
+ ],
+ "title": "Squash",
+ "type": "object"
+ },
+ "Static": {
+ "additionalProperties": false,
+ "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))",
+ "properties": {
+ "axis": {
+ "description": "An identifier for what to move",
+ "title": "Axis"
+ },
+ "value": {
+ "description": "The value at each point",
+ "title": "Value",
+ "type": "number"
+ },
+ "num": {
+ "default": 1,
+ "description": "Number of frames to produce",
+ "minimum": 1,
+ "title": "Num",
+ "type": "integer"
+ },
+ "type": {
+ "const": "Static",
+ "default": "Static",
+ "enum": [
+ "Static"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "axis",
+ "value"
+ ],
+ "title": "Static",
+ "type": "object"
+ },
+ "SymmetricDifferenceOf": {
+ "additionalProperties": false,
+ "description": "A point is in SymmetricDifferenceOf(a, b) if in either a or b, but not both.\n\nTypically created with the ``^`` operator.\n\n>>> r = Range(\"x\", 0.5, 2.5) ^ Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, False, True, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "SymmetricDifferenceOf",
+ "default": "SymmetricDifferenceOf",
+ "enum": [
+ "SymmetricDifferenceOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "SymmetricDifferenceOf",
+ "type": "object"
+ },
+ "UnionOf": {
+ "additionalProperties": false,
+ "description": "A point is in UnionOf(a, b) if in either a or b.\n\nTypically created with the ``|`` operator\n\n>>> r = Range(\"x\", 0.5, 2.5) | Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, True, True, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "UnionOf",
+ "default": "UnionOf",
+ "enum": [
+ "UnionOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "UnionOf",
+ "type": "object"
+ },
+ "Zip": {
+ "additionalProperties": false,
+ "description": "Run two Specs in parallel, merging their midpoints together.\n\nTypically formed using `Spec.zip`.\n\nStacks of Frames are merged by:\n\n- If right creates a stack of a single Frames object of size 1, expand it to\n the size of the fastest Frames object created by left\n- Merge individual Frames 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))",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Spec",
+ "description": "The left-hand Spec to Zip, will appear earlier in axes"
+ },
+ "right": {
+ "$ref": "#/$defs/Spec",
+ "description": "The right-hand Spec to Zip, will appear later in axes"
+ },
+ "type": {
+ "const": "Zip",
+ "default": "Zip",
+ "enum": [
+ "Zip"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "Zip",
+ "type": "object"
+ }
+ },
+ "$ref": "#/$defs/Zip"
+}
+
Restrict Spec to only midpoints that fall inside the given Region.
+Typically created with the &
operator. It also pushes down the
+& | ^ -
operators to its Region
to avoid the need for brackets on
+combinations of Regions.
If a Region spans multiple Frames objects, they will be squashed together.
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.regions import Circle
+from scanspec.specs import Line
+
+spec = Line("y", 1, 3, 3) * Line("x", 3, 5, 5) & Circle("x", "y", 4, 2, 1.2)
+plot_spec(spec)
+
(Source code
, png
, hires.png
, pdf
)
See Also: Why Squash (and Mask) can change the Path
+Show JSON schema
{
+ "$defs": {
+ "Circle": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within an xy circle of given radius.\n\n.. example_spec::\n\n from scanspec.regions import Circle\n from scanspec.specs import Line\n\n grid = Line(\"y\", 1, 3, 10) * ~Line(\"x\", 0, 2, 10)\n spec = grid & Circle(\"x\", \"y\", 1, 2, 0.9)",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_middle": {
+ "description": "The central x point of the circle",
+ "title": "X Middle",
+ "type": "number"
+ },
+ "y_middle": {
+ "description": "The central y point of the circle",
+ "title": "Y Middle",
+ "type": "number"
+ },
+ "radius": {
+ "description": "Radius of the circle",
+ "exclusiveMinimum": 0.0,
+ "title": "Radius",
+ "type": "number"
+ },
+ "type": {
+ "const": "Circle",
+ "default": "Circle",
+ "enum": [
+ "Circle"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_middle",
+ "y_middle",
+ "radius"
+ ],
+ "title": "Circle",
+ "type": "object"
+ },
+ "CombinationOf": {
+ "additionalProperties": false,
+ "description": "Abstract baseclass for a combination of two regions, left and right.",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "CombinationOf",
+ "default": "CombinationOf",
+ "enum": [
+ "CombinationOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "CombinationOf",
+ "type": "object"
+ },
+ "Concat": {
+ "additionalProperties": false,
+ "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))",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Spec",
+ "description": "The left-hand Spec to Concat, midpoints will appear earlier"
+ },
+ "right": {
+ "$ref": "#/$defs/Spec",
+ "description": "The right-hand Spec to Concat, midpoints will appear later"
+ },
+ "gap": {
+ "default": false,
+ "description": "If True, force a gap in the output at the join",
+ "title": "Gap",
+ "type": "boolean"
+ },
+ "check_path_changes": {
+ "default": true,
+ "description": "If True path through scan will not be modified by squash",
+ "title": "Check Path Changes",
+ "type": "boolean"
+ },
+ "type": {
+ "const": "Concat",
+ "default": "Concat",
+ "enum": [
+ "Concat"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "Concat",
+ "type": "object"
+ },
+ "DifferenceOf": {
+ "additionalProperties": false,
+ "description": "A point is in DifferenceOf(a, b) if in a and not in b.\n\nTypically created with the ``-`` operator.\n\n>>> r = Range(\"x\", 0.5, 2.5) - Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, False, False, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "DifferenceOf",
+ "default": "DifferenceOf",
+ "enum": [
+ "DifferenceOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "DifferenceOf",
+ "type": "object"
+ },
+ "Ellipse": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within an xy ellipse of given radius.\n\n.. example_spec::\n\n from scanspec.regions import Ellipse\n from scanspec.specs import Line\n\n grid = Line(\"y\", 3, 8, 10) * ~Line(\"x\", 1 ,8, 10)\n spec = grid & Ellipse(\"x\", \"y\", 5, 5, 2, 3, 75)",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_middle": {
+ "description": "The central x point of the ellipse",
+ "title": "X Middle",
+ "type": "number"
+ },
+ "y_middle": {
+ "description": "The central y point of the ellipse",
+ "title": "Y Middle",
+ "type": "number"
+ },
+ "x_radius": {
+ "description": "The radius along the x axis of the ellipse",
+ "exclusiveMinimum": 0.0,
+ "title": "X Radius",
+ "type": "number"
+ },
+ "y_radius": {
+ "description": "The radius along the y axis of the ellipse",
+ "exclusiveMinimum": 0.0,
+ "title": "Y Radius",
+ "type": "number"
+ },
+ "angle": {
+ "default": 0.0,
+ "description": "The angle of the ellipse (degrees)",
+ "title": "Angle",
+ "type": "number"
+ },
+ "type": {
+ "const": "Ellipse",
+ "default": "Ellipse",
+ "enum": [
+ "Ellipse"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_middle",
+ "y_middle",
+ "x_radius",
+ "y_radius"
+ ],
+ "title": "Ellipse",
+ "type": "object"
+ },
+ "IntersectionOf": {
+ "additionalProperties": false,
+ "description": "A point is in IntersectionOf(a, b) if in both a and b.\n\nTypically created with the ``&`` operator.\n\n>>> r = Range(\"x\", 0.5, 2.5) & Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, False, True, False, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "IntersectionOf",
+ "default": "IntersectionOf",
+ "enum": [
+ "IntersectionOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "IntersectionOf",
+ "type": "object"
+ },
+ "Line": {
+ "additionalProperties": false,
+ "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)",
+ "properties": {
+ "axis": {
+ "description": "An identifier for what to move",
+ "title": "Axis"
+ },
+ "start": {
+ "description": "Midpoint of the first point of the line",
+ "title": "Start",
+ "type": "number"
+ },
+ "stop": {
+ "description": "Midpoint of the last point of the line",
+ "title": "Stop",
+ "type": "number"
+ },
+ "num": {
+ "description": "Number of frames to produce",
+ "minimum": 1,
+ "title": "Num",
+ "type": "integer"
+ },
+ "type": {
+ "const": "Line",
+ "default": "Line",
+ "enum": [
+ "Line"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "axis",
+ "start",
+ "stop",
+ "num"
+ ],
+ "title": "Line",
+ "type": "object"
+ },
+ "Mask": {
+ "additionalProperties": false,
+ "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 Frames 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`",
+ "properties": {
+ "spec": {
+ "$ref": "#/$defs/Spec",
+ "description": "The Spec containing the source midpoints"
+ },
+ "region": {
+ "$ref": "#/$defs/Region",
+ "description": "The Region that midpoints will be inside"
+ },
+ "check_path_changes": {
+ "default": true,
+ "description": "If True path through scan will not be modified by squash",
+ "title": "Check Path Changes",
+ "type": "boolean"
+ },
+ "type": {
+ "const": "Mask",
+ "default": "Mask",
+ "enum": [
+ "Mask"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "spec",
+ "region"
+ ],
+ "title": "Mask",
+ "type": "object"
+ },
+ "Polygon": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within a rotated xy polygon.\n\n.. example_spec::\n\n from scanspec.regions import Polygon\n from scanspec.specs import Line\n\n grid = Line(\"y\", 3, 8, 10) * ~Line(\"x\", 1 ,8, 10)\n spec = grid & Polygon(\"x\", \"y\", [1.0, 6.0, 8.0, 2.0], [4.0, 10.0, 6.0, 1.0])",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_verts": {
+ "description": "The Nx1 x coordinates of the polygons vertices",
+ "items": {
+ "type": "number"
+ },
+ "minItems": 3,
+ "title": "X Verts",
+ "type": "array"
+ },
+ "y_verts": {
+ "description": "The Nx1 y coordinates of the polygons vertices",
+ "items": {
+ "type": "number"
+ },
+ "minItems": 3,
+ "title": "Y Verts",
+ "type": "array"
+ },
+ "type": {
+ "const": "Polygon",
+ "default": "Polygon",
+ "enum": [
+ "Polygon"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_verts",
+ "y_verts"
+ ],
+ "title": "Polygon",
+ "type": "object"
+ },
+ "Product": {
+ "additionalProperties": false,
+ "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)",
+ "properties": {
+ "outer": {
+ "$ref": "#/$defs/Spec",
+ "description": "Will be executed once"
+ },
+ "inner": {
+ "$ref": "#/$defs/Spec",
+ "description": "Will be executed len(outer) times"
+ },
+ "type": {
+ "const": "Product",
+ "default": "Product",
+ "enum": [
+ "Product"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "outer",
+ "inner"
+ ],
+ "title": "Product",
+ "type": "object"
+ },
+ "Range": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis >= min and <= max.\n\n>>> r = Range(\"x\", 1, 2)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, True, False, False])",
+ "properties": {
+ "axis": {
+ "description": "The name matching the axis to mask in spec",
+ "title": "Axis"
+ },
+ "min": {
+ "description": "The minimum inclusive value in the region",
+ "title": "Min",
+ "type": "number"
+ },
+ "max": {
+ "description": "The minimum inclusive value in the region",
+ "title": "Max",
+ "type": "number"
+ },
+ "type": {
+ "const": "Range",
+ "default": "Range",
+ "enum": [
+ "Range"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "axis",
+ "min",
+ "max"
+ ],
+ "title": "Range",
+ "type": "object"
+ },
+ "Rectangle": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within a rotated xy rectangle.\n\n.. example_spec::\n\n from scanspec.regions import Rectangle\n from scanspec.specs import Line\n\n grid = Line(\"y\", 1, 3, 10) * ~Line(\"x\", 0, 2, 10)\n spec = grid & Rectangle(\"x\", \"y\", 0, 1.1, 1.5, 2.1, 30)",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_min": {
+ "description": "Minimum inclusive x value in the region",
+ "title": "X Min",
+ "type": "number"
+ },
+ "y_min": {
+ "description": "Minimum inclusive y value in the region",
+ "title": "Y Min",
+ "type": "number"
+ },
+ "x_max": {
+ "description": "Maximum inclusive x value in the region",
+ "title": "X Max",
+ "type": "number"
+ },
+ "y_max": {
+ "description": "Maximum inclusive y value in the region",
+ "title": "Y Max",
+ "type": "number"
+ },
+ "angle": {
+ "default": 0.0,
+ "description": "Clockwise rotation angle of the rectangle",
+ "title": "Angle",
+ "type": "number"
+ },
+ "type": {
+ "const": "Rectangle",
+ "default": "Rectangle",
+ "enum": [
+ "Rectangle"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_min",
+ "y_min",
+ "x_max",
+ "y_max"
+ ],
+ "title": "Rectangle",
+ "type": "object"
+ },
+ "Region": {
+ "discriminator": {
+ "mapping": {
+ "Circle": "#/$defs/Circle",
+ "CombinationOf": "#/$defs/CombinationOf",
+ "DifferenceOf": "#/$defs/DifferenceOf",
+ "Ellipse": "#/$defs/Ellipse",
+ "IntersectionOf": "#/$defs/IntersectionOf",
+ "Polygon": "#/$defs/Polygon",
+ "Range": "#/$defs/Range",
+ "Rectangle": "#/$defs/Rectangle",
+ "SymmetricDifferenceOf": "#/$defs/SymmetricDifferenceOf",
+ "UnionOf": "#/$defs/UnionOf"
+ },
+ "propertyName": "type"
+ },
+ "oneOf": [
+ {
+ "$ref": "#/$defs/CombinationOf"
+ },
+ {
+ "$ref": "#/$defs/UnionOf"
+ },
+ {
+ "$ref": "#/$defs/IntersectionOf"
+ },
+ {
+ "$ref": "#/$defs/DifferenceOf"
+ },
+ {
+ "$ref": "#/$defs/SymmetricDifferenceOf"
+ },
+ {
+ "$ref": "#/$defs/Range"
+ },
+ {
+ "$ref": "#/$defs/Rectangle"
+ },
+ {
+ "$ref": "#/$defs/Polygon"
+ },
+ {
+ "$ref": "#/$defs/Circle"
+ },
+ {
+ "$ref": "#/$defs/Ellipse"
+ }
+ ]
+ },
+ "Repeat": {
+ "additionalProperties": false,
+ "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",
+ "properties": {
+ "num": {
+ "description": "Number of frames to produce",
+ "minimum": 1,
+ "title": "Num",
+ "type": "integer"
+ },
+ "gap": {
+ "default": true,
+ "description": "If False and the slowest of the stack of Frames is snaked then the end and start of consecutive iterations of Spec will have no gap",
+ "title": "Gap",
+ "type": "boolean"
+ },
+ "type": {
+ "const": "Repeat",
+ "default": "Repeat",
+ "enum": [
+ "Repeat"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "num"
+ ],
+ "title": "Repeat",
+ "type": "object"
+ },
+ "Snake": {
+ "additionalProperties": false,
+ "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)",
+ "properties": {
+ "spec": {
+ "$ref": "#/$defs/Spec",
+ "description": "The Spec to run in reverse every other iteration"
+ },
+ "type": {
+ "const": "Snake",
+ "default": "Snake",
+ "enum": [
+ "Snake"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "spec"
+ ],
+ "title": "Snake",
+ "type": "object"
+ },
+ "Spec": {
+ "discriminator": {
+ "mapping": {
+ "Concat": "#/$defs/Concat",
+ "Line": "#/$defs/Line",
+ "Mask": "#/$defs/Mask",
+ "Product": "#/$defs/Product",
+ "Repeat": "#/$defs/Repeat",
+ "Snake": "#/$defs/Snake",
+ "Spiral": "#/$defs/Spiral",
+ "Squash": "#/$defs/Squash",
+ "Static": "#/$defs/Static",
+ "Zip": "#/$defs/Zip"
+ },
+ "propertyName": "type"
+ },
+ "oneOf": [
+ {
+ "$ref": "#/$defs/Product"
+ },
+ {
+ "$ref": "#/$defs/Repeat"
+ },
+ {
+ "$ref": "#/$defs/Zip"
+ },
+ {
+ "$ref": "#/$defs/Mask"
+ },
+ {
+ "$ref": "#/$defs/Snake"
+ },
+ {
+ "$ref": "#/$defs/Concat"
+ },
+ {
+ "$ref": "#/$defs/Squash"
+ },
+ {
+ "$ref": "#/$defs/Line"
+ },
+ {
+ "$ref": "#/$defs/Static"
+ },
+ {
+ "$ref": "#/$defs/Spiral"
+ }
+ ]
+ },
+ "Spiral": {
+ "additionalProperties": false,
+ "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)",
+ "properties": {
+ "x_axis": {
+ "description": "An identifier for what to move for x",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "An identifier for what to move for y",
+ "title": "Y Axis"
+ },
+ "x_start": {
+ "description": "x centre of the spiral",
+ "title": "X Start",
+ "type": "number"
+ },
+ "y_start": {
+ "description": "y centre of the spiral",
+ "title": "Y Start",
+ "type": "number"
+ },
+ "x_range": {
+ "description": "x width of the spiral",
+ "title": "X Range",
+ "type": "number"
+ },
+ "y_range": {
+ "description": "y width of the spiral",
+ "title": "Y Range",
+ "type": "number"
+ },
+ "num": {
+ "description": "Number of frames to produce",
+ "minimum": 1,
+ "title": "Num",
+ "type": "integer"
+ },
+ "rotate": {
+ "default": 0.0,
+ "description": "How much to rotate the angle of the spiral",
+ "title": "Rotate",
+ "type": "number"
+ },
+ "type": {
+ "const": "Spiral",
+ "default": "Spiral",
+ "enum": [
+ "Spiral"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_start",
+ "y_start",
+ "x_range",
+ "y_range",
+ "num"
+ ],
+ "title": "Spiral",
+ "type": "object"
+ },
+ "Squash": {
+ "additionalProperties": false,
+ "description": "Squash a stack of Frames together into a single expanded Frames 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))",
+ "properties": {
+ "spec": {
+ "$ref": "#/$defs/Spec",
+ "description": "The Spec to squash the dimensions of"
+ },
+ "check_path_changes": {
+ "default": true,
+ "description": "If True path through scan will not be modified by squash",
+ "title": "Check Path Changes",
+ "type": "boolean"
+ },
+ "type": {
+ "const": "Squash",
+ "default": "Squash",
+ "enum": [
+ "Squash"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "spec"
+ ],
+ "title": "Squash",
+ "type": "object"
+ },
+ "Static": {
+ "additionalProperties": false,
+ "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))",
+ "properties": {
+ "axis": {
+ "description": "An identifier for what to move",
+ "title": "Axis"
+ },
+ "value": {
+ "description": "The value at each point",
+ "title": "Value",
+ "type": "number"
+ },
+ "num": {
+ "default": 1,
+ "description": "Number of frames to produce",
+ "minimum": 1,
+ "title": "Num",
+ "type": "integer"
+ },
+ "type": {
+ "const": "Static",
+ "default": "Static",
+ "enum": [
+ "Static"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "axis",
+ "value"
+ ],
+ "title": "Static",
+ "type": "object"
+ },
+ "SymmetricDifferenceOf": {
+ "additionalProperties": false,
+ "description": "A point is in SymmetricDifferenceOf(a, b) if in either a or b, but not both.\n\nTypically created with the ``^`` operator.\n\n>>> r = Range(\"x\", 0.5, 2.5) ^ Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, False, True, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "SymmetricDifferenceOf",
+ "default": "SymmetricDifferenceOf",
+ "enum": [
+ "SymmetricDifferenceOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "SymmetricDifferenceOf",
+ "type": "object"
+ },
+ "UnionOf": {
+ "additionalProperties": false,
+ "description": "A point is in UnionOf(a, b) if in either a or b.\n\nTypically created with the ``|`` operator\n\n>>> r = Range(\"x\", 0.5, 2.5) | Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, True, True, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "UnionOf",
+ "default": "UnionOf",
+ "enum": [
+ "UnionOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "UnionOf",
+ "type": "object"
+ },
+ "Zip": {
+ "additionalProperties": false,
+ "description": "Run two Specs in parallel, merging their midpoints together.\n\nTypically formed using `Spec.zip`.\n\nStacks of Frames are merged by:\n\n- If right creates a stack of a single Frames object of size 1, expand it to\n the size of the fastest Frames object created by left\n- Merge individual Frames 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))",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Spec",
+ "description": "The left-hand Spec to Zip, will appear earlier in axes"
+ },
+ "right": {
+ "$ref": "#/$defs/Spec",
+ "description": "The right-hand Spec to Zip, will appear later in axes"
+ },
+ "type": {
+ "const": "Zip",
+ "default": "Zip",
+ "enum": [
+ "Zip"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "Zip",
+ "type": "object"
+ }
+ },
+ "$ref": "#/$defs/Mask"
+}
+
If True path through scan will not be modified by squash
+Run the Spec in reverse on every other iteration when nested.
+Typically created with the ~
operator.
# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.specs import Line
+
+spec = Line("y", 1, 3, 3) * ~Line("x", 3, 5, 5)
+plot_spec(spec)
+
(Source code
, png
, hires.png
, pdf
)
Show JSON schema
{
+ "$defs": {
+ "Circle": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within an xy circle of given radius.\n\n.. example_spec::\n\n from scanspec.regions import Circle\n from scanspec.specs import Line\n\n grid = Line(\"y\", 1, 3, 10) * ~Line(\"x\", 0, 2, 10)\n spec = grid & Circle(\"x\", \"y\", 1, 2, 0.9)",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_middle": {
+ "description": "The central x point of the circle",
+ "title": "X Middle",
+ "type": "number"
+ },
+ "y_middle": {
+ "description": "The central y point of the circle",
+ "title": "Y Middle",
+ "type": "number"
+ },
+ "radius": {
+ "description": "Radius of the circle",
+ "exclusiveMinimum": 0.0,
+ "title": "Radius",
+ "type": "number"
+ },
+ "type": {
+ "const": "Circle",
+ "default": "Circle",
+ "enum": [
+ "Circle"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_middle",
+ "y_middle",
+ "radius"
+ ],
+ "title": "Circle",
+ "type": "object"
+ },
+ "CombinationOf": {
+ "additionalProperties": false,
+ "description": "Abstract baseclass for a combination of two regions, left and right.",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "CombinationOf",
+ "default": "CombinationOf",
+ "enum": [
+ "CombinationOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "CombinationOf",
+ "type": "object"
+ },
+ "Concat": {
+ "additionalProperties": false,
+ "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))",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Spec",
+ "description": "The left-hand Spec to Concat, midpoints will appear earlier"
+ },
+ "right": {
+ "$ref": "#/$defs/Spec",
+ "description": "The right-hand Spec to Concat, midpoints will appear later"
+ },
+ "gap": {
+ "default": false,
+ "description": "If True, force a gap in the output at the join",
+ "title": "Gap",
+ "type": "boolean"
+ },
+ "check_path_changes": {
+ "default": true,
+ "description": "If True path through scan will not be modified by squash",
+ "title": "Check Path Changes",
+ "type": "boolean"
+ },
+ "type": {
+ "const": "Concat",
+ "default": "Concat",
+ "enum": [
+ "Concat"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "Concat",
+ "type": "object"
+ },
+ "DifferenceOf": {
+ "additionalProperties": false,
+ "description": "A point is in DifferenceOf(a, b) if in a and not in b.\n\nTypically created with the ``-`` operator.\n\n>>> r = Range(\"x\", 0.5, 2.5) - Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, False, False, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "DifferenceOf",
+ "default": "DifferenceOf",
+ "enum": [
+ "DifferenceOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "DifferenceOf",
+ "type": "object"
+ },
+ "Ellipse": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within an xy ellipse of given radius.\n\n.. example_spec::\n\n from scanspec.regions import Ellipse\n from scanspec.specs import Line\n\n grid = Line(\"y\", 3, 8, 10) * ~Line(\"x\", 1 ,8, 10)\n spec = grid & Ellipse(\"x\", \"y\", 5, 5, 2, 3, 75)",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_middle": {
+ "description": "The central x point of the ellipse",
+ "title": "X Middle",
+ "type": "number"
+ },
+ "y_middle": {
+ "description": "The central y point of the ellipse",
+ "title": "Y Middle",
+ "type": "number"
+ },
+ "x_radius": {
+ "description": "The radius along the x axis of the ellipse",
+ "exclusiveMinimum": 0.0,
+ "title": "X Radius",
+ "type": "number"
+ },
+ "y_radius": {
+ "description": "The radius along the y axis of the ellipse",
+ "exclusiveMinimum": 0.0,
+ "title": "Y Radius",
+ "type": "number"
+ },
+ "angle": {
+ "default": 0.0,
+ "description": "The angle of the ellipse (degrees)",
+ "title": "Angle",
+ "type": "number"
+ },
+ "type": {
+ "const": "Ellipse",
+ "default": "Ellipse",
+ "enum": [
+ "Ellipse"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_middle",
+ "y_middle",
+ "x_radius",
+ "y_radius"
+ ],
+ "title": "Ellipse",
+ "type": "object"
+ },
+ "IntersectionOf": {
+ "additionalProperties": false,
+ "description": "A point is in IntersectionOf(a, b) if in both a and b.\n\nTypically created with the ``&`` operator.\n\n>>> r = Range(\"x\", 0.5, 2.5) & Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, False, True, False, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "IntersectionOf",
+ "default": "IntersectionOf",
+ "enum": [
+ "IntersectionOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "IntersectionOf",
+ "type": "object"
+ },
+ "Line": {
+ "additionalProperties": false,
+ "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)",
+ "properties": {
+ "axis": {
+ "description": "An identifier for what to move",
+ "title": "Axis"
+ },
+ "start": {
+ "description": "Midpoint of the first point of the line",
+ "title": "Start",
+ "type": "number"
+ },
+ "stop": {
+ "description": "Midpoint of the last point of the line",
+ "title": "Stop",
+ "type": "number"
+ },
+ "num": {
+ "description": "Number of frames to produce",
+ "minimum": 1,
+ "title": "Num",
+ "type": "integer"
+ },
+ "type": {
+ "const": "Line",
+ "default": "Line",
+ "enum": [
+ "Line"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "axis",
+ "start",
+ "stop",
+ "num"
+ ],
+ "title": "Line",
+ "type": "object"
+ },
+ "Mask": {
+ "additionalProperties": false,
+ "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 Frames 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`",
+ "properties": {
+ "spec": {
+ "$ref": "#/$defs/Spec",
+ "description": "The Spec containing the source midpoints"
+ },
+ "region": {
+ "$ref": "#/$defs/Region",
+ "description": "The Region that midpoints will be inside"
+ },
+ "check_path_changes": {
+ "default": true,
+ "description": "If True path through scan will not be modified by squash",
+ "title": "Check Path Changes",
+ "type": "boolean"
+ },
+ "type": {
+ "const": "Mask",
+ "default": "Mask",
+ "enum": [
+ "Mask"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "spec",
+ "region"
+ ],
+ "title": "Mask",
+ "type": "object"
+ },
+ "Polygon": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within a rotated xy polygon.\n\n.. example_spec::\n\n from scanspec.regions import Polygon\n from scanspec.specs import Line\n\n grid = Line(\"y\", 3, 8, 10) * ~Line(\"x\", 1 ,8, 10)\n spec = grid & Polygon(\"x\", \"y\", [1.0, 6.0, 8.0, 2.0], [4.0, 10.0, 6.0, 1.0])",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_verts": {
+ "description": "The Nx1 x coordinates of the polygons vertices",
+ "items": {
+ "type": "number"
+ },
+ "minItems": 3,
+ "title": "X Verts",
+ "type": "array"
+ },
+ "y_verts": {
+ "description": "The Nx1 y coordinates of the polygons vertices",
+ "items": {
+ "type": "number"
+ },
+ "minItems": 3,
+ "title": "Y Verts",
+ "type": "array"
+ },
+ "type": {
+ "const": "Polygon",
+ "default": "Polygon",
+ "enum": [
+ "Polygon"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_verts",
+ "y_verts"
+ ],
+ "title": "Polygon",
+ "type": "object"
+ },
+ "Product": {
+ "additionalProperties": false,
+ "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)",
+ "properties": {
+ "outer": {
+ "$ref": "#/$defs/Spec",
+ "description": "Will be executed once"
+ },
+ "inner": {
+ "$ref": "#/$defs/Spec",
+ "description": "Will be executed len(outer) times"
+ },
+ "type": {
+ "const": "Product",
+ "default": "Product",
+ "enum": [
+ "Product"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "outer",
+ "inner"
+ ],
+ "title": "Product",
+ "type": "object"
+ },
+ "Range": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis >= min and <= max.\n\n>>> r = Range(\"x\", 1, 2)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, True, False, False])",
+ "properties": {
+ "axis": {
+ "description": "The name matching the axis to mask in spec",
+ "title": "Axis"
+ },
+ "min": {
+ "description": "The minimum inclusive value in the region",
+ "title": "Min",
+ "type": "number"
+ },
+ "max": {
+ "description": "The minimum inclusive value in the region",
+ "title": "Max",
+ "type": "number"
+ },
+ "type": {
+ "const": "Range",
+ "default": "Range",
+ "enum": [
+ "Range"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "axis",
+ "min",
+ "max"
+ ],
+ "title": "Range",
+ "type": "object"
+ },
+ "Rectangle": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within a rotated xy rectangle.\n\n.. example_spec::\n\n from scanspec.regions import Rectangle\n from scanspec.specs import Line\n\n grid = Line(\"y\", 1, 3, 10) * ~Line(\"x\", 0, 2, 10)\n spec = grid & Rectangle(\"x\", \"y\", 0, 1.1, 1.5, 2.1, 30)",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_min": {
+ "description": "Minimum inclusive x value in the region",
+ "title": "X Min",
+ "type": "number"
+ },
+ "y_min": {
+ "description": "Minimum inclusive y value in the region",
+ "title": "Y Min",
+ "type": "number"
+ },
+ "x_max": {
+ "description": "Maximum inclusive x value in the region",
+ "title": "X Max",
+ "type": "number"
+ },
+ "y_max": {
+ "description": "Maximum inclusive y value in the region",
+ "title": "Y Max",
+ "type": "number"
+ },
+ "angle": {
+ "default": 0.0,
+ "description": "Clockwise rotation angle of the rectangle",
+ "title": "Angle",
+ "type": "number"
+ },
+ "type": {
+ "const": "Rectangle",
+ "default": "Rectangle",
+ "enum": [
+ "Rectangle"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_min",
+ "y_min",
+ "x_max",
+ "y_max"
+ ],
+ "title": "Rectangle",
+ "type": "object"
+ },
+ "Region": {
+ "discriminator": {
+ "mapping": {
+ "Circle": "#/$defs/Circle",
+ "CombinationOf": "#/$defs/CombinationOf",
+ "DifferenceOf": "#/$defs/DifferenceOf",
+ "Ellipse": "#/$defs/Ellipse",
+ "IntersectionOf": "#/$defs/IntersectionOf",
+ "Polygon": "#/$defs/Polygon",
+ "Range": "#/$defs/Range",
+ "Rectangle": "#/$defs/Rectangle",
+ "SymmetricDifferenceOf": "#/$defs/SymmetricDifferenceOf",
+ "UnionOf": "#/$defs/UnionOf"
+ },
+ "propertyName": "type"
+ },
+ "oneOf": [
+ {
+ "$ref": "#/$defs/CombinationOf"
+ },
+ {
+ "$ref": "#/$defs/UnionOf"
+ },
+ {
+ "$ref": "#/$defs/IntersectionOf"
+ },
+ {
+ "$ref": "#/$defs/DifferenceOf"
+ },
+ {
+ "$ref": "#/$defs/SymmetricDifferenceOf"
+ },
+ {
+ "$ref": "#/$defs/Range"
+ },
+ {
+ "$ref": "#/$defs/Rectangle"
+ },
+ {
+ "$ref": "#/$defs/Polygon"
+ },
+ {
+ "$ref": "#/$defs/Circle"
+ },
+ {
+ "$ref": "#/$defs/Ellipse"
+ }
+ ]
+ },
+ "Repeat": {
+ "additionalProperties": false,
+ "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",
+ "properties": {
+ "num": {
+ "description": "Number of frames to produce",
+ "minimum": 1,
+ "title": "Num",
+ "type": "integer"
+ },
+ "gap": {
+ "default": true,
+ "description": "If False and the slowest of the stack of Frames is snaked then the end and start of consecutive iterations of Spec will have no gap",
+ "title": "Gap",
+ "type": "boolean"
+ },
+ "type": {
+ "const": "Repeat",
+ "default": "Repeat",
+ "enum": [
+ "Repeat"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "num"
+ ],
+ "title": "Repeat",
+ "type": "object"
+ },
+ "Snake": {
+ "additionalProperties": false,
+ "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)",
+ "properties": {
+ "spec": {
+ "$ref": "#/$defs/Spec",
+ "description": "The Spec to run in reverse every other iteration"
+ },
+ "type": {
+ "const": "Snake",
+ "default": "Snake",
+ "enum": [
+ "Snake"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "spec"
+ ],
+ "title": "Snake",
+ "type": "object"
+ },
+ "Spec": {
+ "discriminator": {
+ "mapping": {
+ "Concat": "#/$defs/Concat",
+ "Line": "#/$defs/Line",
+ "Mask": "#/$defs/Mask",
+ "Product": "#/$defs/Product",
+ "Repeat": "#/$defs/Repeat",
+ "Snake": "#/$defs/Snake",
+ "Spiral": "#/$defs/Spiral",
+ "Squash": "#/$defs/Squash",
+ "Static": "#/$defs/Static",
+ "Zip": "#/$defs/Zip"
+ },
+ "propertyName": "type"
+ },
+ "oneOf": [
+ {
+ "$ref": "#/$defs/Product"
+ },
+ {
+ "$ref": "#/$defs/Repeat"
+ },
+ {
+ "$ref": "#/$defs/Zip"
+ },
+ {
+ "$ref": "#/$defs/Mask"
+ },
+ {
+ "$ref": "#/$defs/Snake"
+ },
+ {
+ "$ref": "#/$defs/Concat"
+ },
+ {
+ "$ref": "#/$defs/Squash"
+ },
+ {
+ "$ref": "#/$defs/Line"
+ },
+ {
+ "$ref": "#/$defs/Static"
+ },
+ {
+ "$ref": "#/$defs/Spiral"
+ }
+ ]
+ },
+ "Spiral": {
+ "additionalProperties": false,
+ "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)",
+ "properties": {
+ "x_axis": {
+ "description": "An identifier for what to move for x",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "An identifier for what to move for y",
+ "title": "Y Axis"
+ },
+ "x_start": {
+ "description": "x centre of the spiral",
+ "title": "X Start",
+ "type": "number"
+ },
+ "y_start": {
+ "description": "y centre of the spiral",
+ "title": "Y Start",
+ "type": "number"
+ },
+ "x_range": {
+ "description": "x width of the spiral",
+ "title": "X Range",
+ "type": "number"
+ },
+ "y_range": {
+ "description": "y width of the spiral",
+ "title": "Y Range",
+ "type": "number"
+ },
+ "num": {
+ "description": "Number of frames to produce",
+ "minimum": 1,
+ "title": "Num",
+ "type": "integer"
+ },
+ "rotate": {
+ "default": 0.0,
+ "description": "How much to rotate the angle of the spiral",
+ "title": "Rotate",
+ "type": "number"
+ },
+ "type": {
+ "const": "Spiral",
+ "default": "Spiral",
+ "enum": [
+ "Spiral"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_start",
+ "y_start",
+ "x_range",
+ "y_range",
+ "num"
+ ],
+ "title": "Spiral",
+ "type": "object"
+ },
+ "Squash": {
+ "additionalProperties": false,
+ "description": "Squash a stack of Frames together into a single expanded Frames 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))",
+ "properties": {
+ "spec": {
+ "$ref": "#/$defs/Spec",
+ "description": "The Spec to squash the dimensions of"
+ },
+ "check_path_changes": {
+ "default": true,
+ "description": "If True path through scan will not be modified by squash",
+ "title": "Check Path Changes",
+ "type": "boolean"
+ },
+ "type": {
+ "const": "Squash",
+ "default": "Squash",
+ "enum": [
+ "Squash"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "spec"
+ ],
+ "title": "Squash",
+ "type": "object"
+ },
+ "Static": {
+ "additionalProperties": false,
+ "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))",
+ "properties": {
+ "axis": {
+ "description": "An identifier for what to move",
+ "title": "Axis"
+ },
+ "value": {
+ "description": "The value at each point",
+ "title": "Value",
+ "type": "number"
+ },
+ "num": {
+ "default": 1,
+ "description": "Number of frames to produce",
+ "minimum": 1,
+ "title": "Num",
+ "type": "integer"
+ },
+ "type": {
+ "const": "Static",
+ "default": "Static",
+ "enum": [
+ "Static"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "axis",
+ "value"
+ ],
+ "title": "Static",
+ "type": "object"
+ },
+ "SymmetricDifferenceOf": {
+ "additionalProperties": false,
+ "description": "A point is in SymmetricDifferenceOf(a, b) if in either a or b, but not both.\n\nTypically created with the ``^`` operator.\n\n>>> r = Range(\"x\", 0.5, 2.5) ^ Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, False, True, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "SymmetricDifferenceOf",
+ "default": "SymmetricDifferenceOf",
+ "enum": [
+ "SymmetricDifferenceOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "SymmetricDifferenceOf",
+ "type": "object"
+ },
+ "UnionOf": {
+ "additionalProperties": false,
+ "description": "A point is in UnionOf(a, b) if in either a or b.\n\nTypically created with the ``|`` operator\n\n>>> r = Range(\"x\", 0.5, 2.5) | Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, True, True, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "UnionOf",
+ "default": "UnionOf",
+ "enum": [
+ "UnionOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "UnionOf",
+ "type": "object"
+ },
+ "Zip": {
+ "additionalProperties": false,
+ "description": "Run two Specs in parallel, merging their midpoints together.\n\nTypically formed using `Spec.zip`.\n\nStacks of Frames are merged by:\n\n- If right creates a stack of a single Frames object of size 1, expand it to\n the size of the fastest Frames object created by left\n- Merge individual Frames 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))",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Spec",
+ "description": "The left-hand Spec to Zip, will appear earlier in axes"
+ },
+ "right": {
+ "$ref": "#/$defs/Spec",
+ "description": "The right-hand Spec to Zip, will appear later in axes"
+ },
+ "type": {
+ "const": "Zip",
+ "default": "Zip",
+ "enum": [
+ "Zip"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "Zip",
+ "type": "object"
+ }
+ },
+ "$ref": "#/$defs/Snake"
+}
+
Concatenate two Specs together, running one after the other.
+Each Dimension of left and right must contain the same axes. Typically
+formed using Spec.concat
.
# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.specs import Line
+
+spec = Line("x", 1, 3, 3).concat(Line("x", 4, 5, 5))
+plot_spec(spec)
+
(Source code
, png
, hires.png
, pdf
)
Show JSON schema
{
+ "$defs": {
+ "Circle": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within an xy circle of given radius.\n\n.. example_spec::\n\n from scanspec.regions import Circle\n from scanspec.specs import Line\n\n grid = Line(\"y\", 1, 3, 10) * ~Line(\"x\", 0, 2, 10)\n spec = grid & Circle(\"x\", \"y\", 1, 2, 0.9)",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_middle": {
+ "description": "The central x point of the circle",
+ "title": "X Middle",
+ "type": "number"
+ },
+ "y_middle": {
+ "description": "The central y point of the circle",
+ "title": "Y Middle",
+ "type": "number"
+ },
+ "radius": {
+ "description": "Radius of the circle",
+ "exclusiveMinimum": 0.0,
+ "title": "Radius",
+ "type": "number"
+ },
+ "type": {
+ "const": "Circle",
+ "default": "Circle",
+ "enum": [
+ "Circle"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_middle",
+ "y_middle",
+ "radius"
+ ],
+ "title": "Circle",
+ "type": "object"
+ },
+ "CombinationOf": {
+ "additionalProperties": false,
+ "description": "Abstract baseclass for a combination of two regions, left and right.",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "CombinationOf",
+ "default": "CombinationOf",
+ "enum": [
+ "CombinationOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "CombinationOf",
+ "type": "object"
+ },
+ "Concat": {
+ "additionalProperties": false,
+ "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))",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Spec",
+ "description": "The left-hand Spec to Concat, midpoints will appear earlier"
+ },
+ "right": {
+ "$ref": "#/$defs/Spec",
+ "description": "The right-hand Spec to Concat, midpoints will appear later"
+ },
+ "gap": {
+ "default": false,
+ "description": "If True, force a gap in the output at the join",
+ "title": "Gap",
+ "type": "boolean"
+ },
+ "check_path_changes": {
+ "default": true,
+ "description": "If True path through scan will not be modified by squash",
+ "title": "Check Path Changes",
+ "type": "boolean"
+ },
+ "type": {
+ "const": "Concat",
+ "default": "Concat",
+ "enum": [
+ "Concat"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "Concat",
+ "type": "object"
+ },
+ "DifferenceOf": {
+ "additionalProperties": false,
+ "description": "A point is in DifferenceOf(a, b) if in a and not in b.\n\nTypically created with the ``-`` operator.\n\n>>> r = Range(\"x\", 0.5, 2.5) - Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, False, False, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "DifferenceOf",
+ "default": "DifferenceOf",
+ "enum": [
+ "DifferenceOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "DifferenceOf",
+ "type": "object"
+ },
+ "Ellipse": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within an xy ellipse of given radius.\n\n.. example_spec::\n\n from scanspec.regions import Ellipse\n from scanspec.specs import Line\n\n grid = Line(\"y\", 3, 8, 10) * ~Line(\"x\", 1 ,8, 10)\n spec = grid & Ellipse(\"x\", \"y\", 5, 5, 2, 3, 75)",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_middle": {
+ "description": "The central x point of the ellipse",
+ "title": "X Middle",
+ "type": "number"
+ },
+ "y_middle": {
+ "description": "The central y point of the ellipse",
+ "title": "Y Middle",
+ "type": "number"
+ },
+ "x_radius": {
+ "description": "The radius along the x axis of the ellipse",
+ "exclusiveMinimum": 0.0,
+ "title": "X Radius",
+ "type": "number"
+ },
+ "y_radius": {
+ "description": "The radius along the y axis of the ellipse",
+ "exclusiveMinimum": 0.0,
+ "title": "Y Radius",
+ "type": "number"
+ },
+ "angle": {
+ "default": 0.0,
+ "description": "The angle of the ellipse (degrees)",
+ "title": "Angle",
+ "type": "number"
+ },
+ "type": {
+ "const": "Ellipse",
+ "default": "Ellipse",
+ "enum": [
+ "Ellipse"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_middle",
+ "y_middle",
+ "x_radius",
+ "y_radius"
+ ],
+ "title": "Ellipse",
+ "type": "object"
+ },
+ "IntersectionOf": {
+ "additionalProperties": false,
+ "description": "A point is in IntersectionOf(a, b) if in both a and b.\n\nTypically created with the ``&`` operator.\n\n>>> r = Range(\"x\", 0.5, 2.5) & Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, False, True, False, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "IntersectionOf",
+ "default": "IntersectionOf",
+ "enum": [
+ "IntersectionOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "IntersectionOf",
+ "type": "object"
+ },
+ "Line": {
+ "additionalProperties": false,
+ "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)",
+ "properties": {
+ "axis": {
+ "description": "An identifier for what to move",
+ "title": "Axis"
+ },
+ "start": {
+ "description": "Midpoint of the first point of the line",
+ "title": "Start",
+ "type": "number"
+ },
+ "stop": {
+ "description": "Midpoint of the last point of the line",
+ "title": "Stop",
+ "type": "number"
+ },
+ "num": {
+ "description": "Number of frames to produce",
+ "minimum": 1,
+ "title": "Num",
+ "type": "integer"
+ },
+ "type": {
+ "const": "Line",
+ "default": "Line",
+ "enum": [
+ "Line"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "axis",
+ "start",
+ "stop",
+ "num"
+ ],
+ "title": "Line",
+ "type": "object"
+ },
+ "Mask": {
+ "additionalProperties": false,
+ "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 Frames 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`",
+ "properties": {
+ "spec": {
+ "$ref": "#/$defs/Spec",
+ "description": "The Spec containing the source midpoints"
+ },
+ "region": {
+ "$ref": "#/$defs/Region",
+ "description": "The Region that midpoints will be inside"
+ },
+ "check_path_changes": {
+ "default": true,
+ "description": "If True path through scan will not be modified by squash",
+ "title": "Check Path Changes",
+ "type": "boolean"
+ },
+ "type": {
+ "const": "Mask",
+ "default": "Mask",
+ "enum": [
+ "Mask"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "spec",
+ "region"
+ ],
+ "title": "Mask",
+ "type": "object"
+ },
+ "Polygon": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within a rotated xy polygon.\n\n.. example_spec::\n\n from scanspec.regions import Polygon\n from scanspec.specs import Line\n\n grid = Line(\"y\", 3, 8, 10) * ~Line(\"x\", 1 ,8, 10)\n spec = grid & Polygon(\"x\", \"y\", [1.0, 6.0, 8.0, 2.0], [4.0, 10.0, 6.0, 1.0])",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_verts": {
+ "description": "The Nx1 x coordinates of the polygons vertices",
+ "items": {
+ "type": "number"
+ },
+ "minItems": 3,
+ "title": "X Verts",
+ "type": "array"
+ },
+ "y_verts": {
+ "description": "The Nx1 y coordinates of the polygons vertices",
+ "items": {
+ "type": "number"
+ },
+ "minItems": 3,
+ "title": "Y Verts",
+ "type": "array"
+ },
+ "type": {
+ "const": "Polygon",
+ "default": "Polygon",
+ "enum": [
+ "Polygon"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_verts",
+ "y_verts"
+ ],
+ "title": "Polygon",
+ "type": "object"
+ },
+ "Product": {
+ "additionalProperties": false,
+ "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)",
+ "properties": {
+ "outer": {
+ "$ref": "#/$defs/Spec",
+ "description": "Will be executed once"
+ },
+ "inner": {
+ "$ref": "#/$defs/Spec",
+ "description": "Will be executed len(outer) times"
+ },
+ "type": {
+ "const": "Product",
+ "default": "Product",
+ "enum": [
+ "Product"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "outer",
+ "inner"
+ ],
+ "title": "Product",
+ "type": "object"
+ },
+ "Range": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis >= min and <= max.\n\n>>> r = Range(\"x\", 1, 2)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, True, False, False])",
+ "properties": {
+ "axis": {
+ "description": "The name matching the axis to mask in spec",
+ "title": "Axis"
+ },
+ "min": {
+ "description": "The minimum inclusive value in the region",
+ "title": "Min",
+ "type": "number"
+ },
+ "max": {
+ "description": "The minimum inclusive value in the region",
+ "title": "Max",
+ "type": "number"
+ },
+ "type": {
+ "const": "Range",
+ "default": "Range",
+ "enum": [
+ "Range"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "axis",
+ "min",
+ "max"
+ ],
+ "title": "Range",
+ "type": "object"
+ },
+ "Rectangle": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within a rotated xy rectangle.\n\n.. example_spec::\n\n from scanspec.regions import Rectangle\n from scanspec.specs import Line\n\n grid = Line(\"y\", 1, 3, 10) * ~Line(\"x\", 0, 2, 10)\n spec = grid & Rectangle(\"x\", \"y\", 0, 1.1, 1.5, 2.1, 30)",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_min": {
+ "description": "Minimum inclusive x value in the region",
+ "title": "X Min",
+ "type": "number"
+ },
+ "y_min": {
+ "description": "Minimum inclusive y value in the region",
+ "title": "Y Min",
+ "type": "number"
+ },
+ "x_max": {
+ "description": "Maximum inclusive x value in the region",
+ "title": "X Max",
+ "type": "number"
+ },
+ "y_max": {
+ "description": "Maximum inclusive y value in the region",
+ "title": "Y Max",
+ "type": "number"
+ },
+ "angle": {
+ "default": 0.0,
+ "description": "Clockwise rotation angle of the rectangle",
+ "title": "Angle",
+ "type": "number"
+ },
+ "type": {
+ "const": "Rectangle",
+ "default": "Rectangle",
+ "enum": [
+ "Rectangle"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_min",
+ "y_min",
+ "x_max",
+ "y_max"
+ ],
+ "title": "Rectangle",
+ "type": "object"
+ },
+ "Region": {
+ "discriminator": {
+ "mapping": {
+ "Circle": "#/$defs/Circle",
+ "CombinationOf": "#/$defs/CombinationOf",
+ "DifferenceOf": "#/$defs/DifferenceOf",
+ "Ellipse": "#/$defs/Ellipse",
+ "IntersectionOf": "#/$defs/IntersectionOf",
+ "Polygon": "#/$defs/Polygon",
+ "Range": "#/$defs/Range",
+ "Rectangle": "#/$defs/Rectangle",
+ "SymmetricDifferenceOf": "#/$defs/SymmetricDifferenceOf",
+ "UnionOf": "#/$defs/UnionOf"
+ },
+ "propertyName": "type"
+ },
+ "oneOf": [
+ {
+ "$ref": "#/$defs/CombinationOf"
+ },
+ {
+ "$ref": "#/$defs/UnionOf"
+ },
+ {
+ "$ref": "#/$defs/IntersectionOf"
+ },
+ {
+ "$ref": "#/$defs/DifferenceOf"
+ },
+ {
+ "$ref": "#/$defs/SymmetricDifferenceOf"
+ },
+ {
+ "$ref": "#/$defs/Range"
+ },
+ {
+ "$ref": "#/$defs/Rectangle"
+ },
+ {
+ "$ref": "#/$defs/Polygon"
+ },
+ {
+ "$ref": "#/$defs/Circle"
+ },
+ {
+ "$ref": "#/$defs/Ellipse"
+ }
+ ]
+ },
+ "Repeat": {
+ "additionalProperties": false,
+ "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",
+ "properties": {
+ "num": {
+ "description": "Number of frames to produce",
+ "minimum": 1,
+ "title": "Num",
+ "type": "integer"
+ },
+ "gap": {
+ "default": true,
+ "description": "If False and the slowest of the stack of Frames is snaked then the end and start of consecutive iterations of Spec will have no gap",
+ "title": "Gap",
+ "type": "boolean"
+ },
+ "type": {
+ "const": "Repeat",
+ "default": "Repeat",
+ "enum": [
+ "Repeat"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "num"
+ ],
+ "title": "Repeat",
+ "type": "object"
+ },
+ "Snake": {
+ "additionalProperties": false,
+ "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)",
+ "properties": {
+ "spec": {
+ "$ref": "#/$defs/Spec",
+ "description": "The Spec to run in reverse every other iteration"
+ },
+ "type": {
+ "const": "Snake",
+ "default": "Snake",
+ "enum": [
+ "Snake"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "spec"
+ ],
+ "title": "Snake",
+ "type": "object"
+ },
+ "Spec": {
+ "discriminator": {
+ "mapping": {
+ "Concat": "#/$defs/Concat",
+ "Line": "#/$defs/Line",
+ "Mask": "#/$defs/Mask",
+ "Product": "#/$defs/Product",
+ "Repeat": "#/$defs/Repeat",
+ "Snake": "#/$defs/Snake",
+ "Spiral": "#/$defs/Spiral",
+ "Squash": "#/$defs/Squash",
+ "Static": "#/$defs/Static",
+ "Zip": "#/$defs/Zip"
+ },
+ "propertyName": "type"
+ },
+ "oneOf": [
+ {
+ "$ref": "#/$defs/Product"
+ },
+ {
+ "$ref": "#/$defs/Repeat"
+ },
+ {
+ "$ref": "#/$defs/Zip"
+ },
+ {
+ "$ref": "#/$defs/Mask"
+ },
+ {
+ "$ref": "#/$defs/Snake"
+ },
+ {
+ "$ref": "#/$defs/Concat"
+ },
+ {
+ "$ref": "#/$defs/Squash"
+ },
+ {
+ "$ref": "#/$defs/Line"
+ },
+ {
+ "$ref": "#/$defs/Static"
+ },
+ {
+ "$ref": "#/$defs/Spiral"
+ }
+ ]
+ },
+ "Spiral": {
+ "additionalProperties": false,
+ "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)",
+ "properties": {
+ "x_axis": {
+ "description": "An identifier for what to move for x",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "An identifier for what to move for y",
+ "title": "Y Axis"
+ },
+ "x_start": {
+ "description": "x centre of the spiral",
+ "title": "X Start",
+ "type": "number"
+ },
+ "y_start": {
+ "description": "y centre of the spiral",
+ "title": "Y Start",
+ "type": "number"
+ },
+ "x_range": {
+ "description": "x width of the spiral",
+ "title": "X Range",
+ "type": "number"
+ },
+ "y_range": {
+ "description": "y width of the spiral",
+ "title": "Y Range",
+ "type": "number"
+ },
+ "num": {
+ "description": "Number of frames to produce",
+ "minimum": 1,
+ "title": "Num",
+ "type": "integer"
+ },
+ "rotate": {
+ "default": 0.0,
+ "description": "How much to rotate the angle of the spiral",
+ "title": "Rotate",
+ "type": "number"
+ },
+ "type": {
+ "const": "Spiral",
+ "default": "Spiral",
+ "enum": [
+ "Spiral"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_start",
+ "y_start",
+ "x_range",
+ "y_range",
+ "num"
+ ],
+ "title": "Spiral",
+ "type": "object"
+ },
+ "Squash": {
+ "additionalProperties": false,
+ "description": "Squash a stack of Frames together into a single expanded Frames 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))",
+ "properties": {
+ "spec": {
+ "$ref": "#/$defs/Spec",
+ "description": "The Spec to squash the dimensions of"
+ },
+ "check_path_changes": {
+ "default": true,
+ "description": "If True path through scan will not be modified by squash",
+ "title": "Check Path Changes",
+ "type": "boolean"
+ },
+ "type": {
+ "const": "Squash",
+ "default": "Squash",
+ "enum": [
+ "Squash"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "spec"
+ ],
+ "title": "Squash",
+ "type": "object"
+ },
+ "Static": {
+ "additionalProperties": false,
+ "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))",
+ "properties": {
+ "axis": {
+ "description": "An identifier for what to move",
+ "title": "Axis"
+ },
+ "value": {
+ "description": "The value at each point",
+ "title": "Value",
+ "type": "number"
+ },
+ "num": {
+ "default": 1,
+ "description": "Number of frames to produce",
+ "minimum": 1,
+ "title": "Num",
+ "type": "integer"
+ },
+ "type": {
+ "const": "Static",
+ "default": "Static",
+ "enum": [
+ "Static"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "axis",
+ "value"
+ ],
+ "title": "Static",
+ "type": "object"
+ },
+ "SymmetricDifferenceOf": {
+ "additionalProperties": false,
+ "description": "A point is in SymmetricDifferenceOf(a, b) if in either a or b, but not both.\n\nTypically created with the ``^`` operator.\n\n>>> r = Range(\"x\", 0.5, 2.5) ^ Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, False, True, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "SymmetricDifferenceOf",
+ "default": "SymmetricDifferenceOf",
+ "enum": [
+ "SymmetricDifferenceOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "SymmetricDifferenceOf",
+ "type": "object"
+ },
+ "UnionOf": {
+ "additionalProperties": false,
+ "description": "A point is in UnionOf(a, b) if in either a or b.\n\nTypically created with the ``|`` operator\n\n>>> r = Range(\"x\", 0.5, 2.5) | Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, True, True, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "UnionOf",
+ "default": "UnionOf",
+ "enum": [
+ "UnionOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "UnionOf",
+ "type": "object"
+ },
+ "Zip": {
+ "additionalProperties": false,
+ "description": "Run two Specs in parallel, merging their midpoints together.\n\nTypically formed using `Spec.zip`.\n\nStacks of Frames are merged by:\n\n- If right creates a stack of a single Frames object of size 1, expand it to\n the size of the fastest Frames object created by left\n- Merge individual Frames 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))",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Spec",
+ "description": "The left-hand Spec to Zip, will appear earlier in axes"
+ },
+ "right": {
+ "$ref": "#/$defs/Spec",
+ "description": "The right-hand Spec to Zip, will appear later in axes"
+ },
+ "type": {
+ "const": "Zip",
+ "default": "Zip",
+ "enum": [
+ "Zip"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "Zip",
+ "type": "object"
+ }
+ },
+ "$ref": "#/$defs/Concat"
+}
+
If True path through scan will not be modified by squash
+The left-hand Spec to Concat, midpoints will appear earlier
+The right-hand Spec to Concat, midpoints will appear later
+Squash a stack of Frames together into a single expanded Frames object.
+See also
+ +# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.specs import Line, Squash
+
+spec = Squash(Line("y", 1, 2, 3) * Line("x", 0, 1, 4))
+plot_spec(spec)
+
(Source code
, png
, hires.png
, pdf
)
Show JSON schema
{
+ "$defs": {
+ "Circle": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within an xy circle of given radius.\n\n.. example_spec::\n\n from scanspec.regions import Circle\n from scanspec.specs import Line\n\n grid = Line(\"y\", 1, 3, 10) * ~Line(\"x\", 0, 2, 10)\n spec = grid & Circle(\"x\", \"y\", 1, 2, 0.9)",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_middle": {
+ "description": "The central x point of the circle",
+ "title": "X Middle",
+ "type": "number"
+ },
+ "y_middle": {
+ "description": "The central y point of the circle",
+ "title": "Y Middle",
+ "type": "number"
+ },
+ "radius": {
+ "description": "Radius of the circle",
+ "exclusiveMinimum": 0.0,
+ "title": "Radius",
+ "type": "number"
+ },
+ "type": {
+ "const": "Circle",
+ "default": "Circle",
+ "enum": [
+ "Circle"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_middle",
+ "y_middle",
+ "radius"
+ ],
+ "title": "Circle",
+ "type": "object"
+ },
+ "CombinationOf": {
+ "additionalProperties": false,
+ "description": "Abstract baseclass for a combination of two regions, left and right.",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "CombinationOf",
+ "default": "CombinationOf",
+ "enum": [
+ "CombinationOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "CombinationOf",
+ "type": "object"
+ },
+ "Concat": {
+ "additionalProperties": false,
+ "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))",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Spec",
+ "description": "The left-hand Spec to Concat, midpoints will appear earlier"
+ },
+ "right": {
+ "$ref": "#/$defs/Spec",
+ "description": "The right-hand Spec to Concat, midpoints will appear later"
+ },
+ "gap": {
+ "default": false,
+ "description": "If True, force a gap in the output at the join",
+ "title": "Gap",
+ "type": "boolean"
+ },
+ "check_path_changes": {
+ "default": true,
+ "description": "If True path through scan will not be modified by squash",
+ "title": "Check Path Changes",
+ "type": "boolean"
+ },
+ "type": {
+ "const": "Concat",
+ "default": "Concat",
+ "enum": [
+ "Concat"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "Concat",
+ "type": "object"
+ },
+ "DifferenceOf": {
+ "additionalProperties": false,
+ "description": "A point is in DifferenceOf(a, b) if in a and not in b.\n\nTypically created with the ``-`` operator.\n\n>>> r = Range(\"x\", 0.5, 2.5) - Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, False, False, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "DifferenceOf",
+ "default": "DifferenceOf",
+ "enum": [
+ "DifferenceOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "DifferenceOf",
+ "type": "object"
+ },
+ "Ellipse": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within an xy ellipse of given radius.\n\n.. example_spec::\n\n from scanspec.regions import Ellipse\n from scanspec.specs import Line\n\n grid = Line(\"y\", 3, 8, 10) * ~Line(\"x\", 1 ,8, 10)\n spec = grid & Ellipse(\"x\", \"y\", 5, 5, 2, 3, 75)",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_middle": {
+ "description": "The central x point of the ellipse",
+ "title": "X Middle",
+ "type": "number"
+ },
+ "y_middle": {
+ "description": "The central y point of the ellipse",
+ "title": "Y Middle",
+ "type": "number"
+ },
+ "x_radius": {
+ "description": "The radius along the x axis of the ellipse",
+ "exclusiveMinimum": 0.0,
+ "title": "X Radius",
+ "type": "number"
+ },
+ "y_radius": {
+ "description": "The radius along the y axis of the ellipse",
+ "exclusiveMinimum": 0.0,
+ "title": "Y Radius",
+ "type": "number"
+ },
+ "angle": {
+ "default": 0.0,
+ "description": "The angle of the ellipse (degrees)",
+ "title": "Angle",
+ "type": "number"
+ },
+ "type": {
+ "const": "Ellipse",
+ "default": "Ellipse",
+ "enum": [
+ "Ellipse"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_middle",
+ "y_middle",
+ "x_radius",
+ "y_radius"
+ ],
+ "title": "Ellipse",
+ "type": "object"
+ },
+ "IntersectionOf": {
+ "additionalProperties": false,
+ "description": "A point is in IntersectionOf(a, b) if in both a and b.\n\nTypically created with the ``&`` operator.\n\n>>> r = Range(\"x\", 0.5, 2.5) & Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, False, True, False, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "IntersectionOf",
+ "default": "IntersectionOf",
+ "enum": [
+ "IntersectionOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "IntersectionOf",
+ "type": "object"
+ },
+ "Line": {
+ "additionalProperties": false,
+ "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)",
+ "properties": {
+ "axis": {
+ "description": "An identifier for what to move",
+ "title": "Axis"
+ },
+ "start": {
+ "description": "Midpoint of the first point of the line",
+ "title": "Start",
+ "type": "number"
+ },
+ "stop": {
+ "description": "Midpoint of the last point of the line",
+ "title": "Stop",
+ "type": "number"
+ },
+ "num": {
+ "description": "Number of frames to produce",
+ "minimum": 1,
+ "title": "Num",
+ "type": "integer"
+ },
+ "type": {
+ "const": "Line",
+ "default": "Line",
+ "enum": [
+ "Line"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "axis",
+ "start",
+ "stop",
+ "num"
+ ],
+ "title": "Line",
+ "type": "object"
+ },
+ "Mask": {
+ "additionalProperties": false,
+ "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 Frames 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`",
+ "properties": {
+ "spec": {
+ "$ref": "#/$defs/Spec",
+ "description": "The Spec containing the source midpoints"
+ },
+ "region": {
+ "$ref": "#/$defs/Region",
+ "description": "The Region that midpoints will be inside"
+ },
+ "check_path_changes": {
+ "default": true,
+ "description": "If True path through scan will not be modified by squash",
+ "title": "Check Path Changes",
+ "type": "boolean"
+ },
+ "type": {
+ "const": "Mask",
+ "default": "Mask",
+ "enum": [
+ "Mask"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "spec",
+ "region"
+ ],
+ "title": "Mask",
+ "type": "object"
+ },
+ "Polygon": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within a rotated xy polygon.\n\n.. example_spec::\n\n from scanspec.regions import Polygon\n from scanspec.specs import Line\n\n grid = Line(\"y\", 3, 8, 10) * ~Line(\"x\", 1 ,8, 10)\n spec = grid & Polygon(\"x\", \"y\", [1.0, 6.0, 8.0, 2.0], [4.0, 10.0, 6.0, 1.0])",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_verts": {
+ "description": "The Nx1 x coordinates of the polygons vertices",
+ "items": {
+ "type": "number"
+ },
+ "minItems": 3,
+ "title": "X Verts",
+ "type": "array"
+ },
+ "y_verts": {
+ "description": "The Nx1 y coordinates of the polygons vertices",
+ "items": {
+ "type": "number"
+ },
+ "minItems": 3,
+ "title": "Y Verts",
+ "type": "array"
+ },
+ "type": {
+ "const": "Polygon",
+ "default": "Polygon",
+ "enum": [
+ "Polygon"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_verts",
+ "y_verts"
+ ],
+ "title": "Polygon",
+ "type": "object"
+ },
+ "Product": {
+ "additionalProperties": false,
+ "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)",
+ "properties": {
+ "outer": {
+ "$ref": "#/$defs/Spec",
+ "description": "Will be executed once"
+ },
+ "inner": {
+ "$ref": "#/$defs/Spec",
+ "description": "Will be executed len(outer) times"
+ },
+ "type": {
+ "const": "Product",
+ "default": "Product",
+ "enum": [
+ "Product"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "outer",
+ "inner"
+ ],
+ "title": "Product",
+ "type": "object"
+ },
+ "Range": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis >= min and <= max.\n\n>>> r = Range(\"x\", 1, 2)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, True, False, False])",
+ "properties": {
+ "axis": {
+ "description": "The name matching the axis to mask in spec",
+ "title": "Axis"
+ },
+ "min": {
+ "description": "The minimum inclusive value in the region",
+ "title": "Min",
+ "type": "number"
+ },
+ "max": {
+ "description": "The minimum inclusive value in the region",
+ "title": "Max",
+ "type": "number"
+ },
+ "type": {
+ "const": "Range",
+ "default": "Range",
+ "enum": [
+ "Range"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "axis",
+ "min",
+ "max"
+ ],
+ "title": "Range",
+ "type": "object"
+ },
+ "Rectangle": {
+ "additionalProperties": false,
+ "description": "Mask contains points of axis within a rotated xy rectangle.\n\n.. example_spec::\n\n from scanspec.regions import Rectangle\n from scanspec.specs import Line\n\n grid = Line(\"y\", 1, 3, 10) * ~Line(\"x\", 0, 2, 10)\n spec = grid & Rectangle(\"x\", \"y\", 0, 1.1, 1.5, 2.1, 30)",
+ "properties": {
+ "x_axis": {
+ "description": "The name matching the x axis of the spec",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "The name matching the y axis of the spec",
+ "title": "Y Axis"
+ },
+ "x_min": {
+ "description": "Minimum inclusive x value in the region",
+ "title": "X Min",
+ "type": "number"
+ },
+ "y_min": {
+ "description": "Minimum inclusive y value in the region",
+ "title": "Y Min",
+ "type": "number"
+ },
+ "x_max": {
+ "description": "Maximum inclusive x value in the region",
+ "title": "X Max",
+ "type": "number"
+ },
+ "y_max": {
+ "description": "Maximum inclusive y value in the region",
+ "title": "Y Max",
+ "type": "number"
+ },
+ "angle": {
+ "default": 0.0,
+ "description": "Clockwise rotation angle of the rectangle",
+ "title": "Angle",
+ "type": "number"
+ },
+ "type": {
+ "const": "Rectangle",
+ "default": "Rectangle",
+ "enum": [
+ "Rectangle"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_min",
+ "y_min",
+ "x_max",
+ "y_max"
+ ],
+ "title": "Rectangle",
+ "type": "object"
+ },
+ "Region": {
+ "discriminator": {
+ "mapping": {
+ "Circle": "#/$defs/Circle",
+ "CombinationOf": "#/$defs/CombinationOf",
+ "DifferenceOf": "#/$defs/DifferenceOf",
+ "Ellipse": "#/$defs/Ellipse",
+ "IntersectionOf": "#/$defs/IntersectionOf",
+ "Polygon": "#/$defs/Polygon",
+ "Range": "#/$defs/Range",
+ "Rectangle": "#/$defs/Rectangle",
+ "SymmetricDifferenceOf": "#/$defs/SymmetricDifferenceOf",
+ "UnionOf": "#/$defs/UnionOf"
+ },
+ "propertyName": "type"
+ },
+ "oneOf": [
+ {
+ "$ref": "#/$defs/CombinationOf"
+ },
+ {
+ "$ref": "#/$defs/UnionOf"
+ },
+ {
+ "$ref": "#/$defs/IntersectionOf"
+ },
+ {
+ "$ref": "#/$defs/DifferenceOf"
+ },
+ {
+ "$ref": "#/$defs/SymmetricDifferenceOf"
+ },
+ {
+ "$ref": "#/$defs/Range"
+ },
+ {
+ "$ref": "#/$defs/Rectangle"
+ },
+ {
+ "$ref": "#/$defs/Polygon"
+ },
+ {
+ "$ref": "#/$defs/Circle"
+ },
+ {
+ "$ref": "#/$defs/Ellipse"
+ }
+ ]
+ },
+ "Repeat": {
+ "additionalProperties": false,
+ "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",
+ "properties": {
+ "num": {
+ "description": "Number of frames to produce",
+ "minimum": 1,
+ "title": "Num",
+ "type": "integer"
+ },
+ "gap": {
+ "default": true,
+ "description": "If False and the slowest of the stack of Frames is snaked then the end and start of consecutive iterations of Spec will have no gap",
+ "title": "Gap",
+ "type": "boolean"
+ },
+ "type": {
+ "const": "Repeat",
+ "default": "Repeat",
+ "enum": [
+ "Repeat"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "num"
+ ],
+ "title": "Repeat",
+ "type": "object"
+ },
+ "Snake": {
+ "additionalProperties": false,
+ "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)",
+ "properties": {
+ "spec": {
+ "$ref": "#/$defs/Spec",
+ "description": "The Spec to run in reverse every other iteration"
+ },
+ "type": {
+ "const": "Snake",
+ "default": "Snake",
+ "enum": [
+ "Snake"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "spec"
+ ],
+ "title": "Snake",
+ "type": "object"
+ },
+ "Spec": {
+ "discriminator": {
+ "mapping": {
+ "Concat": "#/$defs/Concat",
+ "Line": "#/$defs/Line",
+ "Mask": "#/$defs/Mask",
+ "Product": "#/$defs/Product",
+ "Repeat": "#/$defs/Repeat",
+ "Snake": "#/$defs/Snake",
+ "Spiral": "#/$defs/Spiral",
+ "Squash": "#/$defs/Squash",
+ "Static": "#/$defs/Static",
+ "Zip": "#/$defs/Zip"
+ },
+ "propertyName": "type"
+ },
+ "oneOf": [
+ {
+ "$ref": "#/$defs/Product"
+ },
+ {
+ "$ref": "#/$defs/Repeat"
+ },
+ {
+ "$ref": "#/$defs/Zip"
+ },
+ {
+ "$ref": "#/$defs/Mask"
+ },
+ {
+ "$ref": "#/$defs/Snake"
+ },
+ {
+ "$ref": "#/$defs/Concat"
+ },
+ {
+ "$ref": "#/$defs/Squash"
+ },
+ {
+ "$ref": "#/$defs/Line"
+ },
+ {
+ "$ref": "#/$defs/Static"
+ },
+ {
+ "$ref": "#/$defs/Spiral"
+ }
+ ]
+ },
+ "Spiral": {
+ "additionalProperties": false,
+ "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)",
+ "properties": {
+ "x_axis": {
+ "description": "An identifier for what to move for x",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "An identifier for what to move for y",
+ "title": "Y Axis"
+ },
+ "x_start": {
+ "description": "x centre of the spiral",
+ "title": "X Start",
+ "type": "number"
+ },
+ "y_start": {
+ "description": "y centre of the spiral",
+ "title": "Y Start",
+ "type": "number"
+ },
+ "x_range": {
+ "description": "x width of the spiral",
+ "title": "X Range",
+ "type": "number"
+ },
+ "y_range": {
+ "description": "y width of the spiral",
+ "title": "Y Range",
+ "type": "number"
+ },
+ "num": {
+ "description": "Number of frames to produce",
+ "minimum": 1,
+ "title": "Num",
+ "type": "integer"
+ },
+ "rotate": {
+ "default": 0.0,
+ "description": "How much to rotate the angle of the spiral",
+ "title": "Rotate",
+ "type": "number"
+ },
+ "type": {
+ "const": "Spiral",
+ "default": "Spiral",
+ "enum": [
+ "Spiral"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_start",
+ "y_start",
+ "x_range",
+ "y_range",
+ "num"
+ ],
+ "title": "Spiral",
+ "type": "object"
+ },
+ "Squash": {
+ "additionalProperties": false,
+ "description": "Squash a stack of Frames together into a single expanded Frames 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))",
+ "properties": {
+ "spec": {
+ "$ref": "#/$defs/Spec",
+ "description": "The Spec to squash the dimensions of"
+ },
+ "check_path_changes": {
+ "default": true,
+ "description": "If True path through scan will not be modified by squash",
+ "title": "Check Path Changes",
+ "type": "boolean"
+ },
+ "type": {
+ "const": "Squash",
+ "default": "Squash",
+ "enum": [
+ "Squash"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "spec"
+ ],
+ "title": "Squash",
+ "type": "object"
+ },
+ "Static": {
+ "additionalProperties": false,
+ "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))",
+ "properties": {
+ "axis": {
+ "description": "An identifier for what to move",
+ "title": "Axis"
+ },
+ "value": {
+ "description": "The value at each point",
+ "title": "Value",
+ "type": "number"
+ },
+ "num": {
+ "default": 1,
+ "description": "Number of frames to produce",
+ "minimum": 1,
+ "title": "Num",
+ "type": "integer"
+ },
+ "type": {
+ "const": "Static",
+ "default": "Static",
+ "enum": [
+ "Static"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "axis",
+ "value"
+ ],
+ "title": "Static",
+ "type": "object"
+ },
+ "SymmetricDifferenceOf": {
+ "additionalProperties": false,
+ "description": "A point is in SymmetricDifferenceOf(a, b) if in either a or b, but not both.\n\nTypically created with the ``^`` operator.\n\n>>> r = Range(\"x\", 0.5, 2.5) ^ Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, False, True, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "SymmetricDifferenceOf",
+ "default": "SymmetricDifferenceOf",
+ "enum": [
+ "SymmetricDifferenceOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "SymmetricDifferenceOf",
+ "type": "object"
+ },
+ "UnionOf": {
+ "additionalProperties": false,
+ "description": "A point is in UnionOf(a, b) if in either a or b.\n\nTypically created with the ``|`` operator\n\n>>> r = Range(\"x\", 0.5, 2.5) | Range(\"x\", 1.5, 3.5)\n>>> r.mask({\"x\": np.array([0, 1, 2, 3, 4])})\narray([False, True, True, True, False])",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Region",
+ "description": "The left-hand Region to combine"
+ },
+ "right": {
+ "$ref": "#/$defs/Region",
+ "description": "The right-hand Region to combine"
+ },
+ "type": {
+ "const": "UnionOf",
+ "default": "UnionOf",
+ "enum": [
+ "UnionOf"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "UnionOf",
+ "type": "object"
+ },
+ "Zip": {
+ "additionalProperties": false,
+ "description": "Run two Specs in parallel, merging their midpoints together.\n\nTypically formed using `Spec.zip`.\n\nStacks of Frames are merged by:\n\n- If right creates a stack of a single Frames object of size 1, expand it to\n the size of the fastest Frames object created by left\n- Merge individual Frames 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))",
+ "properties": {
+ "left": {
+ "$ref": "#/$defs/Spec",
+ "description": "The left-hand Spec to Zip, will appear earlier in axes"
+ },
+ "right": {
+ "$ref": "#/$defs/Spec",
+ "description": "The right-hand Spec to Zip, will appear later in axes"
+ },
+ "type": {
+ "const": "Zip",
+ "default": "Zip",
+ "enum": [
+ "Zip"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "required": [
+ "left",
+ "right"
+ ],
+ "title": "Zip",
+ "type": "object"
+ }
+ },
+ "$ref": "#/$defs/Squash"
+}
+
If True path through scan will not be modified by squash
+Linearly spaced frames with start and stop as first and last midpoints.
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.specs import Line
+
+spec = Line("x", 1, 2, 5)
+plot_spec(spec)
+
(Source code
, png
, hires.png
, pdf
)
Show JSON schema
{
+ "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)",
+ "type": "object",
+ "properties": {
+ "axis": {
+ "description": "An identifier for what to move",
+ "title": "Axis"
+ },
+ "start": {
+ "description": "Midpoint of the first point of the line",
+ "title": "Start",
+ "type": "number"
+ },
+ "stop": {
+ "description": "Midpoint of the last point of the line",
+ "title": "Stop",
+ "type": "number"
+ },
+ "num": {
+ "description": "Number of frames to produce",
+ "minimum": 1,
+ "title": "Num",
+ "type": "integer"
+ },
+ "type": {
+ "const": "Line",
+ "default": "Line",
+ "enum": [
+ "Line"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "axis",
+ "start",
+ "stop",
+ "num"
+ ]
+}
+
Return the list of axes that are present in the scan.
+Ordered from slowest moving to fastest moving.
+Specify a Line by extreme bounds instead of midpoints.
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.specs import Line
+
+spec = Line.bounded("x", 1, 2, 5)
+plot_spec(spec)
+
(Source code
, png
, hires.png
, pdf
)
A static frame, repeated num times, with axis at value.
+Can be used to set axis=value at every point in a scan.
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.specs import Line, Static
+
+spec = Line("y", 1, 2, 3).zip(Static("x", 3))
+plot_spec(spec)
+
(Source code
, png
, hires.png
, pdf
)
Show JSON schema
{
+ "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))",
+ "type": "object",
+ "properties": {
+ "axis": {
+ "description": "An identifier for what to move",
+ "title": "Axis"
+ },
+ "value": {
+ "description": "The value at each point",
+ "title": "Value",
+ "type": "number"
+ },
+ "num": {
+ "default": 1,
+ "description": "Number of frames to produce",
+ "minimum": 1,
+ "title": "Num",
+ "type": "integer"
+ },
+ "type": {
+ "const": "Static",
+ "default": "Static",
+ "enum": [
+ "Static"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "axis",
+ "value"
+ ]
+}
+
Return the list of axes that are present in the scan.
+Ordered from slowest moving to fastest moving.
+Produce a stack of nested Frames
that form the scan.
Ordered from slowest moving to fastest moving.
+A static spec with no motion, only a duration repeated “num” times.
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.specs import Line, Static
+
+spec = Line("y", 1, 2, 3).zip(Static.duration(0.1))
+plot_spec(spec)
+
(Source code
, png
, hires.png
, pdf
)
Archimedean spiral of “x_axis” and “y_axis”.
+Starts at centre point (“x_start”, “y_start”) with angle “rotate”. Produces +“num” points in a spiral spanning width of “x_range” and height of “y_range”
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.specs import Spiral
+
+spec = Spiral("x", "y", 1, 5, 10, 50, 30)
+plot_spec(spec)
+
(Source code
, png
, hires.png
, pdf
)
Show JSON schema
{
+ "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)",
+ "type": "object",
+ "properties": {
+ "x_axis": {
+ "description": "An identifier for what to move for x",
+ "title": "X Axis"
+ },
+ "y_axis": {
+ "description": "An identifier for what to move for y",
+ "title": "Y Axis"
+ },
+ "x_start": {
+ "description": "x centre of the spiral",
+ "title": "X Start",
+ "type": "number"
+ },
+ "y_start": {
+ "description": "y centre of the spiral",
+ "title": "Y Start",
+ "type": "number"
+ },
+ "x_range": {
+ "description": "x width of the spiral",
+ "title": "X Range",
+ "type": "number"
+ },
+ "y_range": {
+ "description": "y width of the spiral",
+ "title": "Y Range",
+ "type": "number"
+ },
+ "num": {
+ "description": "Number of frames to produce",
+ "minimum": 1,
+ "title": "Num",
+ "type": "integer"
+ },
+ "rotate": {
+ "default": 0.0,
+ "description": "How much to rotate the angle of the spiral",
+ "title": "Rotate",
+ "type": "number"
+ },
+ "type": {
+ "const": "Spiral",
+ "default": "Spiral",
+ "enum": [
+ "Spiral"
+ ],
+ "title": "Type",
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "x_axis",
+ "y_axis",
+ "x_start",
+ "y_start",
+ "x_range",
+ "y_range",
+ "num"
+ ]
+}
+
Return the list of axes that are present in the scan.
+Ordered from slowest moving to fastest moving.
+Produce a stack of nested Frames
that form the scan.
Ordered from slowest moving to fastest moving.
+Specify a Spiral equally spaced in “x_axis” and “y_axis”.
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.specs import Spiral
+
+spec = Spiral.spaced("x", "y", 0, 0, 10, 3)
+plot_spec(spec)
+
(Source code
, png
, hires.png
, pdf
)
Flyscan, zipping with fixed duration for every frame.
+spec – The source Spec
to continuously move
duration – How long to spend at each frame in the spec
# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.specs import Line, fly
+
+spec = fly(Line("x", 1, 2, 3), 0.1)
+plot_spec(spec)
+
(Source code
, png
, hires.png
, pdf
)
Step scan, with num frames of given duration at each frame in the spec.
+spec – The source Spec
with midpoints to move to and stop
duration – The duration of each scan frame
num – Number of frames to produce with given duration at each of frame +in the spec
# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.specs import Line, step
+
+spec = step(Line("x", 1, 2, 3), 0.1)
+plot_spec(spec)
+
(Source code
, png
, hires.png
, pdf
)
+"""Core classes like `Frames` and `Path`."""
+
+from __future__ import annotations
+
+import itertools
+from collections.abc import Callable, Iterable, Iterator, Sequence
+from functools import lru_cache
+from inspect import isclass
+from typing import (
+ Any,
+ Generic,
+ Literal,
+ TypeVar,
+)
+
+import numpy as np
+import numpy.typing as npt
+from pydantic import BaseModel, ConfigDict, Field, GetCoreSchemaHandler
+from pydantic.dataclasses import is_pydantic_dataclass, rebuild_dataclass
+from pydantic_core import CoreSchema
+from pydantic_core.core_schema import tagged_union_schema
+
+__all__ = [
+ "Axis",
+ "OtherAxis",
+ "if_instance_do",
+ "AxesPoints",
+ "Frames",
+ "SnakedFrames",
+ "gap_between_frames",
+ "squash_frames",
+ "Path",
+ "Midpoints",
+ "discriminated_union_of_subclasses",
+ "StrictConfig",
+]
+
+#: Used to ensure pydantic dataclasses error if given extra arguments
+StrictConfig: ConfigDict = {"extra": "forbid"}
+
+C = TypeVar("C")
+T = TypeVar("T")
+
+GapArray = npt.NDArray[np.bool]
+
+
+
+[docs]
+def discriminated_union_of_subclasses(
+ super_cls: type[C],
+ discriminator: str = "type",
+) -> type[C]:
+ """Add all subclasses of super_cls to a discriminated union.
+
+ For all subclasses of super_cls, add a discriminator field to identify
+ the type. Raw JSON should look like {<discriminator>: <type name>, params for
+ <type name>...}.
+
+ Subclasses that extend this class must be Pydantic dataclasses, and types that
+ need their schema to be updated when a new type that extends super_cls is
+ created must be either Pydantic dataclasses or BaseModels.
+
+ Example::
+
+ @discriminated_union_of_subclasses
+ class Expression(ABC):
+ @abstractmethod
+ def calculate(self) -> int:
+ ...
+
+
+ @dataclass
+ class Add(Expression):
+ left: Expression
+ right: Expression
+
+ def calculate(self) -> int:
+ return self.left.calculate() + self.right.calculate()
+
+
+ @dataclass
+ class Subtract(Expression):
+ left: Expression
+ right: Expression
+
+ def calculate(self) -> int:
+ return self.left.calculate() - self.right.calculate()
+
+
+ @dataclass
+ class IntLiteral(Expression):
+ value: int
+
+ def calculate(self) -> int:
+ return self.value
+
+
+ my_sum = Add(IntLiteral(5), Subtract(IntLiteral(10), IntLiteral(2)))
+ assert my_sum.calculate() == 13
+
+ assert my_sum == parse_obj_as(
+ Expression,
+ {
+ "type": "Add",
+ "left": {"type": "IntLiteral", "value": 5},
+ "right": {
+ "type": "Subtract",
+ "left": {"type": "IntLiteral", "value": 10},
+ "right": {"type": "IntLiteral", "value": 2},
+ },
+ },
+ )
+
+ Args:
+ super_cls: The superclass of the union, Expression in the above example
+ discriminator: The discriminator that will be inserted into the
+ serialized documents for type determination. Defaults to "type".
+
+ Returns:
+ Type: decorated superclass with handling for subclasses to be added
+ to its discriminated union for deserialization
+
+ """
+ tagged_union = _TaggedUnion(super_cls, discriminator)
+ _tagged_unions[super_cls] = tagged_union
+
+ def add_subclass_to_union(subclass: type[C]):
+ # Add a discriminator field to a subclass so it can
+ # be identified when deserializing
+ subclass.__annotations__ = {
+ **subclass.__annotations__,
+ discriminator: Literal[subclass.__name__], # type: ignore
+ }
+ setattr(subclass, discriminator, Field(subclass.__name__, repr=False)) # type: ignore
+
+ def get_schema_of_union(
+ cls: type[C], source_type: Any, handler: GetCoreSchemaHandler
+ ):
+ if cls is not super_cls:
+ tagged_union.add_member(cls)
+ return handler(cls)
+ # Rebuild any dataclass (including this one) that references this union
+ # Note that this has to be done after the creation of the dataclass so that
+ # previously created classes can refer to this newly created class
+ return tagged_union.schema(handler)
+
+ super_cls.__init_subclass__ = classmethod(add_subclass_to_union) # type: ignore
+ super_cls.__get_pydantic_core_schema__ = classmethod(get_schema_of_union) # type: ignore
+ return super_cls
+
+
+
+_tagged_unions: dict[type, _TaggedUnion] = {}
+
+
+class _TaggedUnion:
+ def __init__(self, base_class: type[Any], discriminator: str):
+ self._base_class = base_class
+ # Classes and their field names that refer to this tagged union
+ self._discriminator = discriminator
+ # The members of the tagged union, i.e. subclasses of the baseclass
+ self._subclasses: list[type] = []
+
+ def add_member(self, cls: type):
+ if cls in self._subclasses:
+ return
+ self._subclasses.append(cls)
+ for member in self._subclasses:
+ if member is not cls:
+ _TaggedUnion._rebuild(member)
+
+ @staticmethod
+ def _rebuild(cls_or_func: Callable[..., T]) -> None:
+ if isclass(cls_or_func):
+ if is_pydantic_dataclass(cls_or_func):
+ rebuild_dataclass(cls_or_func, force=True)
+ if issubclass(cls_or_func, BaseModel):
+ cls_or_func.model_rebuild(force=True)
+
+ def schema(self, handler: GetCoreSchemaHandler) -> CoreSchema:
+ return tagged_union_schema(
+ _make_schema(tuple(self._subclasses), handler),
+ discriminator=self._discriminator,
+ ref=self._base_class.__name__,
+ )
+
+
+@lru_cache(1)
+def _make_schema(
+ members: tuple[type[Any], ...], handler: Callable[[Any], CoreSchema]
+) -> dict[str, CoreSchema]:
+ return {member.__name__: handler(member) for member in members}
+
+
+
+[docs]
+def if_instance_do(x: C, cls: type[C], 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.
+ """
+ if isinstance(x, cls):
+ return func(x)
+ else:
+ return NotImplemented
+
+
+
+#: A type variable for an `axis_` that can be specified for a scan
+Axis = TypeVar("Axis", covariant=True)
+
+#: Alternative axis variable to be used when two are required in the same type binding
+OtherAxis = TypeVar("OtherAxis")
+
+#: Map of axes to float ndarray of points
+#: E.g. {xmotor: array([0, 1, 2]), ymotor: array([2, 2, 2])}
+AxesPoints = dict[Axis, npt.NDArray[np.floating[Any]]]
+
+
+
+[docs]
+class Frames(Generic[Axis]):
+ """Represents a series of scan frames along a number of axes.
+
+ During a scan each axis will traverse lower-midpoint-upper for each frame.
+
+ Args:
+ midpoints: The midpoints of scan frames for each axis
+ lower: Lower bounds of scan frames if different from midpoints
+ upper: Upper bounds of scan frames if different from midpoints
+ gap: If supplied, define if there is a gap between frame and previous
+ otherwise it is calculated by looking at lower and upper bounds
+
+ Typically used in two ways:
+
+ - A list of Frames objects returned from `Spec.calculate` represents a scan
+ as a linear stack of frames. Interpreted as nested from slowest moving to
+ fastest moving, so each faster Frames object will iterate once per
+ position of the slower Frames object. It is passed to a `Path` for
+ calculation of the actual scan path.
+ - A single Frames object returned from `Path.consume` represents a chunk of
+ frames forming part of a scan path, for interpretation by the code
+ that will actually perform the scan.
+
+ See Also:
+ `technical-terms`
+
+ """
+
+ def __init__(
+ self,
+ midpoints: AxesPoints[Axis],
+ lower: AxesPoints[Axis] | None = None,
+ upper: AxesPoints[Axis] | None = None,
+ gap: GapArray | None = None,
+ ):
+ #: The midpoints of scan frames for each axis
+ self.midpoints = midpoints
+ #: The lower bounds of each scan frame in each axis for fly-scanning
+ self.lower = lower or midpoints
+ #: The upper bounds of each scan frame in each axis for fly-scanning
+ self.upper = upper or midpoints
+ if gap is not None:
+ #: Whether there is a gap between this frame and the previous. First
+ #: element is whether there is a gap between the last frame and the first
+ self.gap = gap
+ else:
+ # Need to calculate gap as not passed one
+ # We have a gap if upper[i] != lower[i+1] for any axes
+ axes_gap = [
+ np.roll(upper, 1) != lower
+ for upper, lower in zip(
+ self.upper.values(), self.lower.values(), strict=False
+ )
+ ]
+ self.gap = np.logical_or.reduce(axes_gap)
+ # Check all axes and ordering are the same
+ assert list(self.midpoints) == list(self.lower) == list(self.upper), (
+ f"Mismatching axes "
+ f"{list(self.midpoints)} != {list(self.lower)} != {list(self.upper)}"
+ )
+ # Check all lengths are the same
+ lengths = {
+ len(arr)
+ for d in (self.midpoints, self.lower, self.upper)
+ for arr in d.values()
+ }
+ lengths.add(len(self.gap))
+ assert len(lengths) <= 1, f"Mismatching lengths {list(lengths)}"
+
+
+[docs]
+ def axes(self) -> list[Axis]:
+ """The axes which will move during the scan.
+
+ These will be present in `midpoints`, `lower` and `upper`.
+ """
+ return list(self.midpoints.keys())
+
+
+ def __len__(self) -> int:
+ """The number of frames in this section of the scan."""
+ # All axespoints arrays are same length, pick the first one
+ return len(self.gap)
+
+
+[docs]
+ def extract(
+ self, indices: npt.NDArray[np.signedinteger[Any]], calculate_gap: bool = True
+ ) -> Frames[Axis]:
+ """Return a new Frames object restricted to the indices provided.
+
+ Args:
+ indices: The indices of the frames to extract, modulo scan length
+ calculate_gap: If True then recalculate the gap from upper and lower
+
+ >>> frames = Frames({"x": np.array([1, 2, 3])})
+ >>> frames.extract(np.array([1, 0, 1])).midpoints
+ {'x': array([2, 1, 2])}
+
+ """
+ dim_indices = indices % len(self)
+
+ def extract_dict(ds: Iterable[AxesPoints[Axis]]) -> AxesPoints[Axis]:
+ for d in ds:
+ return {k: v[dim_indices] for k, v in d.items()}
+ return {}
+
+ def extract_gap(gaps: Iterable[GapArray]) -> GapArray | None:
+ for gap in gaps:
+ if not calculate_gap:
+ return gap[dim_indices]
+ return None
+
+ return _merge_frames(self, dict_merge=extract_dict, gap_merge=extract_gap)
+
+
+
+[docs]
+ def concat(self, other: Frames[Axis], gap: bool = False) -> Frames[Axis]:
+ """Return a new Frames object concatenating self and other.
+
+ Requires both Frames objects to have the same axes, but not necessarily in
+ the same order. The order is inherited from self, so other may be reordered.
+
+ Args:
+ other: The Frames to concatenate to self
+ gap: Whether to force a gap between the two Frames objects
+
+ >>> frames = Frames({"x": np.array([1, 2, 3]), "y": np.array([6, 5, 4])})
+ >>> frames2 = Frames({"y": np.array([3, 2, 1]), "x": np.array([4, 5, 6])})
+ >>> frames.concat(frames2).midpoints
+ {'x': array([1, 2, 3, 4, 5, 6]), 'y': array([6, 5, 4, 3, 2, 1])}
+
+ """
+ assert set(self.axes()) == set(
+ other.axes()
+ ), f"axes {self.axes()} != {other.axes()}"
+
+ def concat_dict(ds: Sequence[AxesPoints[Axis]]) -> AxesPoints[Axis]:
+ # Concat each array in midpoints, lower, upper. E.g.
+ # lower[ax] = np.concatenate(self.lower[ax], other.lower[ax])
+ return {a: np.concatenate([d[a] for d in ds]) for a in self.axes()}
+
+ def concat_gap(gaps: Sequence[GapArray]) -> GapArray:
+ g = np.concatenate(gaps)
+ # Calc the first frame
+ g[0] = gap_between_frames(other, self)
+ # And the join frame
+ g[len(self)] = gap or gap_between_frames(self, other)
+ return g
+
+ return _merge_frames(self, other, dict_merge=concat_dict, gap_merge=concat_gap)
+
+
+
+[docs]
+ def zip(self, other: Frames[Axis]) -> Frames[Axis]:
+ """Return a new Frames object merging self and other.
+
+ Require both Frames objects to not share axes.
+
+ >>> fx = Frames({"x": np.array([1, 2, 3])})
+ >>> fy = Frames({"y": np.array([5, 6, 7])})
+ >>> fx.zip(fy).midpoints
+ {'x': array([1, 2, 3]), 'y': array([5, 6, 7])}
+ """
+ overlapping = list(set(self.axes()).intersection(other.axes()))
+ assert not overlapping, f"Zipping would overwrite axes {overlapping}"
+
+ def zip_dict(ds: Sequence[AxesPoints[Axis]]) -> AxesPoints[Axis]:
+ # Merge dicts for midpoints, lower, upper. E.g.
+ # lower[ax] = {**self.lower[ax], **other.lower[ax]}
+ return dict(kv for d in ds for kv in d.items())
+
+ def zip_gap(gaps: Sequence[GapArray]) -> GapArray:
+ # Gap if either frames has a gap. E.g.
+ # gap[i] = self.gap[i] | other.gap[i]
+ return np.logical_or.reduce(gaps)
+
+ return _merge_frames(self, other, dict_merge=zip_dict, gap_merge=zip_gap)
+
+
+
+
+def _merge_frames(
+ *stack: Frames[Axis],
+ dict_merge: Callable[[Sequence[AxesPoints[Axis]]], AxesPoints[Axis]], # type: ignore
+ gap_merge: Callable[[Sequence[GapArray]], GapArray | None],
+) -> Frames[Axis]:
+ types = {type(fs) for fs in stack}
+ assert len(types) == 1, f"Mismatching types for {stack}"
+ cls = types.pop()
+
+ # Apply to midpoints, force calculation of gap
+ return cls(
+ midpoints=dict_merge([fs.midpoints for fs in stack]),
+ gap=gap_merge([fs.gap for fs in stack]),
+ # If any lower or upper are different, apply to those
+ lower=dict_merge([fs.lower for fs in stack])
+ if any(fs.midpoints is not fs.lower for fs in stack)
+ else None,
+ upper=dict_merge([fs.upper for fs in stack])
+ if any(fs.midpoints is not fs.upper for fs in stack)
+ else None,
+ )
+
+
+
+[docs]
+class SnakedFrames(Frames[Axis]):
+ """Like a `Frames` object, but each alternate repetition will run in reverse."""
+
+ def __init__(
+ self,
+ midpoints: AxesPoints[Axis],
+ lower: AxesPoints[Axis] | None = None,
+ upper: AxesPoints[Axis] | None = None,
+ gap: GapArray | None = None,
+ ):
+ super().__init__(midpoints, lower=lower, upper=upper, gap=gap)
+ # Override first element of gap to be True, as subsequent runs
+ # of snake scans are always joined end -> start
+ self.gap[0] = False
+
+
+[docs]
+ @classmethod
+ def from_frames(
+ cls: type[SnakedFrames[Any]], frames: Frames[OtherAxis]
+ ) -> SnakedFrames[OtherAxis]:
+ """Create a snaked version of a `Frames` object."""
+ return cls(frames.midpoints, frames.lower, frames.upper, frames.gap)
+
+
+
+[docs]
+ def extract(
+ self, indices: npt.NDArray[np.signedinteger[Any]], calculate_gap: bool = True
+ ) -> Frames[Axis]:
+ """Return a new Frames object restricted to the indices provided.
+
+ Args:
+ indices: The indices of the frames to extract, can extend past len(self)
+ calculate_gap: If True then recalculate the gap from upper and lower
+
+ >>> frames = SnakedFrames({"x": np.array([1, 2, 3])})
+ >>> frames.extract(np.array([0, 1, 2, 3, 4, 5])).midpoints
+ {'x': array([1, 2, 3, 3, 2, 1])}
+
+ """
+ # Calculate the indices
+ # E.g for len = 4
+ # indices: 0123456789
+ # backwards: 0000111100
+ # snake_indices: 0123321001
+ # gap_indices: 0123032101
+ length = len(self)
+ backwards = (indices // length) % 2
+ snake_indices = np.where(backwards, (length - 1) - indices, indices) % length
+ cls: type[Frames[Any]]
+ if not calculate_gap:
+ cls = Frames
+ gap = self.gap[np.where(backwards, length - indices, indices) % length]
+ else:
+ cls = type(self)
+ gap = None
+
+ # Apply to midpoints
+ return cls(
+ {k: v[snake_indices] for k, v in self.midpoints.items()},
+ gap=gap,
+ # If lower or upper are different, apply to those
+ lower={
+ k: np.where(backwards, self.upper[k][snake_indices], v[snake_indices])
+ for k, v in self.lower.items()
+ }
+ if self.midpoints is not self.lower
+ else None,
+ upper={
+ k: np.where(backwards, self.lower[k][snake_indices], v[snake_indices])
+ for k, v in self.upper.items()
+ }
+ if self.midpoints is not self.upper
+ else None,
+ )
+
+
+
+
+
+[docs]
+def gap_between_frames(frames1: Frames[Axis], frames2: Frames[Axis]) -> bool:
+ """Is there a gap between end of frames1 and start of frames2."""
+ return any(frames1.upper[a][-1] != frames2.lower[a][0] for a in frames1.axes())
+
+
+
+
+[docs]
+def squash_frames(
+ stack: list[Frames[Axis]], check_path_changes: bool = True
+) -> Frames[Axis]:
+ """Squash a stack of nested Frames into a single one.
+
+ Args:
+ stack: The Frames stack to squash, from slowest to fastest moving
+ check_path_changes: If True then check that nesting the output
+ Frames object within others will provide the same path
+ as nesting the input Frames stack within others
+
+ See Also:
+ `why-squash-can-change-path`
+
+ >>> fx = SnakedFrames({"x": np.array([1, 2])})
+ >>> fy = Frames({"y": np.array([3, 4])})
+ >>> squash_frames([fy, fx]).midpoints
+ {'y': array([3, 3, 4, 4]), 'x': array([1, 2, 2, 1])}
+
+ """
+ path = Path(stack)
+ # Consuming a Path through these Frames performs the squash
+ squashed = path.consume()
+ # Check that the squash is the same as the original
+ if stack and isinstance(stack[0], SnakedFrames):
+ squashed = SnakedFrames.from_frames(squashed)
+ # The top level is snaking, so this Frames object will run backwards
+ # This means any non-snaking axes will run backwards, which is
+ # surprising, so don't allow it
+ if check_path_changes:
+ non_snaking = [
+ k for d in stack for k in d.axes() if not isinstance(d, SnakedFrames)
+ ]
+ if non_snaking:
+ raise ValueError(
+ f"Cannot squash non-snaking Frames inside a SnakingFrames "
+ f"otherwise {non_snaking} would run backwards"
+ )
+ elif check_path_changes:
+ # The top level is not snaking, so make sure there is an even
+ # number of iterations of any snaking axis within it so it
+ # doesn't jump when this frames object is iterated a second time
+ for i, frames in enumerate(stack):
+ # A SnakedFrames within a non-snaking top level must repeat
+ # an even number of times
+ if isinstance(frames, SnakedFrames) and np.prod(path.lengths[:i]) % 2:
+ raise ValueError(
+ f"Cannot squash SnakingFrames inside a non-snaking Frames "
+ f"when they do not repeat an even number of times "
+ f"otherwise {frames.axes()} would jump in position"
+ )
+ return squashed
+
+
+
+
+[docs]
+class Path(Generic[Axis]):
+ """A consumable route through a stack of Frames, representing a scan path.
+
+ Args:
+ stack: The Frames stack describing the scan, from slowest to fastest
+ moving
+ start: The index of where in the Path to start
+ num: The number of scan frames to produce after start. None means up to
+ the end
+
+ See Also:
+ `iterate-a-spec`
+
+ """
+
+ def __init__(
+ self, stack: list[Frames[Axis]], start: int = 0, num: int | None = None
+ ):
+ #: The Frames stack describing the scan, from slowest to fastest moving
+ self.stack = stack
+ #: Index that is next to be consumed
+ self.index = start
+ #: The lengths of all the stack
+ self.lengths = np.array([len(f) for f in stack])
+ #: Index of the end frame, one more than the last index that will be
+ #: produced
+ self.end_index = int(np.prod(self.lengths))
+ if num is not None and start + num < self.end_index:
+ self.end_index = start + num
+
+
+[docs]
+ def consume(self, num: int | None = None) -> Frames[Axis]:
+ """Consume at most num frames from the Path and return as a Frames object.
+
+ >>> fx = SnakedFrames({"x": np.array([1, 2])})
+ >>> fy = Frames({"y": np.array([3, 4])})
+ >>> path = Path([fy, fx])
+ >>> path.consume(3).midpoints
+ {'y': array([3, 3, 4]), 'x': array([1, 2, 2])}
+ >>> path.consume(3).midpoints
+ {'y': array([4]), 'x': array([1])}
+ >>> path.consume(3).midpoints
+ {'y': array([], dtype=int64), 'x': array([], dtype=int64)}
+ """
+ if num is None:
+ end_index = self.end_index
+ else:
+ end_index = min(self.index + num, self.end_index)
+ indices = np.arange(self.index, end_index)
+ self.index = end_index
+ stack: Frames[Axis] = Frames(
+ {}, {}, {}, np.zeros(indices.shape, dtype=np.bool_)
+ )
+ # Example numbers below from a 2x3x4 ZxYxX scan
+ for i, frames in enumerate(self.stack):
+ # Number of times each frame will repeat: Z:12, Y:4, X:1
+ repeats = np.prod(self.lengths[i + 1 :])
+ # Scan indices mapped to indices within Frames object:
+ # Z:000000000000111111111111
+ # Y:000011112222000011112222
+ # X:012301230123012301230123
+ if repeats > 1:
+ dim_indices = indices // repeats
+ else:
+ dim_indices = indices
+ # Create the sliced frames
+ sliced = frames.extract(dim_indices, calculate_gap=False)
+ if repeats > 1:
+ # Whether this frames contributes to the gap bit
+ # Z:000000000000100000000000
+ # Y:000010001000100010001000
+ # X:111111111111111111111111
+ in_gap = (indices % repeats) == 0
+ # If in_gap, then keep the relevant gap bit
+ sliced.gap &= in_gap
+ # Zip it with the output Frames object
+ stack = stack.zip(sliced)
+ return stack
+
+
+ def __len__(self) -> int:
+ """Number of frames left in a scan, reduces when `consume` is called."""
+ return self.end_index - self.index
+
+
+
+
+[docs]
+class Midpoints(Generic[Axis]):
+ """Convenience iterable that produces the scan midpoints for each axis.
+
+ For better performance, consume from a `Path` instead.
+
+ Args:
+ stack: The stack of Frames describing the scan, from slowest to fastest
+ moving
+
+ See Also:
+ `iterate-a-spec`
+
+ >>> fx = SnakedFrames({"x": np.array([1, 2])})
+ >>> fy = Frames({"y": np.array([3, 4])})
+ >>> mp = Midpoints([fy, fx])
+ >>> for p in mp: print(p)
+ {'y': np.int64(3), 'x': np.int64(1)}
+ {'y': np.int64(3), 'x': np.int64(2)}
+ {'y': np.int64(4), 'x': np.int64(2)}
+ {'y': np.int64(4), 'x': np.int64(1)}
+
+ """
+
+ def __init__(self, stack: list[Frames[Axis]]):
+ #: The stack of Frames describing the scan, from slowest to fastest moving
+ self.stack = stack
+
+ @property
+ def axes(self) -> list[Axis]:
+ """The axes that will be present in each points dictionary."""
+ return list(itertools.chain(*(frames.axes() for frames in self.stack)))
+
+ def __len__(self) -> int:
+ """The number of dictionaries that will be produced if iterated over."""
+ return int(np.prod([len(frames) for frames in self.stack]))
+
+ def __iter__(self) -> Iterator[dict[Axis, float]]:
+ """Yield {axis: midpoint} for each frame in the scan."""
+ path = Path(self.stack)
+ while len(path):
+ frames = path.consume(1)
+ yield {a: frames.midpoints[a][0] for a in frames.axes()}
+
+
+"""`plot_spec` to visualize a scan."""
+
+from collections.abc import Iterable, Iterator
+from itertools import cycle
+from typing import Any
+
+import numpy as np
+import numpy.typing as npt
+from matplotlib import colors, patches
+from matplotlib import pyplot as plt
+from matplotlib.axes import Axes
+from mpl_toolkits.mplot3d import Axes3D, proj3d # type: ignore
+from scipy import interpolate # type: ignore
+
+from .core import Path
+from .regions import Circle, Ellipse, Polygon, Rectangle, Region, find_regions
+from .specs import DURATION, Spec
+
+__all__ = ["plot_spec"]
+
+
+def _plot_arrays(
+ axes: Axes, arrays: list[npt.NDArray[np.floating[Any]]], **kwargs: Any
+):
+ if len(arrays) > 2:
+ axes.plot3D(arrays[2], arrays[1], arrays[0], **kwargs) # type: ignore
+ elif len(arrays) == 2:
+ axes.plot(arrays[1], arrays[0], **kwargs) # type: ignore
+ else:
+ axes.plot(arrays[0], np.zeros(len(arrays[0])), **kwargs) # type: ignore
+
+
+# https://stackoverflow.com/a/11156353
+class Arrow3D(patches.FancyArrowPatch):
+ def __init__(
+ self,
+ xs: npt.NDArray[np.floating[Any]],
+ ys: npt.NDArray[np.floating[Any]],
+ zs: npt.NDArray[np.floating[Any]],
+ *args: Any,
+ **kwargs: Any,
+ ):
+ super().__init__((0, 0), (0, 0), *args, **kwargs) # type: ignore
+ self._verts3d = xs, ys, zs
+
+ # Added here because of https://github.com/matplotlib/matplotlib/issues/21688
+ def do_3d_projection(self, renderer: Any = None):
+ xs3d, ys3d, zs3d = self._verts3d
+ xs, ys, zs = proj3d.proj_transform(xs3d, ys3d, zs3d, self.axes.M) # type: ignore
+ self.set_positions((xs[0], ys[0]), (xs[1], ys[1]))
+
+ return np.min(zs)
+
+ @property
+ def verts3d(
+ self,
+ ) -> tuple[
+ npt.NDArray[np.floating[Any]],
+ npt.NDArray[np.floating[Any]],
+ npt.NDArray[np.floating[Any]],
+ ]:
+ return self._verts3d
+
+
+def _plot_arrow(axes: Axes, arrays: list[npt.NDArray[np.floating[Any]]]):
+ if len(arrays) == 1:
+ arrays = [np.array([0, 0])] + arrays
+ if len(arrays) == 2:
+ head = [a[-1] for a in reversed(arrays)]
+ tail = [a[-1] - (a[-1] - a[-2]) * 0.1 for a in reversed(arrays)]
+ axes.annotate( # type: ignore
+ "",
+ tuple(head[:2]),
+ tuple(tail[:2]),
+ arrowprops={"color": "lightgrey", "arrowstyle": "-|>"},
+ )
+ elif len(arrays) == 3:
+ arrows = [a[-2:] for a in reversed(arrays)]
+ a = Arrow3D(*arrows[:3], mutation_scale=10, arrowstyle="-|>", color="lightgrey")
+ axes.add_artist(a)
+
+
+def _plot_spline(
+ axes: Axes,
+ ranges: list[float],
+ arrays: list[npt.NDArray[np.floating[Any]]],
+ index_colours: dict[int, str],
+) -> Iterable[list[npt.NDArray[np.floating[Any]]]]:
+ scaled_arrays = [a / r for a, r in zip(arrays, ranges, strict=False)]
+ # Define curves parametrically
+ t = np.zeros(len(arrays[0]))
+ t[1:] = np.sqrt(sum((arr[1:] - arr[:-1]) ** 2 for arr in scaled_arrays))
+ t = np.cumsum(t)
+ if t[-1] > 0:
+ # Can't make a spline that starts and ends in the same place, so add a small
+ # delta
+ for s, r in zip(scaled_arrays, ranges, strict=False):
+ if s[0] == s[-1]:
+ s += np.linspace(0, r * 1e-7, len(s))
+ # There are no duplicated points, plot a spline
+ t /= t[-1]
+ # Scale the arrays so splines don't favour larger scaled axes
+ tck, _ = interpolate.splprep(scaled_arrays, k=2, s=0) # type: ignore
+ starts = sorted(index_colours)
+ stops = starts[1:] + [len(arrays[0]) - 1]
+ for start, stop in zip(starts, stops, strict=False):
+ start_value: float = t[start]
+ stop_value: float = t[stop]
+ tnew = np.linspace(start_value, stop_value, num=1001)
+ spline: npt.NDArray[np.floating[Any]] = interpolate.splev(tnew, tck) # type: ignore
+ # Scale the splines back to the original scaling
+ unscaled_splines = [a * r for a, r in zip(spline, ranges, strict=False)]
+ _plot_arrays(axes, unscaled_splines, color=index_colours[start])
+ yield unscaled_splines
+
+
+
+[docs]
+def plot_spec(spec: Spec[Any], title: str | None = None):
+ """Plot a spec, drawing the path taken through the scan.
+
+ Uses a different colour for each frame, grey for the turnarounds, and
+ marks the midpoints with a filled circle if there are less than 200 of
+ them. If the scan is 2D then 2D regions are shown in black.
+
+ .. example_spec::
+
+ from scanspec.specs import Line
+ from scanspec.regions import Circle
+
+ cube = Line("z", 1, 3, 3) * Line("y", 1, 3, 10) * ~Line("x", 0, 2, 10)
+ spec = cube & Circle("x", "y", 1, 2, 0.9)
+ """
+ dims = spec.calculate()
+ dim = Path(dims).consume()
+ axes = [a for a in spec.axes() if a is not DURATION]
+ ndims = len(axes)
+
+ # Setup axes
+ if ndims > 2:
+ plt.figure(figsize=(6, 6)) # type: ignore
+ plt_axes: Axes = plt.axes(projection="3d") # type: ignore
+ plt_axes.grid(False) # type: ignore
+ if isinstance(plt_axes, Axes3D):
+ plt_axes.set_zlabel(axes[-3]) # type: ignore
+ plt_axes.set_ylabel(axes[-2]) # type: ignore
+ plt_axes.view_init(elev=15) # type: ignore
+ else:
+ raise TypeError(
+ "Expected matplotlib to create an Axes3D object, "
+ f"instead got: {plt_axes}"
+ )
+ elif ndims == 2:
+ plt.figure(figsize=(6, 6)) # type: ignore
+ plt_axes = plt.axes() # type: ignore
+ plt_axes.set_ylabel(axes[-2]) # type: ignore
+ else:
+ plt.figure(figsize=(6, 2)) # type: ignore
+ plt_axes = plt.axes() # type: ignore
+ plt_axes.yaxis.set_visible(False)
+ plt_axes.set_xlabel(axes[-1]) # type: ignore
+
+ # Title with dimension sizes
+ title = title or ", ".join(f"Dim[{' '.join(d.axes())} len={len(d)}]" for d in dims)
+ plt.title(title) # type: ignore
+
+ # Plot any Regions
+ if ndims <= 2:
+ regions: Iterator[Region[Any]] = find_regions(spec)
+ for region in regions:
+ if isinstance(region, Rectangle):
+ xy = (region.x_min, region.y_min)
+ width = region.x_max - region.x_min
+ height = region.y_max - region.y_min
+ plt_axes.add_patch(
+ patches.Rectangle(xy, width, height, angle=region.angle, fill=False)
+ )
+ elif isinstance(region, Circle):
+ xy = (region.x_middle, region.y_middle)
+ plt_axes.add_patch(patches.Circle(xy, region.radius, fill=False))
+ elif isinstance(region, Ellipse):
+ xy = (region.x_middle, region.y_middle)
+ width = region.x_radius * 2
+ height = region.y_radius * 2
+ angle = region.angle
+ plt_axes.add_patch(
+ patches.Ellipse(xy, width, height, angle=angle, fill=False)
+ )
+ elif isinstance(region, Polygon):
+ # *xy_verts* is a numpy array with shape Nx2.
+ xy_verts = np.column_stack((region.x_verts, region.y_verts))
+ plt_axes.add_patch(patches.Polygon(xy_verts, fill=False))
+
+ # Plot the splines
+ tail: dict[str, npt.NDArray[np.floating[Any]] | None] = {a: None for a in axes}
+ ranges = [max(float(np.max(v) - np.min(v)), 0.0001) for v in dim.midpoints.values()]
+ seg_col = cycle(colors.TABLEAU_COLORS)
+ last_index = 0
+ splines = None
+ # The first element of gap is undefined (as there is no previous frame)
+ # so discard it
+ gap_indices = list(np.nonzero(dim.gap[1:])[0] + 1)
+ for index in gap_indices + [len(dim)]:
+ num_points = index - last_index
+ arrays: list[npt.NDArray[np.floating[Any]]] = []
+ turnaround: list[npt.NDArray[np.floating[Any]]] = []
+ for a in axes:
+ # Add the midpoints and the lower and upper bounds
+ arr = np.empty(num_points * 2 + 1)
+ arr[:-1:2] = dim.lower[a][last_index:index]
+ arr[1::2] = dim.midpoints[a][last_index:index]
+ arr[-1] = dim.upper[a][index - 1]
+ arrays.append(arr)
+ # Add the turnaround
+ axis_tail = tail[a]
+ if axis_tail is not None:
+ # Already had a tail, add lead in points
+ axis_tail[2:] = np.linspace(-0.01, 0, 2) * (arr[1] - arr[0]) + arr[0]
+ turnaround.append(axis_tail)
+ # Add tail off points
+ axis_tail = np.empty(4)
+ axis_tail[:2] = np.linspace(0, 0.01, 2) * (arr[-1] - arr[-2]) + arr[-1]
+ tail[a] = axis_tail
+ last_index = index
+
+ arrow_arr = None
+ if turnaround:
+ # If we didn't move then plot a straight line from start to stop
+ if all(t[1] - t[0] == 0 for t in turnaround):
+ for t in turnaround:
+ t[1] += (t[2] - t[1]) / 4
+ if all(t[3] - t[2] == 0 for t in turnaround):
+ for t in turnaround:
+ t[2] -= (t[2] - t[1]) / 4
+ # Plot the turnaround
+ arrow_arr = list(
+ _plot_spline(plt_axes, ranges, turnaround, {0: "lightgrey"})
+ )[0]
+
+ # Plot the points
+ index_colours = {2 * i: next(seg_col) for i in range(num_points)}
+ splines = list(_plot_spline(plt_axes, ranges, arrays, index_colours))
+
+ if arrow_arr:
+ # Plot the arrow on the turnaround
+ _plot_arrow(plt_axes, arrow_arr)
+ elif splines:
+ # Plot the starting arrow in the direction of the first point
+ arrow_arr = [np.array([2 * a[0] - a[1], a[0]]) for a in splines[0]]
+ _plot_arrow(plt_axes, arrow_arr)
+ else:
+ # First point isn't moving, put a right caret marker
+ _plot_arrays(
+ plt_axes,
+ [np.array([dim.lower[a][0]]) for a in axes],
+ marker=5,
+ color="lightgrey",
+ )
+
+ # Plot the capture points
+ if len(dim) < 200:
+ arrays = [dim.midpoints[a] for a in axes]
+ _plot_arrays(plt_axes, arrays, linestyle="", marker=".", color="k")
+
+ # Plot the end
+ _plot_arrays(
+ plt_axes,
+ [np.array([dim.upper[a][-1]]) for a in axes],
+ marker="x",
+ color="lightgrey",
+ )
+
+ plt.show() # type: ignore
+
+
+"""`Region` and its subclasses.
+
+.. inheritance-diagram:: scanspec.regions
+ :top-classes: scanspec.regions.Region
+ :parts: 1
+"""
+
+from __future__ import annotations
+
+from collections.abc import Iterator, Mapping
+from dataclasses import is_dataclass
+from typing import Any, Generic, cast
+
+import numpy as np
+import numpy.typing as npt
+from pydantic import BaseModel, Field, TypeAdapter
+from pydantic.dataclasses import dataclass
+
+from .core import (
+ AxesPoints,
+ Axis,
+ StrictConfig,
+ discriminated_union_of_subclasses,
+ if_instance_do,
+)
+
+__all__ = [
+ "Region",
+ "get_mask",
+ "CombinationOf",
+ "UnionOf",
+ "IntersectionOf",
+ "DifferenceOf",
+ "SymmetricDifferenceOf",
+ "Range",
+ "Rectangle",
+ "Polygon",
+ "Circle",
+ "Ellipse",
+ "find_regions",
+]
+
+NpMask = npt.NDArray[np.bool]
+
+
+
+[docs]
+@discriminated_union_of_subclasses
+class Region(Generic[Axis]):
+ """Abstract baseclass for a Region that can `Mask` a `Spec`.
+
+ Supports operators:
+
+ - ``|``: `UnionOf` two Regions, midpoints present in either
+ - ``&``: `IntersectionOf` two Regions, midpoints present in both
+ - ``-``: `DifferenceOf` two Regions, midpoints present in first not second
+ - ``^``: `SymmetricDifferenceOf` two Regions, midpoints present in one not both
+ """
+
+
+[docs]
+ def axis_sets(self) -> list[set[Axis]]: # noqa: D102
+ """Produce the non-overlapping sets of axes this region spans."""
+ raise NotImplementedError(self)
+
+
+
+[docs]
+ def mask(self, points: AxesPoints[Axis]) -> NpMask: # noqa: D102
+ """Produce a mask of which points are in the region."""
+ raise NotImplementedError(self)
+
+
+ def __or__(self, other: Region[Axis]) -> UnionOf[Axis]:
+ return if_instance_do(other, Region, lambda o: UnionOf(self, o))
+
+ def __and__(self, other: Region[Axis]) -> IntersectionOf[Axis]:
+ return if_instance_do(other, Region, lambda o: IntersectionOf(self, o))
+
+ def __sub__(self, other: Region[Axis]) -> DifferenceOf[Axis]:
+ return if_instance_do(other, Region, lambda o: DifferenceOf(self, o))
+
+ def __xor__(self, other: Region[Axis]) -> SymmetricDifferenceOf[Axis]:
+ return if_instance_do(other, Region, lambda o: SymmetricDifferenceOf(self, o))
+
+
+[docs]
+ def serialize(self) -> Mapping[str, Any]:
+ """Serialize the Region to a dictionary."""
+ return TypeAdapter(Region[Any]).dump_python(self)
+
+
+
+[docs]
+ @staticmethod
+ def deserialize(obj: Any) -> Region[Any]:
+ """Deserialize a Region from a dictionary."""
+ return TypeAdapter(Region[Any]).validate_python(obj)
+
+
+
+
+
+[docs]
+def get_mask(region: Region[Axis], points: AxesPoints[Axis]) -> NpMask:
+ """Return a mask of the points inside the region.
+
+ If there is an overlap of axes of region and points return a
+ mask of the points in the region, otherwise return all ones
+ """
+ axes = set(points)
+ needs_mask = any(ks & axes for ks in region.axis_sets())
+ if needs_mask:
+ return region.mask(points)
+ else:
+ return np.ones(len(list(points.values())[0]), dtype=np.bool)
+
+
+
+def _merge_axis_sets(axis_sets: list[set[Axis]]) -> Iterator[set[Axis]]:
+ # Take overlapping axis sets and merge any that overlap into each
+ # other
+ for ks in axis_sets: # ks = key_sets - left over from a previous naming standard
+ axis_set = ks.copy()
+ # Empty matching sets into this axis_set
+ for ks in axis_sets:
+ if ks & axis_set:
+ while ks:
+ axis_set.add(ks.pop())
+ # It might be emptied already, only yield if it isn't
+ if axis_set:
+ yield axis_set
+
+
+
+[docs]
+@dataclass(config=StrictConfig)
+class CombinationOf(Region[Axis]):
+ """Abstract baseclass for a combination of two regions, left and right."""
+
+ left: Region[Axis] = Field(description="The left-hand Region to combine")
+ right: Region[Axis] = Field(description="The right-hand Region to combine")
+
+
+[docs]
+ def axis_sets(self) -> list[set[Axis]]: # noqa: D102
+ axis_sets = list(
+ _merge_axis_sets(self.left.axis_sets() + self.right.axis_sets())
+ )
+ return axis_sets
+
+
+
+
+# Naming so we don't clash with typing.Union
+
+[docs]
+@dataclass(config=StrictConfig)
+class UnionOf(CombinationOf[Axis]):
+ """A point is in UnionOf(a, b) if in either a or b.
+
+ Typically created with the ``|`` operator
+
+ >>> r = Range("x", 0.5, 2.5) | Range("x", 1.5, 3.5)
+ >>> r.mask({"x": np.array([0, 1, 2, 3, 4])})
+ array([False, True, True, True, False])
+ """
+
+
+[docs]
+ def mask(self, points: AxesPoints[Axis]) -> NpMask: # noqa: D102
+ mask = get_mask(self.left, points) | get_mask(self.right, points)
+ return mask
+
+
+
+
+
+[docs]
+@dataclass(config=StrictConfig)
+class IntersectionOf(CombinationOf[Axis]):
+ """A point is in IntersectionOf(a, b) if in both a and b.
+
+ Typically created with the ``&`` operator.
+
+ >>> r = Range("x", 0.5, 2.5) & Range("x", 1.5, 3.5)
+ >>> r.mask({"x": np.array([0, 1, 2, 3, 4])})
+ array([False, False, True, False, False])
+ """
+
+
+[docs]
+ def mask(self, points: AxesPoints[Axis]) -> NpMask: # noqa: D102
+ mask = get_mask(self.left, points) & get_mask(self.right, points)
+ return mask
+
+
+
+
+
+[docs]
+@dataclass(config=StrictConfig)
+class DifferenceOf(CombinationOf[Axis]):
+ """A point is in DifferenceOf(a, b) if in a and not in b.
+
+ Typically created with the ``-`` operator.
+
+ >>> r = Range("x", 0.5, 2.5) - Range("x", 1.5, 3.5)
+ >>> r.mask({"x": np.array([0, 1, 2, 3, 4])})
+ array([False, True, False, False, False])
+ """
+
+
+[docs]
+ def mask(self, points: AxesPoints[Axis]) -> NpMask: # noqa: D102
+ left_mask = get_mask(self.left, points)
+ # Return the xor restricted to the left region
+ mask = left_mask ^ get_mask(self.right, points) & left_mask
+ return mask
+
+
+
+
+
+[docs]
+@dataclass(config=StrictConfig)
+class SymmetricDifferenceOf(CombinationOf[Axis]):
+ """A point is in SymmetricDifferenceOf(a, b) if in either a or b, but not both.
+
+ Typically created with the ``^`` operator.
+
+ >>> r = Range("x", 0.5, 2.5) ^ Range("x", 1.5, 3.5)
+ >>> r.mask({"x": np.array([0, 1, 2, 3, 4])})
+ array([False, True, False, True, False])
+ """
+
+
+[docs]
+ def mask(self, points: AxesPoints[Axis]) -> NpMask: # noqa: D102
+ mask = get_mask(self.left, points) ^ get_mask(self.right, points)
+ return mask
+
+
+
+
+
+[docs]
+@dataclass(config=StrictConfig)
+class Range(Region[Axis]):
+ """Mask contains points of axis >= min and <= max.
+
+ >>> r = Range("x", 1, 2)
+ >>> r.mask({"x": np.array([0, 1, 2, 3, 4])})
+ array([False, True, True, False, False])
+ """
+
+ axis: Axis = Field(description="The name matching the axis to mask in spec")
+ min: float = Field(description="The minimum inclusive value in the region")
+ max: float = Field(description="The minimum inclusive value in the region")
+
+
+
+
+
+[docs]
+ def mask(self, points: AxesPoints[Axis]) -> NpMask: # noqa: D102
+ v = points[self.axis]
+ mask = np.bitwise_and(v >= self.min, v <= self.max)
+ return mask
+
+
+
+
+
+[docs]
+@dataclass(config=StrictConfig)
+class Rectangle(Region[Axis]):
+ """Mask contains points of axis within a rotated xy rectangle.
+
+ .. example_spec::
+
+ from scanspec.regions import Rectangle
+ from scanspec.specs import Line
+
+ grid = Line("y", 1, 3, 10) * ~Line("x", 0, 2, 10)
+ spec = grid & Rectangle("x", "y", 0, 1.1, 1.5, 2.1, 30)
+ """
+
+ x_axis: Axis = Field(description="The name matching the x axis of the spec")
+ y_axis: Axis = Field(description="The name matching the y axis of the spec")
+ x_min: float = Field(description="Minimum inclusive x value in the region")
+ y_min: float = Field(description="Minimum inclusive y value in the region")
+ x_max: float = Field(description="Maximum inclusive x value in the region")
+ y_max: float = Field(description="Maximum inclusive y value in the region")
+ angle: float = Field(
+ description="Clockwise rotation angle of the rectangle", default=0.0
+ )
+
+
+[docs]
+ def axis_sets(self) -> list[set[Axis]]: # noqa: D102
+ return [{self.x_axis, self.y_axis}]
+
+
+
+[docs]
+ def mask(self, points: AxesPoints[Axis]) -> NpMask: # noqa: D102
+ x = points[self.x_axis] - self.x_min
+ y = points[self.y_axis] - self.y_min
+ if self.angle != 0:
+ # Rotate src points by -angle
+ phi = np.radians(-self.angle)
+ rx = x * np.cos(phi) - y * np.sin(phi)
+ ry = x * np.sin(phi) + y * np.cos(phi)
+ x = rx
+ y = ry
+ mask_x = np.bitwise_and(x >= 0, x <= (self.x_max - self.x_min))
+ mask_y = np.bitwise_and(y >= 0, y <= (self.y_max - self.y_min))
+ return mask_x & mask_y
+
+
+
+
+
+[docs]
+@dataclass(config=StrictConfig)
+class Polygon(Region[Axis]):
+ """Mask contains points of axis within a rotated xy polygon.
+
+ .. example_spec::
+
+ from scanspec.regions import Polygon
+ from scanspec.specs import Line
+
+ grid = Line("y", 3, 8, 10) * ~Line("x", 1 ,8, 10)
+ spec = grid & Polygon("x", "y", [1.0, 6.0, 8.0, 2.0], [4.0, 10.0, 6.0, 1.0])
+ """
+
+ x_axis: Axis = Field(description="The name matching the x axis of the spec")
+ y_axis: Axis = Field(description="The name matching the y axis of the spec")
+ x_verts: list[float] = Field(
+ description="The Nx1 x coordinates of the polygons vertices", min_length=3
+ )
+ y_verts: list[float] = Field(
+ description="The Nx1 y coordinates of the polygons vertices", min_length=3
+ )
+
+
+[docs]
+ def axis_sets(self) -> list[set[Axis]]: # noqa: D102
+ return [{self.x_axis, self.y_axis}]
+
+
+
+[docs]
+ def mask(self, points: AxesPoints[Axis]) -> NpMask: # noqa: D102
+ x = points[self.x_axis]
+ y = points[self.y_axis]
+ v1x, v1y = self.x_verts[-1], self.y_verts[-1]
+ mask = np.full(len(x), False, dtype=np.bool)
+ for v2x, v2y in zip(self.x_verts, self.y_verts, strict=False):
+ # skip horizontal edges
+ if v2y != v1y:
+ vmask = np.full(len(x), False, dtype=np.bool)
+ vmask |= (y < v2y) & (y >= v1y)
+ vmask |= (y < v1y) & (y >= v2y)
+ t = (y - v1y) / (v2y - v1y)
+ vmask &= x < v1x + t * (v2x - v1x)
+ mask ^= vmask
+ v1x, v1y = v2x, v2y
+ return mask
+
+
+
+
+
+[docs]
+@dataclass(config=StrictConfig)
+class Circle(Region[Axis]):
+ """Mask contains points of axis within an xy circle of given radius.
+
+ .. example_spec::
+
+ from scanspec.regions import Circle
+ from scanspec.specs import Line
+
+ grid = Line("y", 1, 3, 10) * ~Line("x", 0, 2, 10)
+ spec = grid & Circle("x", "y", 1, 2, 0.9)
+ """
+
+ x_axis: Axis = Field(description="The name matching the x axis of the spec")
+ y_axis: Axis = Field(description="The name matching the y axis of the spec")
+ x_middle: float = Field(description="The central x point of the circle")
+ y_middle: float = Field(description="The central y point of the circle")
+ radius: float = Field(description="Radius of the circle", gt=0.0)
+
+
+[docs]
+ def axis_sets(self) -> list[set[Axis]]: # noqa: D102
+ return [{self.x_axis, self.y_axis}]
+
+
+
+[docs]
+ def mask(self, points: AxesPoints[Axis]) -> NpMask: # noqa: D102
+ x = points[self.x_axis] - self.x_middle
+ y = points[self.y_axis] - self.y_middle
+ mask = x * x + y * y <= (self.radius * self.radius)
+ return mask
+
+
+
+
+
+[docs]
+@dataclass(config=StrictConfig)
+class Ellipse(Region[Axis]):
+ """Mask contains points of axis within an xy ellipse of given radius.
+
+ .. example_spec::
+
+ from scanspec.regions import Ellipse
+ from scanspec.specs import Line
+
+ grid = Line("y", 3, 8, 10) * ~Line("x", 1 ,8, 10)
+ spec = grid & Ellipse("x", "y", 5, 5, 2, 3, 75)
+ """
+
+ x_axis: Axis = Field(description="The name matching the x axis of the spec")
+ y_axis: Axis = Field(description="The name matching the y axis of the spec")
+ x_middle: float = Field(description="The central x point of the ellipse")
+ y_middle: float = Field(description="The central y point of the ellipse")
+ x_radius: float = Field(
+ description="The radius along the x axis of the ellipse", gt=0.0
+ )
+ y_radius: float = Field(
+ description="The radius along the y axis of the ellipse", gt=0.0
+ )
+ angle: float = Field(description="The angle of the ellipse (degrees)", default=0.0)
+
+
+[docs]
+ def axis_sets(self) -> list[set[Axis]]: # noqa: D102
+ return [{self.x_axis, self.y_axis}]
+
+
+
+[docs]
+ def mask(self, points: AxesPoints[Axis]) -> NpMask: # noqa: D102
+ x = points[self.x_axis] - self.x_middle
+ y = points[self.y_axis] - self.y_middle
+ if self.angle != 0:
+ # Rotate src points by -angle
+ phi = np.radians(-self.angle)
+ tx = x * np.cos(phi) - y * np.sin(phi)
+ ty = x * np.sin(phi) + y * np.cos(phi)
+ x = tx
+ y = ty
+ mask = (x / self.x_radius) ** 2 + (y / self.y_radius) ** 2 <= 1
+ return mask
+
+
+
+
+
+[docs]
+def find_regions(obj: Any) -> Iterator[Region[Any]]: # noqa: D102
+ """Recursively yield Regions from obj and its children."""
+ if (
+ hasattr(obj, "__pydantic_model__")
+ and issubclass(obj.__pydantic_model__, BaseModel)
+ or is_dataclass(obj)
+ ):
+ if isinstance(obj, Region):
+ yield obj
+ for name in obj.__dict__.keys():
+ regions: Iterator[Region[Any]] = find_regions(getattr(cast(Any, obj), name))
+ yield from regions
+
+
+"""`Spec` and its subclasses.
+
+.. inheritance-diagram:: scanspec.specs
+ :top-classes: scanspec.specs.Spec
+ :parts: 1
+"""
+
+from __future__ import annotations
+
+from collections.abc import Callable, Mapping
+from typing import Any, Generic, overload
+
+import numpy as np
+import numpy.typing as npt
+from pydantic import Field, TypeAdapter, validate_call
+from pydantic.dataclasses import dataclass
+
+from .core import (
+ Axis,
+ Frames,
+ Midpoints,
+ OtherAxis,
+ Path,
+ SnakedFrames,
+ StrictConfig,
+ discriminated_union_of_subclasses,
+ gap_between_frames,
+ if_instance_do,
+ squash_frames,
+)
+from .regions import Region, get_mask
+
+__all__ = [
+ "DURATION",
+ "Spec",
+ "Product",
+ "Repeat",
+ "Zip",
+ "Mask",
+ "Snake",
+ "Concat",
+ "Squash",
+ "Line",
+ "Static",
+ "Spiral",
+ "fly",
+ "step",
+]
+
+
+#: Can be used as a special key to indicate how long each point should be
+DURATION = "DURATION"
+
+
+
+[docs]
+@discriminated_union_of_subclasses
+class Spec(Generic[Axis]):
+ """A serializable representation of the type and parameters of a scan.
+
+ 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`
+ - ``&``: `Mask` the Spec with a `Region`, excluding midpoints outside of it
+ - ``~``: `Snake` the Spec, reversing every other iteration of it
+ """
+
+
+[docs]
+ def axes(self) -> list[Axis]: # noqa: D102
+ """Return the list of axes that are present in the scan.
+
+ Ordered from slowest moving to fastest moving.
+ """
+ raise NotImplementedError(self)
+
+
+
+[docs]
+ def calculate(
+ self, bounds: bool = True, nested: bool = False
+ ) -> list[Frames[Axis]]: # noqa: D102
+ """Produce a stack of nested `Frames` that form the scan.
+
+ Ordered from slowest moving to fastest moving.
+ """
+ raise NotImplementedError(self)
+
+
+
+[docs]
+ def frames(self) -> Frames[Axis]:
+ """Expand all the scan `Frames` and return them."""
+ return Path(self.calculate()).consume()
+
+
+
+[docs]
+ def midpoints(self) -> Midpoints[Axis]:
+ """Return `Midpoints` that can be iterated point by point."""
+ return Midpoints(self.calculate(bounds=False))
+
+
+
+[docs]
+ 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))
+
+ @overload
+ def __mul__(self, other: Spec[Axis]) -> Product[Axis]: ...
+
+ @overload
+ def __mul__(self, other: Spec[OtherAxis]) -> Product[Axis | OtherAxis]: ...
+
+ 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 __and__(self, other: Region[Axis]) -> Mask[Axis]:
+ return if_instance_do(other, Region, lambda o: Mask(self, o))
+
+ def __invert__(self) -> Snake[Axis]:
+ return Snake(self)
+
+
+[docs]
+ def zip(self, other: Spec[OtherAxis]) -> Zip[Axis | OtherAxis]:
+ """`Zip` the Spec with another, iterating in tandem."""
+ return Zip(left=self, right=other)
+
+
+
+[docs]
+ def concat(self, other: Spec[Axis]) -> Concat[Axis]:
+ """`Concat` the Spec with another, iterating one after the other."""
+ return Concat(self, other)
+
+
+
+[docs]
+ def serialize(self) -> Mapping[str, Any]:
+ """Serialize the Spec to a dictionary."""
+ return TypeAdapter(Spec[Any]).dump_python(self)
+
+
+
+[docs]
+ @staticmethod
+ def deserialize(obj: Any) -> Spec[Any]:
+ """Deserialize a Spec from a dictionary."""
+ return TypeAdapter(Spec[Any]).validate_python(obj)
+
+
+
+
+
+[docs]
+@dataclass(config=StrictConfig)
+class Product(Spec[Axis]):
+ """Outer product of two Specs, nesting inner within outer.
+
+ This means that inner will run in its entirety at each point in outer.
+
+ .. example_spec::
+
+ from scanspec.specs import Line
+
+ spec = Line("y", 1, 2, 3) * Line("x", 3, 4, 12)
+ """
+
+ outer: Spec[Axis] = Field(description="Will be executed once")
+ inner: Spec[Axis] = Field(description="Will be executed len(outer) times")
+
+
+
+
+
+[docs]
+ def calculate( # noqa: D102
+ self, bounds: bool = True, nested: bool = False
+ ) -> list[Frames[Axis]]:
+ frames_outer = self.outer.calculate(bounds=False, nested=nested)
+ frames_inner = self.inner.calculate(bounds, nested=True)
+ return frames_outer + frames_inner
+
+
+
+
+
+[docs]
+@dataclass(config=StrictConfig)
+class Repeat(Spec[Axis]):
+ """Repeat an empty frame num times.
+
+ Can be used on the outside of a scan to repeat the same scan many times.
+
+ .. example_spec::
+
+ from scanspec.specs import Line
+
+ spec = 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
+
+ spec = Repeat(2, gap=False) * ~Line.bounded("x", 3, 4, 1)
+
+ .. note:: There is no turnaround arrow at x=4
+ """
+
+ num: int = Field(ge=1, description="Number of frames to produce")
+ gap: bool = Field(
+ description="If False and the slowest of the stack of Frames is snaked "
+ "then the end and start of consecutive iterations of Spec will have no gap",
+ default=True,
+ )
+
+
+
+
+
+[docs]
+ def calculate( # noqa: D102
+ self, bounds: bool = True, nested: bool = False
+ ) -> list[Frames[Axis]]:
+ return [Frames({}, gap=np.full(self.num, self.gap))]
+
+
+
+
+
+[docs]
+@dataclass(config=StrictConfig)
+class Zip(Spec[Axis]):
+ """Run two Specs in parallel, merging their midpoints together.
+
+ Typically formed using `Spec.zip`.
+
+ Stacks of Frames are merged by:
+
+ - If right creates a stack of a single Frames object of size 1, expand it to
+ the size of the fastest Frames object created by left
+ - Merge individual Frames objects together from fastest to slowest
+
+ This means that Zipping a Spec producing stack [l2, l1] with a Spec
+ producing stack [r1] will assert len(l1)==len(r1), and produce
+ stack [l2, l1.zip(r1)].
+
+ .. example_spec::
+
+ from scanspec.specs import Line
+
+ spec = Line("z", 1, 2, 3) * Line("y", 3, 4, 5).zip(Line("x", 4, 5, 5))
+ """
+
+ left: Spec[Axis] = Field(
+ description="The left-hand Spec to Zip, will appear earlier in axes"
+ )
+ right: Spec[Axis] = Field(
+ description="The right-hand Spec to Zip, will appear later in axes"
+ )
+
+
+
+
+
+[docs]
+ def calculate( # noqa: D102
+ self, bounds: bool = True, nested: bool = False
+ ) -> list[Frames[Axis]]:
+ frames_left = self.left.calculate(bounds, nested)
+ frames_right = self.right.calculate(bounds, nested)
+ assert len(frames_left) >= len(
+ frames_right
+ ), f"Zip requires len({self.left}) >= len({self.right})"
+
+ # Pad and expand the right to be the same size as left. Special case, if
+ # only one Frames object with size 1, expand to the right size
+ if len(frames_right) == 1 and len(frames_right[0]) == 1:
+ # Take the 0th element N times to make a repeated Frames object
+ indices = np.zeros(len(frames_left[-1]), dtype=np.int8)
+ repeated = frames_right[0].extract(indices)
+ if isinstance(frames_left[-1], SnakedFrames):
+ repeated = SnakedFrames.from_frames(repeated)
+ frames_right = [repeated]
+
+ # Left pad frames_right with Nones so they are the same size
+ npad = len(frames_left) - len(frames_right)
+ padded_right: list[Frames[Axis] | None] = [None] * npad
+ # Mypy doesn't like this because lists are invariant:
+ # https://github.com/python/mypy/issues/4244
+ padded_right += frames_right # type: ignore
+
+ # Work through, zipping them together one by one
+ frames: list[Frames[Axis]] = []
+ for left, right in zip(frames_left, padded_right, strict=False):
+ if right is None:
+ combined = left
+ else:
+ combined = left.zip(right)
+ assert isinstance(
+ combined, Frames
+ ), f"Padding went wrong {frames_left} {padded_right}"
+ frames.append(combined)
+ return frames
+
+
+
+
+
+[docs]
+@dataclass(config=StrictConfig)
+class Mask(Spec[Axis]):
+ """Restrict Spec to only midpoints that fall inside the given Region.
+
+ Typically created with the ``&`` operator. It also pushes down the
+ ``& | ^ -`` operators to its `Region` to avoid the need for brackets on
+ combinations of Regions.
+
+ If a Region spans multiple Frames objects, they will be squashed together.
+
+ .. example_spec::
+
+ from scanspec.regions import Circle
+ from scanspec.specs import Line
+
+ spec = Line("y", 1, 3, 3) * Line("x", 3, 5, 5) & Circle("x", "y", 4, 2, 1.2)
+
+ See Also: `why-squash-can-change-path`
+ """
+
+ spec: Spec[Axis] = Field(description="The Spec containing the source midpoints")
+ region: Region[Axis] = Field(description="The Region that midpoints will be inside")
+ check_path_changes: bool = Field(
+ description="If True path through scan will not be modified by squash",
+ default=True,
+ )
+
+
+
+
+
+[docs]
+ def calculate( # noqa: D102
+ self, bounds: bool = True, nested: bool = False
+ ) -> list[Frames[Axis]]:
+ frames = self.spec.calculate(bounds, nested)
+ for axis_set in self.region.axis_sets():
+ # Find the start and end index of any dimensions containing these axes
+ matches = [i for i, d in enumerate(frames) if set(d.axes()) & axis_set]
+ assert matches, f"No Specs match axes {list(axis_set)}"
+ si, ei = matches[0], matches[-1]
+ if si != ei:
+ # The axis_set spans multiple Dimensions, squash them together
+ # If the spec to be squashed is nested (inside the Mask or outside)
+ # then check the path changes if requested
+ check_path_changes = bool(nested or si) and self.check_path_changes
+ squashed = squash_frames(frames[si : ei + 1], check_path_changes)
+ frames = frames[:si] + [squashed] + frames[ei + 1 :]
+ # Generate masks from the midpoints showing what's inside
+ masked_frames: list[Frames[Axis]] = []
+ for f in frames:
+ indices = get_mask(self.region, f.midpoints).nonzero()[0]
+ masked_frames.append(f.extract(indices))
+ return masked_frames
+
+
+ # *+ bind more tightly than &|^ so without these overrides we
+ # would need to add brackets to all combinations of Regions
+ def __or__(self, other: Region[Axis]) -> Mask[Axis]:
+ return if_instance_do(other, Region, lambda o: Mask(self.spec, self.region | o))
+
+ def __and__(self, other: Region[Axis]) -> Mask[Axis]:
+ return if_instance_do(other, Region, lambda o: Mask(self.spec, self.region & o))
+
+ def __xor__(self, other: Region[Axis]) -> Mask[Axis]:
+ return if_instance_do(other, Region, lambda o: Mask(self.spec, self.region ^ o))
+
+ # This is here for completeness, tends not to be called as - binds
+ # tighter than &
+ def __sub__(self, other: Region[Axis]) -> Mask[Axis]:
+ return if_instance_do(other, Region, lambda o: Mask(self.spec, self.region - o))
+
+
+
+
+[docs]
+@dataclass(config=StrictConfig)
+class Snake(Spec[Axis]):
+ """Run the Spec in reverse on every other iteration when nested.
+
+ Typically created with the ``~`` operator.
+
+ .. example_spec::
+
+ from scanspec.specs import Line
+
+ spec = Line("y", 1, 3, 3) * ~Line("x", 3, 5, 5)
+ """
+
+ spec: Spec[Axis] = Field(
+ description="The Spec to run in reverse every other iteration"
+ )
+
+
+
+
+
+[docs]
+ def calculate( # noqa: D102
+ self, bounds: bool = True, nested: bool = False
+ ) -> list[Frames[Axis]]:
+ return [
+ SnakedFrames.from_frames(segment)
+ for segment in self.spec.calculate(bounds, nested)
+ ]
+
+
+
+
+
+[docs]
+@dataclass(config=StrictConfig)
+class Concat(Spec[Axis]):
+ """Concatenate two Specs together, running one after the other.
+
+ Each Dimension of left and right must contain the same axes. Typically
+ formed using `Spec.concat`.
+
+ .. example_spec::
+
+ from scanspec.specs import Line
+
+ spec = Line("x", 1, 3, 3).concat(Line("x", 4, 5, 5))
+ """
+
+ left: Spec[Axis] = Field(
+ description="The left-hand Spec to Concat, midpoints will appear earlier"
+ )
+ right: Spec[Axis] = Field(
+ description="The right-hand Spec to Concat, midpoints will appear later"
+ )
+
+ gap: bool = Field(
+ description="If True, force a gap in the output at the join", default=False
+ )
+ check_path_changes: bool = Field(
+ description="If True path through scan will not be modified by squash",
+ default=True,
+ )
+
+
+[docs]
+ def axes(self) -> list[Axis]: # noqa: D102
+ left_axes, right_axes = self.left.axes(), self.right.axes()
+ # Assuming the axes are the same, the order does not matter, we inherit the
+ # order from the left-hand side. See also scanspec.core.concat.
+ assert set(left_axes) == set(right_axes), f"axes {left_axes} != {right_axes}"
+ return left_axes
+
+
+
+[docs]
+ def calculate( # noqa: D102
+ self, bounds: bool = True, nested: bool = False
+ ) -> list[Frames[Axis]]:
+ dim_left = squash_frames(
+ self.left.calculate(bounds, nested), nested and self.check_path_changes
+ )
+ dim_right = squash_frames(
+ self.right.calculate(bounds, nested), nested and self.check_path_changes
+ )
+ dim = dim_left.concat(dim_right, self.gap)
+ return [dim]
+
+
+
+
+
+[docs]
+@dataclass(config=StrictConfig)
+class Squash(Spec[Axis]):
+ """Squash a stack of Frames together into a single expanded Frames object.
+
+ See Also:
+ `why-squash-can-change-path`
+
+ .. example_spec::
+
+ from scanspec.specs import Line, Squash
+
+ spec = Squash(Line("y", 1, 2, 3) * Line("x", 0, 1, 4))
+
+ """
+
+ spec: Spec[Axis] = Field(description="The Spec to squash the dimensions of")
+ check_path_changes: bool = Field(
+ description="If True path through scan will not be modified by squash",
+ default=True,
+ )
+
+
+
+
+
+[docs]
+ def calculate( # noqa: D102
+ self, bounds: bool = True, nested: bool = False
+ ) -> list[Frames[Axis]]:
+ dims = self.spec.calculate(bounds, nested)
+ dim = squash_frames(dims, nested and self.check_path_changes)
+ return [dim]
+
+
+
+
+def _dimensions_from_indexes(
+ func: Callable[[npt.NDArray[np.float64]], dict[Axis, npt.NDArray[np.float64]]],
+ axes: list[Axis],
+ num: int,
+ bounds: bool,
+) -> list[Frames[Axis]]:
+ # Calc num midpoints (fences) from 0.5 .. num - 0.5
+ midpoints_calc = func(np.linspace(0.5, num - 0.5, num))
+ midpoints = {a: midpoints_calc[a] for a in axes}
+ if bounds:
+ # Calc num + 1 bounds (posts) from 0 .. num
+ bounds_calc = func(np.linspace(0, num, num + 1))
+ lower = {a: bounds_calc[a][:-1] for a in axes}
+ upper = {a: bounds_calc[a][1:] for a in axes}
+ # Points must have no gap as upper[a][i] == lower[a][i+1]
+ # because we initialized it to be that way
+ gap = np.zeros(num, dtype=np.bool_)
+ dimension = Frames(midpoints, lower, upper, gap)
+ # But calc the first point as difference between first
+ # and last
+ gap[0] = gap_between_frames(dimension, dimension)
+ else:
+ # Gap can be calculated in Dimension
+ dimension = Frames(midpoints)
+ return [dimension]
+
+
+
+[docs]
+@dataclass(config=StrictConfig)
+class Line(Spec[Axis]):
+ """Linearly spaced frames with start and stop as first and last midpoints.
+
+ .. example_spec::
+
+ from scanspec.specs import Line
+
+ spec = Line("x", 1, 2, 5)
+ """
+
+ axis: Axis = Field(description="An identifier for what to move")
+ start: float = Field(description="Midpoint of the first point of the line")
+ stop: float = Field(description="Midpoint of the last point of the line")
+ num: int = Field(ge=1, description="Number of frames to produce")
+
+
+
+
+ def _line_from_indexes(
+ self, indexes: npt.NDArray[np.float64]
+ ) -> dict[Axis, npt.NDArray[np.float64]]:
+ if self.num == 1:
+ # Only one point, stop-start gives length of one point
+ step = self.stop - self.start
+ else:
+ # Multiple points, stop-start gives length of num-1 points
+ step = (self.stop - self.start) / (self.num - 1)
+ # self.start is the first centre point, but we need the lower bound
+ # of the first point as this is where the index array starts
+ first = self.start - step / 2
+ return {self.axis: indexes * step + first}
+
+
+[docs]
+ def calculate( # noqa: D102
+ self, bounds: bool = True, nested: bool = False
+ ) -> list[Frames[Axis]]:
+ return _dimensions_from_indexes(
+ self._line_from_indexes, self.axes(), self.num, bounds
+ )
+
+
+
+[docs]
+ @classmethod
+ def bounded(
+ cls: type[Line[Any]],
+ axis: OtherAxis = Field(description="An identifier for what to move"),
+ lower: float = Field(description="Lower bound of the first point of the line"),
+ upper: float = Field(description="Upper bound of the last point of the line"),
+ num: int = Field(ge=1, description="Number of frames to produce"),
+ ) -> Line[OtherAxis]:
+ """Specify a Line by extreme bounds instead of midpoints.
+
+ .. example_spec::
+
+ from scanspec.specs import Line
+
+ spec = Line.bounded("x", 1, 2, 5)
+ """
+ half_step = (upper - lower) / num / 2
+ start = lower + half_step
+ if num == 1:
+ # One point, stop will only be used for step size
+ stop = upper + half_step
+ else:
+ # Many points, stop will be produced
+ stop = upper - half_step
+ return cls(axis, start, stop, num)
+
+
+
+
+"""
+Defers wrapping function with validate_call until class is fully instantiated
+"""
+Line.bounded = validate_call(Line.bounded) # type:ignore
+
+
+
+[docs]
+@dataclass(config=StrictConfig)
+class Static(Spec[Axis]):
+ """A static frame, repeated num times, with axis at value.
+
+ Can be used to set axis=value at every point in a scan.
+
+ .. example_spec::
+
+ from scanspec.specs import Line, Static
+
+ spec = Line("y", 1, 2, 3).zip(Static("x", 3))
+ """
+
+ axis: Axis = Field(description="An identifier for what to move")
+ value: float = Field(description="The value at each point")
+ num: int = Field(ge=1, description="Number of frames to produce", default=1)
+
+
+[docs]
+ @classmethod
+ def duration(
+ cls: type[Static[Any]],
+ duration: float = Field(description="The duration of each static point"),
+ num: int = Field(ge=1, description="Number of frames to produce", default=1),
+ ) -> Static[str]:
+ """A static spec with no motion, only a duration repeated "num" times.
+
+ .. example_spec::
+
+ from scanspec.specs import Line, Static
+
+ spec = Line("y", 1, 2, 3).zip(Static.duration(0.1))
+ """
+ return Static(DURATION, duration, num)
+
+
+
+
+
+ def _repeats_from_indexes(
+ self, indexes: npt.NDArray[np.float64]
+ ) -> dict[Axis, npt.NDArray[np.float64]]:
+ return {self.axis: np.full(len(indexes), self.value)}
+
+
+[docs]
+ def calculate( # noqa: D102
+ self, bounds: bool = True, nested: bool = False
+ ) -> list[Frames[Axis]]:
+ return _dimensions_from_indexes(
+ self._repeats_from_indexes, self.axes(), self.num, bounds
+ )
+
+
+
+
+Static.duration = validate_call(Static.duration) # type:ignore
+
+
+
+[docs]
+@dataclass(config=StrictConfig)
+class Spiral(Spec[Axis]):
+ """Archimedean spiral of "x_axis" and "y_axis".
+
+ Starts at centre point ("x_start", "y_start") with angle "rotate". Produces
+ "num" points in a spiral spanning width of "x_range" and height of "y_range"
+
+ .. example_spec::
+
+ from scanspec.specs import Spiral
+
+ spec = Spiral("x", "y", 1, 5, 10, 50, 30)
+ """
+
+ # TODO: Make use of typing.Annotated upon fix of
+ # https://github.com/pydantic/pydantic/issues/3496
+ x_axis: Axis = Field(description="An identifier for what to move for x")
+ y_axis: Axis = Field(description="An identifier for what to move for y")
+ x_start: float = Field(description="x centre of the spiral")
+ y_start: float = Field(description="y centre of the spiral")
+ x_range: float = Field(description="x width of the spiral")
+ y_range: float = Field(description="y width of the spiral")
+ num: int = Field(ge=1, description="Number of frames to produce")
+ rotate: float = Field(
+ description="How much to rotate the angle of the spiral", default=0.0
+ )
+
+
+[docs]
+ def axes(self) -> list[Axis]: # noqa: D102
+ # TODO: reversed from __init__ args, a good idea?
+ return [self.y_axis, self.x_axis]
+
+
+ def _spiral_from_indexes(
+ self, indexes: npt.NDArray[np.float64]
+ ) -> dict[Axis, npt.NDArray[np.float64]]:
+ # simplest spiral equation: r = phi
+ # we want point spacing across area to be the same as between rings
+ # so: sqrt(area / num) = ring_spacing
+ # so: sqrt(pi * phi^2 / num) = 2 * pi
+ # so: phi = sqrt(4 * pi * num)
+ phi = np.sqrt(4 * np.pi * indexes)
+ # indexes are 0..num inclusive, and diameter is 2x biggest phi
+ diameter = 2 * np.sqrt(4 * np.pi * self.num)
+ # scale so that the spiral is strictly smaller than the range
+ x_scale = self.x_range / diameter
+ y_scale = self.y_range / diameter
+ return {
+ self.y_axis: self.y_start + y_scale * phi * np.cos(phi + self.rotate),
+ self.x_axis: self.x_start + x_scale * phi * np.sin(phi + self.rotate),
+ }
+
+
+[docs]
+ def calculate( # noqa: D102
+ self, bounds: bool = True, nested: bool = False
+ ) -> list[Frames[Axis]]:
+ return _dimensions_from_indexes(
+ self._spiral_from_indexes, self.axes(), self.num, bounds
+ )
+
+
+
+[docs]
+ @classmethod
+ def spaced(
+ cls: type[Spiral[Any]],
+ x_axis: OtherAxis = Field(description="An identifier for what to move for x"),
+ y_axis: OtherAxis = Field(description="An identifier for what to move for y"),
+ x_start: float = Field(description="x centre of the spiral"),
+ y_start: float = Field(description="y centre of the spiral"),
+ radius: float = Field(description="radius of the spiral"),
+ dr: float = Field(description="difference between each ring"),
+ rotate: float = Field(
+ description="How much to rotate the angle of the spiral", default=0.0
+ ),
+ ) -> Spiral[OtherAxis]:
+ """Specify a Spiral equally spaced in "x_axis" and "y_axis".
+
+ .. example_spec::
+
+ from scanspec.specs import Spiral
+
+ spec = Spiral.spaced("x", "y", 0, 0, 10, 3)
+ """
+ # phi = sqrt(4 * pi * num)
+ # and: n_rings = phi / (2 * pi)
+ # so: n_rings * 2 * pi = sqrt(4 * pi * num)
+ # so: num = n_rings^2 * pi
+ n_rings = radius / dr
+ num = int(n_rings**2 * np.pi)
+ return cls(
+ x_axis,
+ y_axis,
+ x_start,
+ y_start,
+ radius * 2,
+ radius * 2,
+ num,
+ rotate,
+ )
+
+
+
+
+Spiral.spaced = validate_call(Spiral.spaced) # type:ignore
+
+
+
+[docs]
+def fly(spec: Spec[Axis], duration: float) -> Spec[Axis | str]:
+ """Flyscan, zipping with fixed duration for every frame.
+
+ Args:
+ 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
+
+ spec = fly(Line("x", 1, 2, 3), 0.1)
+
+ """
+ return spec.zip(Static.duration(duration))
+
+
+
+
+[docs]
+def step(spec: Spec[Axis], duration: float, num: int = 1) -> Spec[Axis | str]:
+ """Step scan, with num frames of given duration at each frame in the spec.
+
+ Args:
+ spec: The source `Spec` with midpoints to move to and stop
+ duration: The duration of each scan frame
+ num: Number of frames to produce with given duration at each of frame
+ in the spec
+
+ .. example_spec::
+
+ from scanspec.specs import Line, step
+
+ spec = step(Line("x", 1, 2, 3), 0.1)
+
+ """
+ return spec * Static.duration(duration, num)
+
+
+
+def get_constant_duration(frames: list[Frames[Any]]) -> float | None:
+ """Returns the duration of a number of ScanSpec frames, if known and consistent.
+
+ Args:
+ frames (List[Frames]): A number of Frame objects
+
+ Returns:
+ duration (float): if all frames have a consistent duration
+ None: otherwise
+
+ """
+ duration_frame = [
+ f for f in frames if DURATION in f.axes() and len(f.midpoints[DURATION])
+ ]
+ if len(duration_frame) != 1 or len(duration_frame[0]) < 1:
+ # Either no frame has DURATION axis,
+ # the frame with a DURATION axis has 0 points,
+ # or multiple frames have DURATION axis
+ return None
+ durations = duration_frame[0].midpoints[DURATION]
+ first_duration = durations[0]
+ if np.any(durations != first_duration):
+ # Not all durations are the same
+ return None
+ return first_duration
+
Short
+ */ + .o-tooltip--left { + position: relative; + } + + .o-tooltip--left:after { + opacity: 0; + visibility: hidden; + position: absolute; + content: attr(data-tooltip); + padding: .2em; + font-size: .8em; + left: -.2em; + background: grey; + color: white; + white-space: nowrap; + z-index: 2; + border-radius: 2px; + transform: translateX(-102%) translateY(0); + transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1); +} + +.o-tooltip--left:hover:after { + display: block; + opacity: 1; + visibility: visible; + transform: translateX(-100%) translateY(0); + transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1); + transition-delay: .5s; +} + +/* By default the copy button shouldn't show up when printing a page */ +@media print { + button.copybtn { + display: none; + } +} diff --git a/0.7.5/_static/copybutton.js b/0.7.5/_static/copybutton.js new file mode 100644 index 00000000..e0da1932 --- /dev/null +++ b/0.7.5/_static/copybutton.js @@ -0,0 +1,248 @@ +// Localization support +const messages = { + 'en': { + 'copy': 'Copy', + 'copy_to_clipboard': 'Copy to clipboard', + 'copy_success': 'Copied!', + 'copy_failure': 'Failed to copy', + }, + 'es' : { + 'copy': 'Copiar', + 'copy_to_clipboard': 'Copiar al portapapeles', + 'copy_success': '¡Copiado!', + 'copy_failure': 'Error al copiar', + }, + 'de' : { + 'copy': 'Kopieren', + 'copy_to_clipboard': 'In die Zwischenablage kopieren', + 'copy_success': 'Kopiert!', + 'copy_failure': 'Fehler beim Kopieren', + }, + 'fr' : { + 'copy': 'Copier', + 'copy_to_clipboard': 'Copier dans le presse-papier', + 'copy_success': 'Copié !', + 'copy_failure': 'Échec de la copie', + }, + 'ru': { + 'copy': 'Скопировать', + 'copy_to_clipboard': 'Скопировать в буфер', + 'copy_success': 'Скопировано!', + 'copy_failure': 'Не удалось скопировать', + }, + 'zh-CN': { + 'copy': '复制', + 'copy_to_clipboard': '复制到剪贴板', + 'copy_success': '复制成功!', + 'copy_failure': '复制失败', + }, + 'it' : { + 'copy': 'Copiare', + 'copy_to_clipboard': 'Copiato negli appunti', + 'copy_success': 'Copiato!', + 'copy_failure': 'Errore durante la copia', + } +} + +let locale = 'en' +if( document.documentElement.lang !== undefined + && messages[document.documentElement.lang] !== undefined ) { + locale = document.documentElement.lang +} + +let doc_url_root = DOCUMENTATION_OPTIONS.URL_ROOT; +if (doc_url_root == '#') { + doc_url_root = ''; +} + +/** + * SVG files for our copy buttons + */ +let iconCheck = `` + +// If the user specified their own SVG use that, otherwise use the default +let iconCopy = ``; +if (!iconCopy) { + iconCopy = `` +} + +/** + * Set up copy/paste for code blocks + */ + +const runWhenDOMLoaded = cb => { + if (document.readyState != 'loading') { + cb() + } else if (document.addEventListener) { + document.addEventListener('DOMContentLoaded', cb) + } else { + document.attachEvent('onreadystatechange', function() { + if (document.readyState == 'complete') cb() + }) + } +} + +const codeCellId = index => `codecell${index}` + +// Clears selected text since ClipboardJS will select the text when copying +const clearSelection = () => { + if (window.getSelection) { + window.getSelection().removeAllRanges() + } else if (document.selection) { + document.selection.empty() + } +} + +// Changes tooltip text for a moment, then changes it back +// We want the timeout of our `success` class to be a bit shorter than the +// tooltip and icon change, so that we can hide the icon before changing back. +var timeoutIcon = 2000; +var timeoutSuccessClass = 1500; + +const temporarilyChangeTooltip = (el, oldText, newText) => { + el.setAttribute('data-tooltip', newText) + el.classList.add('success') + // Remove success a little bit sooner than we change the tooltip + // So that we can use CSS to hide the copybutton first + setTimeout(() => el.classList.remove('success'), timeoutSuccessClass) + setTimeout(() => el.setAttribute('data-tooltip', oldText), timeoutIcon) +} + +// Changes the copy button icon for two seconds, then changes it back +const temporarilyChangeIcon = (el) => { + el.innerHTML = iconCheck; + setTimeout(() => {el.innerHTML = iconCopy}, timeoutIcon) +} + +const addCopyButtonToCodeCells = () => { + // If ClipboardJS hasn't loaded, wait a bit and try again. This + // happens because we load ClipboardJS asynchronously. + if (window.ClipboardJS === undefined) { + setTimeout(addCopyButtonToCodeCells, 250) + return + } + + // Add copybuttons to all of our code cells + const COPYBUTTON_SELECTOR = 'div.highlight pre'; + const codeCells = document.querySelectorAll(COPYBUTTON_SELECTOR) + codeCells.forEach((codeCell, index) => { + const id = codeCellId(index) + codeCell.setAttribute('id', id) + + const clipboardButton = id => + `` + codeCell.insertAdjacentHTML('afterend', clipboardButton(id)) + }) + +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + +/** + * Removes excluded text from a Node. + * + * @param {Node} target Node to filter. + * @param {string} exclude CSS selector of nodes to exclude. + * @returns {DOMString} Text from `target` with text removed. + */ +function filterText(target, exclude) { + const clone = target.cloneNode(true); // clone as to not modify the live DOM + if (exclude) { + // remove excluded nodes + clone.querySelectorAll(exclude).forEach(node => node.remove()); + } + return clone.innerText; +} + +// Callback when a copy button is clicked. Will be passed the node that was clicked +// should then grab the text and replace pieces of text that shouldn't be used in output +function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") { + var regexp; + var match; + + // Do we check for line continuation characters and "HERE-documents"? + var useLineCont = !!lineContinuationChar + var useHereDoc = !!hereDocDelim + + // create regexp to capture prompt and remaining line + if (isRegexp) { + regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)') + } else { + regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)') + } + + const outputLines = []; + var promptFound = false; + var gotLineCont = false; + var gotHereDoc = false; + const lineGotPrompt = []; + for (const line of textContent.split('\n')) { + match = line.match(regexp) + if (match || gotLineCont || gotHereDoc) { + promptFound = regexp.test(line) + lineGotPrompt.push(promptFound) + if (removePrompts && promptFound) { + outputLines.push(match[2]) + } else { + outputLines.push(line) + } + gotLineCont = line.endsWith(lineContinuationChar) & useLineCont + if (line.includes(hereDocDelim) & useHereDoc) + gotHereDoc = !gotHereDoc + } else if (!onlyCopyPromptLines) { + outputLines.push(line) + } else if (copyEmptyLines && line.trim() === '') { + outputLines.push(line) + } + } + + // If no lines with the prompt were found then just use original lines + if (lineGotPrompt.some(v => v === true)) { + textContent = outputLines.join('\n'); + } + + // Remove a trailing newline to avoid auto-running when pasting + if (textContent.endsWith("\n")) { + textContent = textContent.slice(0, -1) + } + return textContent +} + + +var copyTargetText = (trigger) => { + var target = document.querySelector(trigger.attributes['data-clipboard-target'].value); + + // get filtered text + let exclude = '.linenos'; + + let text = filterText(target, exclude); + return formatCopyText(text, '>>> |\\.\\.\\. |\\$ |In \\[\\d*\\]: | {2,5}\\.\\.\\.: | {5,8}: ', true, true, true, true, '', '') +} + + // Initialize with a callback so we can modify the text before copy + const clipboard = new ClipboardJS('.copybtn', {text: copyTargetText}) + + // Update UI with error/success messages + clipboard.on('success', event => { + clearSelection() + temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_success']) + temporarilyChangeIcon(event.trigger) + }) + + clipboard.on('error', event => { + temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_failure']) + }) +} + +runWhenDOMLoaded(addCopyButtonToCodeCells) \ No newline at end of file diff --git a/0.7.5/_static/copybutton_funcs.js b/0.7.5/_static/copybutton_funcs.js new file mode 100644 index 00000000..dbe1aaad --- /dev/null +++ b/0.7.5/_static/copybutton_funcs.js @@ -0,0 +1,73 @@ +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + +/** + * Removes excluded text from a Node. + * + * @param {Node} target Node to filter. + * @param {string} exclude CSS selector of nodes to exclude. + * @returns {DOMString} Text from `target` with text removed. + */ +export function filterText(target, exclude) { + const clone = target.cloneNode(true); // clone as to not modify the live DOM + if (exclude) { + // remove excluded nodes + clone.querySelectorAll(exclude).forEach(node => node.remove()); + } + return clone.innerText; +} + +// Callback when a copy button is clicked. Will be passed the node that was clicked +// should then grab the text and replace pieces of text that shouldn't be used in output +export function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") { + var regexp; + var match; + + // Do we check for line continuation characters and "HERE-documents"? + var useLineCont = !!lineContinuationChar + var useHereDoc = !!hereDocDelim + + // create regexp to capture prompt and remaining line + if (isRegexp) { + regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)') + } else { + regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)') + } + + const outputLines = []; + var promptFound = false; + var gotLineCont = false; + var gotHereDoc = false; + const lineGotPrompt = []; + for (const line of textContent.split('\n')) { + match = line.match(regexp) + if (match || gotLineCont || gotHereDoc) { + promptFound = regexp.test(line) + lineGotPrompt.push(promptFound) + if (removePrompts && promptFound) { + outputLines.push(match[2]) + } else { + outputLines.push(line) + } + gotLineCont = line.endsWith(lineContinuationChar) & useLineCont + if (line.includes(hereDocDelim) & useHereDoc) + gotHereDoc = !gotHereDoc + } else if (!onlyCopyPromptLines) { + outputLines.push(line) + } else if (copyEmptyLines && line.trim() === '') { + outputLines.push(line) + } + } + + // If no lines with the prompt were found then just use original lines + if (lineGotPrompt.some(v => v === true)) { + textContent = outputLines.join('\n'); + } + + // Remove a trailing newline to avoid auto-running when pasting + if (textContent.endsWith("\n")) { + textContent = textContent.slice(0, -1) + } + return textContent +} diff --git a/0.7.5/_static/design-tabs.js b/0.7.5/_static/design-tabs.js new file mode 100644 index 00000000..b25bd6a4 --- /dev/null +++ b/0.7.5/_static/design-tabs.js @@ -0,0 +1,101 @@ +// @ts-check + +// Extra JS capability for selected tabs to be synced +// The selection is stored in local storage so that it persists across page loads. + +/** + * @type {Record