From 270216513abb8a60997038e2a152ef89058634d7 Mon Sep 17 00:00:00 2001 From: Luis Segalla Date: Wed, 9 Jul 2025 09:18:14 +0100 Subject: [PATCH 01/38] Initial implementation of the new Duration Spec --- src/scanspec/core.py | 65 +++++++++++++++++++++++++++++++++---------- src/scanspec/specs.py | 42 ++++++++++++++++++++++++++-- 2 files changed, 91 insertions(+), 16 deletions(-) diff --git a/src/scanspec/core.py b/src/scanspec/core.py index e38aacb1..c0eb743c 100644 --- a/src/scanspec/core.py +++ b/src/scanspec/core.py @@ -354,6 +354,7 @@ def __init__( lower: AxesPoints[Axis] | None = None, upper: AxesPoints[Axis] | None = None, gap: GapArray | None = None, + duration: DurationArray | None = None, ): #: The midpoints of scan frames for each axis self.midpoints = midpoints @@ -361,6 +362,7 @@ def __init__( self.lower = lower or midpoints #: The upper bounds of each scan frame in each axis for fly-scanning self.upper = upper or midpoints + self.duration = duration 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 @@ -428,7 +430,21 @@ def extract_gap(gaps: Iterable[GapArray]) -> GapArray | None: return gap[dim_indices] return None - return _merge_frames(self, dict_merge=extract_dict, gap_merge=extract_gap) + def extract_duration( + durations: Iterable[DurationArray | None], + ) -> DurationArray | None: + for d in durations: + if d is not None: + for d in durations: + return d[dim_indices] + return None + + return _merge_frames( + self, + dict_merge=extract_dict, + gap_merge=extract_gap, + duration_merge=extract_duration, + ) def concat(self, other: Dimension[Axis], gap: bool = False) -> Dimension[Axis]: """Return a new Dimension object concatenating self and other. @@ -488,13 +504,26 @@ def zip_gap(gaps: Sequence[GapArray]) -> GapArray: # 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 zip_duration( + durations: Sequence[DurationArray] | None, + ) -> DurationArray | None: + return durations[-1] if durations is not None else None + + return _merge_frames( + self, + other, + dict_merge=zip_dict, + gap_merge=zip_gap, + duration_merge=zip_duration, + ) def _merge_frames( *stack: Dimension[Axis], dict_merge: Callable[[Sequence[AxesPoints[Axis]]], AxesPoints[Axis]], # type: ignore gap_merge: Callable[[Sequence[GapArray]], GapArray | None], + duration_merge: Callable[[Sequence[DurationArray]], DurationArray | None] + | None = None, ) -> Dimension[Axis]: types = {type(fs) for fs in stack} assert len(types) == 1, f"Mismatching types for {stack}" @@ -511,6 +540,9 @@ def _merge_frames( upper=dict_merge([fs.upper for fs in stack]) if any(fs.midpoints is not fs.upper for fs in stack) else None, + duration=duration_merge([fs.duration for fs in stack]) + if duration_merge is not None + else None, ) @@ -530,6 +562,7 @@ def stack2dimension( {}, {}, np.zeros(indices.shape, dtype=np.bool_), + np.zeros(indices.shape, dtype=np.float64), ) # Example numbers below from a 2x3x4 ZxYxX scan for i, frames in enumerate(dimensions): @@ -559,15 +592,13 @@ def stack2dimension( return stack -def dimension2slice( - dimension: Dimension[Axis], duration: DurationArray | None -) -> Slice[Axis]: +def dimension2slice(dimension: Dimension[Axis]) -> Slice[Axis]: return Slice( midpoints=dimension.midpoints, gap=dimension.gap, upper=dimension.upper, lower=dimension.lower, - duration=duration, + duration=dimension.duration, ) @@ -580,8 +611,11 @@ def __init__( lower: AxesPoints[Axis] | None = None, upper: AxesPoints[Axis] | None = None, gap: GapArray | None = None, + duration: DurationArray | None = None, ): - super().__init__(midpoints, lower=lower, upper=upper, gap=gap) + super().__init__( + midpoints, lower=lower, upper=upper, gap=gap, duration=duration + ) # Override first element of gap to be True, as subsequent runs # of snake scans are always joined end -> start self.gap[0] = False @@ -591,7 +625,9 @@ def from_frames( cls: type[SnakedDimension[Any]], frames: Dimension[OtherAxis] ) -> SnakedDimension[OtherAxis]: """Create a snaked version of a `Dimension` object.""" - return cls(frames.midpoints, frames.lower, frames.upper, frames.gap) + return cls( + frames.midpoints, frames.lower, frames.upper, frames.gap, frames.duration + ) def extract( self, indices: npt.NDArray[np.signedinteger[Any]], calculate_gap: bool = True @@ -623,7 +659,11 @@ def extract( else: cls = type(self) gap = None - + duration = None + if self.duration is not None: + duration = self.duration[ + np.where(backwards, length - indices, indices) % length + ] # Apply to midpoints return cls( {k: v[snake_indices] for k, v in self.midpoints.items()}, @@ -641,6 +681,7 @@ def extract( } if self.midpoints is not self.upper else None, + duration=duration, ) @@ -757,11 +798,7 @@ def consume(self, num: int | None = None) -> Slice[Axis]: stack = stack2dimension(self.stack, indices, self.lengths) - duration = None - if DURATION in stack.axes(): - duration = stack.midpoints.pop(DURATION) - - return dimension2slice(stack, duration) + return dimension2slice(stack) def __len__(self) -> int: """Number of frames left in a scan, reduces when `consume` is called.""" diff --git a/src/scanspec/specs.py b/src/scanspec/specs.py index 45046dab..0b5dcefe 100644 --- a/src/scanspec/specs.py +++ b/src/scanspec/specs.py @@ -248,6 +248,15 @@ def calculate( # noqa: D102 repeated = SnakedDimension.from_frames(repeated) frames_right = [repeated] + if ( + len(frames_right) == 1 + and len(frames_right[0]) == len(frames_left[-1]) + and isinstance(frames_left[-1], SnakedDimension) + ): + indices = np.zeros(len(frames_left[-1]), dtype=np.int8) + repeated = frames_right[0].extract(indices, calculate_gap=False) + repeated = SnakedDimension.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[Dimension[Axis] | None] = [None] * npad @@ -549,6 +558,33 @@ def bounded( Line.bounded = validate_call(Line.bounded) # type:ignore +@dataclass(config=StrictConfig) +class Duration(Spec[Axis]): + """A special spec used to hold information about the duration of each frame.""" + + value: float = Field(description="The value at each point") + num: int = Field(ge=1, description="Number of frames to produce", default=1) + + @classmethod + def redefine( + cls: type[Duration[Any]], + duration: float = Field(description="The duration of each static point"), + num: int = Field(ge=1, description="Number of frames to produce", default=1), + ) -> Duration[str]: + return Duration(duration, num) + + def calculate(self, bounds=True, nested=False) -> list[Dimension[Axis]]: + return [ + Dimension( + midpoints={}, + lower=None, + upper=None, + gap=np.full(self.num, False), + duration=np.full(self.num, self.value), + ) + ] + + @dataclass(config=StrictConfig) class Static(Spec[Axis]): """A static frame, repeated num times, with axis at value. @@ -714,7 +750,8 @@ def fly(spec: Spec[Axis], duration: float) -> Spec[Axis | str]: spec = fly(Line("x", 1, 2, 3), 0.1) """ - return spec.zip(Static.duration(duration)) + # return spec.zip(Static.duration(duration)) + return spec.zip(Duration.redefine(duration, min(spec.shape()))) def step(spec: Spec[Axis], duration: float, num: int = 1) -> Spec[Axis | str]: @@ -733,7 +770,8 @@ def step(spec: Spec[Axis], duration: float, num: int = 1) -> Spec[Axis | str]: spec = step(Line("x", 1, 2, 3), 0.1) """ - return spec * Static.duration(duration, num) + # return spec * Static.duration(duration, num) + return spec * Duration.redefine(duration, min(spec.shape())) def get_constant_duration(frames: list[Dimension[Any]]) -> float | None: From 2d2bf5adb9bef4c46ed21c6291444472f443fbb8 Mon Sep 17 00:00:00 2001 From: Luis Segalla Date: Thu, 10 Jul 2025 09:42:09 +0100 Subject: [PATCH 02/38] Update Duration spec Fly and Step now are using the Duration Spec and the code in core.py and spec.py was update to also take into consideration the .duration field in the Dimension class. --- schema.json | 37 +++++++++++++++++++++++++++++++++++++ src/scanspec/core.py | 29 ++++++++++++++++------------- src/scanspec/plot.py | 4 ++-- src/scanspec/service.py | 4 ++-- src/scanspec/specs.py | 30 ++++++++++++++++-------------- tests/test_specs.py | 9 +++++---- 6 files changed, 78 insertions(+), 35 deletions(-) diff --git a/schema.json b/schema.json index 8f109672..eb83d07b 100644 --- a/schema.json +++ b/schema.json @@ -597,6 +597,35 @@ "title": "DifferenceOf", "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])" }, + "Duration_str_": { + "properties": { + "value": { + "type": "number", + "title": "Value", + "description": "The value at each point" + }, + "num": { + "type": "integer", + "minimum": 1.0, + "title": "Num", + "description": "Number of frames to produce", + "default": 1 + }, + "type": { + "type": "string", + "const": "Duration", + "title": "Type", + "default": "Duration" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "value" + ], + "title": "Duration", + "description": "A special spec used to hold information about the duration of each frame." + }, "Ellipse_str_": { "properties": { "x_axis": { @@ -1343,6 +1372,9 @@ { "$ref": "#/components/schemas/Line_str_" }, + { + "$ref": "#/components/schemas/Duration_str_" + }, { "$ref": "#/components/schemas/Static_str_" }, @@ -1354,6 +1386,7 @@ "propertyName": "type", "mapping": { "Concat": "#/components/schemas/Concat_str_-Input", + "Duration": "#/components/schemas/Duration_str_", "Line": "#/components/schemas/Line_str_", "Mask": "#/components/schemas/Mask_str_-Input", "Product": "#/components/schemas/Product_str_-Input", @@ -1392,6 +1425,9 @@ { "$ref": "#/components/schemas/Line_str_" }, + { + "$ref": "#/components/schemas/Duration_str_" + }, { "$ref": "#/components/schemas/Static_str_" }, @@ -1403,6 +1439,7 @@ "propertyName": "type", "mapping": { "Concat": "#/components/schemas/Concat_str_-Output", + "Duration": "#/components/schemas/Duration_str_", "Line": "#/components/schemas/Line_str_", "Mask": "#/components/schemas/Mask_str_-Output", "Product": "#/components/schemas/Product_str_-Output", diff --git a/src/scanspec/core.py b/src/scanspec/core.py index c0eb743c..1fe24170 100644 --- a/src/scanspec/core.py +++ b/src/scanspec/core.py @@ -382,13 +382,16 @@ def __init__( 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)) + if len(midpoints) > 0: + # 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)) + else: + lengths = {len(self.duration)} if self.duration is not None else {0} assert len(lengths) <= 1, f"Mismatching lengths {list(lengths)}" def axes(self) -> list[Axis]: @@ -435,8 +438,7 @@ def extract_duration( ) -> DurationArray | None: for d in durations: if d is not None: - for d in durations: - return d[dim_indices] + return d[dim_indices] return None return _merge_frames( @@ -505,9 +507,10 @@ def zip_gap(gaps: Sequence[GapArray]) -> GapArray: return np.logical_or.reduce(gaps) def zip_duration( - durations: Sequence[DurationArray] | None, + durations: Sequence[DurationArray | None], ) -> DurationArray | None: - return durations[-1] if durations is not None else None + # assert len(durations) == 1, "Can't have more than one durations array" + return durations[-1] return _merge_frames( self, @@ -540,8 +543,8 @@ def _merge_frames( upper=dict_merge([fs.upper for fs in stack]) if any(fs.midpoints is not fs.upper for fs in stack) else None, - duration=duration_merge([fs.duration for fs in stack]) - if duration_merge is not None + duration=duration_merge([fs.duration for fs in stack]) # type: ignore | not sure + if any(fs.duration is not None for fs in stack) else None, ) diff --git a/src/scanspec/plot.py b/src/scanspec/plot.py index 10269392..73af2234 100644 --- a/src/scanspec/plot.py +++ b/src/scanspec/plot.py @@ -14,7 +14,7 @@ from .core import stack2dimension from .regions import Circle, Ellipse, Polygon, Rectangle, Region, find_regions -from .specs import DURATION, Spec +from .specs import Spec __all__ = ["plot_spec"] @@ -129,7 +129,7 @@ def plot_spec(spec: Spec[Any], title: str | None = None): """ dims = spec.calculate() dim = stack2dimension(dims) - axes = [a for a in spec.axes() if a is not DURATION] + axes = spec.axes() ndims = len(axes) # Setup axes diff --git a/src/scanspec/service.py b/src/scanspec/service.py index 8f6115dc..945d7ee6 100644 --- a/src/scanspec/service.py +++ b/src/scanspec/service.py @@ -223,7 +223,7 @@ def gap( dims = spec.calculate() # Grab dimensions from spec path = Path(dims) # Convert to a path gap = list(path.consume().gap) - return GapResponse(gap) + return GapResponse(gap) # type: ignore @app.post("/smalleststep", response_model=SmallestStepResponse) @@ -296,7 +296,7 @@ def _format_axes_points( """ if format is PointsFormat.FLOAT_LIST: - return {axis: list(points) for axis, points in axes_points.items()} + return {axis: list(points) for axis, points in axes_points.items()} # type: ignore elif format is PointsFormat.STRING: return {axis: str(points) for axis, points in axes_points.items()} elif format is PointsFormat.BASE64_ENCODED: diff --git a/src/scanspec/specs.py b/src/scanspec/specs.py index 0b5dcefe..9e7ce0fb 100644 --- a/src/scanspec/specs.py +++ b/src/scanspec/specs.py @@ -244,19 +244,18 @@ def calculate( # noqa: D102 # Take the 0th element N times to make a repeated Dimension object indices = np.zeros(len(frames_left[-1]), dtype=np.int8) repeated = frames_right[0].extract(indices) + # Check if frames_right is a Duration type frame + if ( + len(repeated.midpoints) == 0 + and repeated.duration is not None + and len(repeated.duration) != 0 + ): + repeated.gap = np.full(len(repeated.duration), False) + if isinstance(frames_left[-1], SnakedDimension): repeated = SnakedDimension.from_frames(repeated) frames_right = [repeated] - if ( - len(frames_right) == 1 - and len(frames_right[0]) == len(frames_left[-1]) - and isinstance(frames_left[-1], SnakedDimension) - ): - indices = np.zeros(len(frames_left[-1]), dtype=np.int8) - repeated = frames_right[0].extract(indices, calculate_gap=False) - repeated = SnakedDimension.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[Dimension[Axis] | None] = [None] * npad @@ -565,6 +564,9 @@ class Duration(Spec[Axis]): value: float = Field(description="The value at each point") num: int = Field(ge=1, description="Number of frames to produce", default=1) + def axes(self) -> list[Axis]: # noqa: D102 + return [] + @classmethod def redefine( cls: type[Duration[Any]], @@ -573,7 +575,9 @@ def redefine( ) -> Duration[str]: return Duration(duration, num) - def calculate(self, bounds=True, nested=False) -> list[Dimension[Axis]]: + def calculate( + self, bounds: bool = True, nested: bool = False + ) -> list[Dimension[Axis]]: return [ Dimension( midpoints={}, @@ -750,8 +754,7 @@ def fly(spec: Spec[Axis], duration: float) -> Spec[Axis | str]: spec = fly(Line("x", 1, 2, 3), 0.1) """ - # return spec.zip(Static.duration(duration)) - return spec.zip(Duration.redefine(duration, min(spec.shape()))) + return spec.zip(Duration(duration, 1)) def step(spec: Spec[Axis], duration: float, num: int = 1) -> Spec[Axis | str]: @@ -770,8 +773,7 @@ def step(spec: Spec[Axis], duration: float, num: int = 1) -> Spec[Axis | str]: spec = step(Line("x", 1, 2, 3), 0.1) """ - # return spec * Static.duration(duration, num) - return spec * Duration.redefine(duration, min(spec.shape())) + return spec * Duration(duration, 1) def get_constant_duration(frames: list[Dimension[Any]]) -> float | None: diff --git a/tests/test_specs.py b/tests/test_specs.py index e4fd8d4f..69aea3e5 100644 --- a/tests/test_specs.py +++ b/tests/test_specs.py @@ -63,7 +63,10 @@ def test_two_point_stepped_line() -> None: inst = step(Line(x, 0, 1, 2), 0.1) dimx, dimt = inst.calculate() assert dimx.midpoints == dimx.lower == dimx.upper == {x: approx([0, 1])} - assert dimt.midpoints == dimt.lower == dimt.upper == {DURATION: approx([0.1])} + assert dimt.midpoints == dimt.lower == dimt.upper == {} + assert dimt.duration == approx( + [0.1] + ) # Don't think this is how it's supposed to work assert inst.frames().gap == ints("11") @@ -72,17 +75,15 @@ def test_two_point_fly_line() -> None: (dim,) = inst.calculate() assert dim.midpoints == { x: approx([0, 1]), - DURATION: approx([0.1, 0.1]), } assert dim.lower == { x: approx([-0.5, 0.5]), - DURATION: approx([0.1, 0.1]), } assert dim.upper == { x: approx([0.5, 1.5]), - DURATION: approx([0.1, 0.1]), } assert dim.gap == ints("10") + assert dim.duration == approx([0.1, 0.1]) def test_many_point_line() -> None: From 763662b32fcd0c9988135f057ae1589cc29c0a51 Mon Sep 17 00:00:00 2001 From: Luis Segalla Date: Fri, 11 Jul 2025 13:18:16 +0100 Subject: [PATCH 03/38] Updates for the PR - Updated Dimension's class __init__ method to take into consideration cases without a provided Midpoint; - Updated Spec as PR comments suggested; - Removed unnecessary comments --- schema.json | 1820 +---------------------------------------- src/scanspec/core.py | 36 +- src/scanspec/specs.py | 25 +- tests/test_specs.py | 4 +- 4 files changed, 29 insertions(+), 1856 deletions(-) diff --git a/schema.json b/schema.json index eb83d07b..650e612a 100644 --- a/schema.json +++ b/schema.json @@ -1,1819 +1 @@ -{ - "openapi": "3.1.0", - "info": { - "title": "FastAPI", - "version": "0.1.1" - }, - "paths": { - "/valid": { - "post": { - "summary": "Valid", - "description": "Validate wether a ScanSpec[str] can produce a viable scan.\n\nArgs:\n spec: The scanspec to validate\n\nReturns:\n ValidResponse: A canonical version of the spec if it is valid.\n An error otherwise.", - "operationId": "valid_valid_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Spec-Input", - "examples": [ - { - "outer": { - "axis": "y", - "start": 0.0, - "stop": 10.0, - "num": 3, - "type": "Line" - }, - "inner": { - "axis": "x", - "start": 0.0, - "stop": 10.0, - "num": 4, - "type": "Line" - }, - "type": "Product" - } - ] - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ValidResponse" - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/midpoints": { - "post": { - "summary": "Midpoints", - "description": "Generate midpoints from a scanspec.\n\nA scanspec can produce bounded points (i.e. a point is valid if an\naxis is between a minimum and and a maximum, see /bounds). The midpoints\nare the middle of each set of bounds.\n\nArgs:\n request: Scanspec and formatting info.\n\nReturns:\n MidpointsResponse: Midpoints of the scan", - "operationId": "midpoints_midpoints_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PointsRequest", - "examples": [ - { - "spec": { - "outer": { - "axis": "y", - "start": 0.0, - "stop": 10.0, - "num": 3, - "type": "Line" - }, - "inner": { - "axis": "x", - "start": 0.0, - "stop": 10.0, - "num": 4, - "type": "Line" - }, - "type": "Product" - }, - "max_frames": 1024, - "format": "FLOAT_LIST" - } - ] - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MidpointsResponse" - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/bounds": { - "post": { - "summary": "Bounds", - "description": "Generate bounds from a scanspec.\n\nA scanspec can produce points with lower and upper bounds.\n\nArgs:\n request: Scanspec and formatting info.\n\nReturns:\n BoundsResponse: Bounds of the scan", - "operationId": "bounds_bounds_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PointsRequest", - "examples": [ - { - "spec": { - "outer": { - "axis": "y", - "start": 0.0, - "stop": 10.0, - "num": 3, - "type": "Line" - }, - "inner": { - "axis": "x", - "start": 0.0, - "stop": 10.0, - "num": 4, - "type": "Line" - }, - "type": "Product" - }, - "max_frames": 1024, - "format": "FLOAT_LIST" - } - ] - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BoundsResponse" - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/gap": { - "post": { - "summary": "Gap", - "description": "Generate gaps from a scanspec.\n\nA scanspec may indicate if there is a gap between two frames.\nThe array returned corresponds to whether or not there is a gap\nafter each frame.\n\nArgs:\n spec: Scanspec and formatting info.\n\nReturns:\n GapResponse: Bounds of the scan", - "operationId": "gap_gap_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Spec-Input", - "examples": [ - { - "outer": { - "axis": "y", - "start": 0.0, - "stop": 10.0, - "num": 3, - "type": "Line" - }, - "inner": { - "axis": "x", - "start": 0.0, - "stop": 10.0, - "num": 4, - "type": "Line" - }, - "type": "Product" - } - ] - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GapResponse" - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/smalleststep": { - "post": { - "summary": "Smallest Step", - "description": "Calculate the smallest step in a scan, both absolutely and per-axis.\n\nIgnore any steps of size 0.\n\nArgs:\n spec: The spec of the scan\n\nReturns:\n SmallestStepResponse: A description of the smallest steps in the spec", - "operationId": "smallest_step_smalleststep_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Spec-Input", - "examples": [ - { - "outer": { - "axis": "y", - "start": 0.0, - "stop": 10.0, - "num": 3, - "type": "Line" - }, - "inner": { - "axis": "x", - "start": 0.0, - "stop": 10.0, - "num": 4, - "type": "Line" - }, - "type": "Product" - } - ] - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SmallestStepResponse" - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "BoundsResponse": { - "properties": { - "total_frames": { - "type": "integer", - "title": "Total Frames", - "description": "Total number of frames in spec" - }, - "returned_frames": { - "type": "integer", - "title": "Returned Frames", - "description": "Total of number of frames in this response, may be less than total_frames due to downsampling etc." - }, - "format": { - "$ref": "#/components/schemas/PointsFormat", - "description": "Format of returned point data" - }, - "lower": { - "additionalProperties": { - "anyOf": [ - { - "type": "string" - }, - { - "items": { - "type": "number" - }, - "type": "array" - } - ] - }, - "type": "object", - "title": "Lower", - "description": "Lower bounds of scan frames if different from midpoints" - }, - "upper": { - "additionalProperties": { - "anyOf": [ - { - "type": "string" - }, - { - "items": { - "type": "number" - }, - "type": "array" - } - ] - }, - "type": "object", - "title": "Upper", - "description": "Upper bounds of scan frames if different from midpoints" - } - }, - "type": "object", - "required": [ - "total_frames", - "returned_frames", - "format", - "lower", - "upper" - ], - "title": "BoundsResponse", - "description": "Bounds of a generated scan." - }, - "Circle_str_": { - "properties": { - "x_axis": { - "type": "string", - "title": "X Axis", - "description": "The name matching the x axis of the spec" - }, - "y_axis": { - "type": "string", - "title": "Y Axis", - "description": "The name matching the y axis of the spec" - }, - "x_middle": { - "type": "number", - "title": "X Middle", - "description": "The central x point of the circle" - }, - "y_middle": { - "type": "number", - "title": "Y Middle", - "description": "The central y point of the circle" - }, - "radius": { - "type": "number", - "exclusiveMinimum": 0.0, - "title": "Radius", - "description": "Radius of the circle" - }, - "type": { - "type": "string", - "const": "Circle", - "title": "Type", - "default": "Circle" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "x_axis", - "y_axis", - "x_middle", - "y_middle", - "radius" - ], - "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)" - }, - "CombinationOf_str_-Input": { - "properties": { - "left": { - "$ref": "#/components/schemas/Region-Input", - "description": "The left-hand Region to combine" - }, - "right": { - "$ref": "#/components/schemas/Region-Input", - "description": "The right-hand Region to combine" - }, - "type": { - "type": "string", - "const": "CombinationOf", - "title": "Type", - "default": "CombinationOf" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "left", - "right" - ], - "title": "CombinationOf", - "description": "Abstract baseclass for a combination of two regions, left and right." - }, - "CombinationOf_str_-Output": { - "properties": { - "left": { - "$ref": "#/components/schemas/Region-Output", - "description": "The left-hand Region to combine" - }, - "right": { - "$ref": "#/components/schemas/Region-Output", - "description": "The right-hand Region to combine" - }, - "type": { - "type": "string", - "const": "CombinationOf", - "title": "Type", - "default": "CombinationOf" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "left", - "right" - ], - "title": "CombinationOf", - "description": "Abstract baseclass for a combination of two regions, left and right." - }, - "Concat_str_-Input": { - "properties": { - "left": { - "$ref": "#/components/schemas/Spec-Input", - "description": "The left-hand Spec to Concat, midpoints will appear earlier" - }, - "right": { - "$ref": "#/components/schemas/Spec-Input", - "description": "The right-hand Spec to Concat, midpoints will appear later" - }, - "gap": { - "type": "boolean", - "title": "Gap", - "description": "If True, force a gap in the output at the join", - "default": false - }, - "check_path_changes": { - "type": "boolean", - "title": "Check Path Changes", - "description": "If True path through scan will not be modified by squash", - "default": true - }, - "type": { - "type": "string", - "const": "Concat", - "title": "Type", - "default": "Concat" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "left", - "right" - ], - "title": "Concat", - "description": "Concatenate two Specs together, running one after the other.\n\nEach Dimension of left and right must contain the same axes. Typically\nformed using `Spec.concat`.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"x\", 1, 3, 3).concat(Line(\"x\", 4, 5, 5))" - }, - "Concat_str_-Output": { - "properties": { - "left": { - "$ref": "#/components/schemas/Spec-Output", - "description": "The left-hand Spec to Concat, midpoints will appear earlier" - }, - "right": { - "$ref": "#/components/schemas/Spec-Output", - "description": "The right-hand Spec to Concat, midpoints will appear later" - }, - "gap": { - "type": "boolean", - "title": "Gap", - "description": "If True, force a gap in the output at the join", - "default": false - }, - "check_path_changes": { - "type": "boolean", - "title": "Check Path Changes", - "description": "If True path through scan will not be modified by squash", - "default": true - }, - "type": { - "type": "string", - "const": "Concat", - "title": "Type", - "default": "Concat" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "left", - "right" - ], - "title": "Concat", - "description": "Concatenate two Specs together, running one after the other.\n\nEach Dimension of left and right must contain the same axes. Typically\nformed using `Spec.concat`.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"x\", 1, 3, 3).concat(Line(\"x\", 4, 5, 5))" - }, - "DifferenceOf_str_-Input": { - "properties": { - "left": { - "$ref": "#/components/schemas/Region-Input", - "description": "The left-hand Region to combine" - }, - "right": { - "$ref": "#/components/schemas/Region-Input", - "description": "The right-hand Region to combine" - }, - "type": { - "type": "string", - "const": "DifferenceOf", - "title": "Type", - "default": "DifferenceOf" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "left", - "right" - ], - "title": "DifferenceOf", - "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])" - }, - "DifferenceOf_str_-Output": { - "properties": { - "left": { - "$ref": "#/components/schemas/Region-Output", - "description": "The left-hand Region to combine" - }, - "right": { - "$ref": "#/components/schemas/Region-Output", - "description": "The right-hand Region to combine" - }, - "type": { - "type": "string", - "const": "DifferenceOf", - "title": "Type", - "default": "DifferenceOf" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "left", - "right" - ], - "title": "DifferenceOf", - "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])" - }, - "Duration_str_": { - "properties": { - "value": { - "type": "number", - "title": "Value", - "description": "The value at each point" - }, - "num": { - "type": "integer", - "minimum": 1.0, - "title": "Num", - "description": "Number of frames to produce", - "default": 1 - }, - "type": { - "type": "string", - "const": "Duration", - "title": "Type", - "default": "Duration" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "value" - ], - "title": "Duration", - "description": "A special spec used to hold information about the duration of each frame." - }, - "Ellipse_str_": { - "properties": { - "x_axis": { - "type": "string", - "title": "X Axis", - "description": "The name matching the x axis of the spec" - }, - "y_axis": { - "type": "string", - "title": "Y Axis", - "description": "The name matching the y axis of the spec" - }, - "x_middle": { - "type": "number", - "title": "X Middle", - "description": "The central x point of the ellipse" - }, - "y_middle": { - "type": "number", - "title": "Y Middle", - "description": "The central y point of the ellipse" - }, - "x_radius": { - "type": "number", - "exclusiveMinimum": 0.0, - "title": "X Radius", - "description": "The radius along the x axis of the ellipse" - }, - "y_radius": { - "type": "number", - "exclusiveMinimum": 0.0, - "title": "Y Radius", - "description": "The radius along the y axis of the ellipse" - }, - "angle": { - "type": "number", - "title": "Angle", - "description": "The angle of the ellipse (degrees)", - "default": 0.0 - }, - "type": { - "type": "string", - "const": "Ellipse", - "title": "Type", - "default": "Ellipse" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "x_axis", - "y_axis", - "x_middle", - "y_middle", - "x_radius", - "y_radius" - ], - "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)" - }, - "GapResponse": { - "properties": { - "gap": { - "items": { - "type": "boolean" - }, - "type": "array", - "title": "Gap", - "description": "Boolean array indicating if there is a gap between each frame" - } - }, - "type": "object", - "required": [ - "gap" - ], - "title": "GapResponse", - "description": "Presence of gaps in a generated scan." - }, - "HTTPValidationError": { - "properties": { - "detail": { - "items": { - "$ref": "#/components/schemas/ValidationError" - }, - "type": "array", - "title": "Detail" - } - }, - "type": "object", - "title": "HTTPValidationError" - }, - "IntersectionOf_str_-Input": { - "properties": { - "left": { - "$ref": "#/components/schemas/Region-Input", - "description": "The left-hand Region to combine" - }, - "right": { - "$ref": "#/components/schemas/Region-Input", - "description": "The right-hand Region to combine" - }, - "type": { - "type": "string", - "const": "IntersectionOf", - "title": "Type", - "default": "IntersectionOf" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "left", - "right" - ], - "title": "IntersectionOf", - "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])" - }, - "IntersectionOf_str_-Output": { - "properties": { - "left": { - "$ref": "#/components/schemas/Region-Output", - "description": "The left-hand Region to combine" - }, - "right": { - "$ref": "#/components/schemas/Region-Output", - "description": "The right-hand Region to combine" - }, - "type": { - "type": "string", - "const": "IntersectionOf", - "title": "Type", - "default": "IntersectionOf" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "left", - "right" - ], - "title": "IntersectionOf", - "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])" - }, - "Line_str_": { - "properties": { - "axis": { - "type": "string", - "title": "Axis", - "description": "An identifier for what to move" - }, - "start": { - "type": "number", - "title": "Start", - "description": "Midpoint of the first point of the line" - }, - "stop": { - "type": "number", - "title": "Stop", - "description": "Midpoint of the last point of the line" - }, - "num": { - "type": "integer", - "minimum": 1.0, - "title": "Num", - "description": "Number of frames to produce" - }, - "type": { - "type": "string", - "const": "Line", - "title": "Type", - "default": "Line" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "axis", - "start", - "stop", - "num" - ], - "title": "Line", - "description": "Linearly spaced frames with start and stop as first and last midpoints.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"x\", 1, 2, 5)" - }, - "Mask_str_-Input": { - "properties": { - "spec": { - "$ref": "#/components/schemas/Spec-Input", - "description": "The Spec containing the source midpoints" - }, - "region": { - "$ref": "#/components/schemas/Region-Input", - "description": "The Region that midpoints will be inside" - }, - "check_path_changes": { - "type": "boolean", - "title": "Check Path Changes", - "description": "If True path through scan will not be modified by squash", - "default": true - }, - "type": { - "type": "string", - "const": "Mask", - "title": "Type", - "default": "Mask" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "spec", - "region" - ], - "title": "Mask", - "description": "Restrict Spec to only midpoints that fall inside the given Region.\n\nTypically created with the ``&`` operator. It also pushes down the\n``& | ^ -`` operators to its `Region` to avoid the need for brackets on\ncombinations of Regions.\n\nIf a Region spans multiple Dimension objects, they will be squashed together.\n\n.. example_spec::\n\n from scanspec.regions import Circle\n from scanspec.specs import Line\n\n spec = Line(\"y\", 1, 3, 3) * Line(\"x\", 3, 5, 5) & Circle(\"x\", \"y\", 4, 2, 1.2)\n\nSee Also: `why-squash-can-change-path`" - }, - "Mask_str_-Output": { - "properties": { - "spec": { - "$ref": "#/components/schemas/Spec-Output", - "description": "The Spec containing the source midpoints" - }, - "region": { - "$ref": "#/components/schemas/Region-Output", - "description": "The Region that midpoints will be inside" - }, - "check_path_changes": { - "type": "boolean", - "title": "Check Path Changes", - "description": "If True path through scan will not be modified by squash", - "default": true - }, - "type": { - "type": "string", - "const": "Mask", - "title": "Type", - "default": "Mask" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "spec", - "region" - ], - "title": "Mask", - "description": "Restrict Spec to only midpoints that fall inside the given Region.\n\nTypically created with the ``&`` operator. It also pushes down the\n``& | ^ -`` operators to its `Region` to avoid the need for brackets on\ncombinations of Regions.\n\nIf a Region spans multiple Dimension objects, they will be squashed together.\n\n.. example_spec::\n\n from scanspec.regions import Circle\n from scanspec.specs import Line\n\n spec = Line(\"y\", 1, 3, 3) * Line(\"x\", 3, 5, 5) & Circle(\"x\", \"y\", 4, 2, 1.2)\n\nSee Also: `why-squash-can-change-path`" - }, - "MidpointsResponse": { - "properties": { - "total_frames": { - "type": "integer", - "title": "Total Frames", - "description": "Total number of frames in spec" - }, - "returned_frames": { - "type": "integer", - "title": "Returned Frames", - "description": "Total of number of frames in this response, may be less than total_frames due to downsampling etc." - }, - "format": { - "$ref": "#/components/schemas/PointsFormat", - "description": "Format of returned point data" - }, - "midpoints": { - "additionalProperties": { - "anyOf": [ - { - "type": "string" - }, - { - "items": { - "type": "number" - }, - "type": "array" - } - ] - }, - "type": "object", - "title": "Midpoints", - "description": "The midpoints of scan frames for each axis" - } - }, - "type": "object", - "required": [ - "total_frames", - "returned_frames", - "format", - "midpoints" - ], - "title": "MidpointsResponse", - "description": "Midpoints of a generated scan." - }, - "PointsFormat": { - "type": "string", - "enum": [ - "STRING", - "FLOAT_LIST", - "BASE64_ENCODED" - ], - "title": "PointsFormat", - "description": "Formats in which we can return points." - }, - "PointsRequest": { - "properties": { - "spec": { - "$ref": "#/components/schemas/Spec-Input", - "description": "The spec from which to generate points" - }, - "max_frames": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "title": "Max Frames", - "description": "The maximum number of points to return, if None will return as many as calculated", - "default": 100000 - }, - "format": { - "$ref": "#/components/schemas/PointsFormat", - "description": "The format in which to output the points data", - "default": "FLOAT_LIST" - } - }, - "type": "object", - "required": [ - "spec" - ], - "title": "PointsRequest", - "description": "A request for generated scan points." - }, - "Polygon_str_": { - "properties": { - "x_axis": { - "type": "string", - "title": "X Axis", - "description": "The name matching the x axis of the spec" - }, - "y_axis": { - "type": "string", - "title": "Y Axis", - "description": "The name matching the y axis of the spec" - }, - "x_verts": { - "items": { - "type": "number" - }, - "type": "array", - "minItems": 3, - "title": "X Verts", - "description": "The Nx1 x coordinates of the polygons vertices" - }, - "y_verts": { - "items": { - "type": "number" - }, - "type": "array", - "minItems": 3, - "title": "Y Verts", - "description": "The Nx1 y coordinates of the polygons vertices" - }, - "type": { - "type": "string", - "const": "Polygon", - "title": "Type", - "default": "Polygon" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "x_axis", - "y_axis", - "x_verts", - "y_verts" - ], - "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])" - }, - "Product_str_-Input": { - "properties": { - "outer": { - "$ref": "#/components/schemas/Spec-Input", - "description": "Will be executed once" - }, - "inner": { - "$ref": "#/components/schemas/Spec-Input", - "description": "Will be executed len(outer) times" - }, - "type": { - "type": "string", - "const": "Product", - "title": "Type", - "default": "Product" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "outer", - "inner" - ], - "title": "Product", - "description": "Outer product of two Specs, nesting inner within outer.\n\nThis means that inner will run in its entirety at each point in outer.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"y\", 1, 2, 3) * Line(\"x\", 3, 4, 12)" - }, - "Product_str_-Output": { - "properties": { - "outer": { - "$ref": "#/components/schemas/Spec-Output", - "description": "Will be executed once" - }, - "inner": { - "$ref": "#/components/schemas/Spec-Output", - "description": "Will be executed len(outer) times" - }, - "type": { - "type": "string", - "const": "Product", - "title": "Type", - "default": "Product" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "outer", - "inner" - ], - "title": "Product", - "description": "Outer product of two Specs, nesting inner within outer.\n\nThis means that inner will run in its entirety at each point in outer.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"y\", 1, 2, 3) * Line(\"x\", 3, 4, 12)" - }, - "Range_str_": { - "properties": { - "axis": { - "type": "string", - "title": "Axis", - "description": "The name matching the axis to mask in spec" - }, - "min": { - "type": "number", - "title": "Min", - "description": "The minimum inclusive value in the region" - }, - "max": { - "type": "number", - "title": "Max", - "description": "The minimum inclusive value in the region" - }, - "type": { - "type": "string", - "const": "Range", - "title": "Type", - "default": "Range" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "axis", - "min", - "max" - ], - "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])" - }, - "Rectangle_str_": { - "properties": { - "x_axis": { - "type": "string", - "title": "X Axis", - "description": "The name matching the x axis of the spec" - }, - "y_axis": { - "type": "string", - "title": "Y Axis", - "description": "The name matching the y axis of the spec" - }, - "x_min": { - "type": "number", - "title": "X Min", - "description": "Minimum inclusive x value in the region" - }, - "y_min": { - "type": "number", - "title": "Y Min", - "description": "Minimum inclusive y value in the region" - }, - "x_max": { - "type": "number", - "title": "X Max", - "description": "Maximum inclusive x value in the region" - }, - "y_max": { - "type": "number", - "title": "Y Max", - "description": "Maximum inclusive y value in the region" - }, - "angle": { - "type": "number", - "title": "Angle", - "description": "Clockwise rotation angle of the rectangle", - "default": 0.0 - }, - "type": { - "type": "string", - "const": "Rectangle", - "title": "Type", - "default": "Rectangle" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "x_axis", - "y_axis", - "x_min", - "y_min", - "x_max", - "y_max" - ], - "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)" - }, - "Region-Input": { - "oneOf": [ - { - "$ref": "#/components/schemas/CombinationOf_str_-Input" - }, - { - "$ref": "#/components/schemas/UnionOf_str_-Input" - }, - { - "$ref": "#/components/schemas/IntersectionOf_str_-Input" - }, - { - "$ref": "#/components/schemas/DifferenceOf_str_-Input" - }, - { - "$ref": "#/components/schemas/SymmetricDifferenceOf_str_-Input" - }, - { - "$ref": "#/components/schemas/Range_str_" - }, - { - "$ref": "#/components/schemas/Rectangle_str_" - }, - { - "$ref": "#/components/schemas/Polygon_str_" - }, - { - "$ref": "#/components/schemas/Circle_str_" - }, - { - "$ref": "#/components/schemas/Ellipse_str_" - } - ], - "discriminator": { - "propertyName": "type", - "mapping": { - "Circle": "#/components/schemas/Circle_str_", - "CombinationOf": "#/components/schemas/CombinationOf_str_-Input", - "DifferenceOf": "#/components/schemas/DifferenceOf_str_-Input", - "Ellipse": "#/components/schemas/Ellipse_str_", - "IntersectionOf": "#/components/schemas/IntersectionOf_str_-Input", - "Polygon": "#/components/schemas/Polygon_str_", - "Range": "#/components/schemas/Range_str_", - "Rectangle": "#/components/schemas/Rectangle_str_", - "SymmetricDifferenceOf": "#/components/schemas/SymmetricDifferenceOf_str_-Input", - "UnionOf": "#/components/schemas/UnionOf_str_-Input" - } - } - }, - "Region-Output": { - "oneOf": [ - { - "$ref": "#/components/schemas/CombinationOf_str_-Output" - }, - { - "$ref": "#/components/schemas/UnionOf_str_-Output" - }, - { - "$ref": "#/components/schemas/IntersectionOf_str_-Output" - }, - { - "$ref": "#/components/schemas/DifferenceOf_str_-Output" - }, - { - "$ref": "#/components/schemas/SymmetricDifferenceOf_str_-Output" - }, - { - "$ref": "#/components/schemas/Range_str_" - }, - { - "$ref": "#/components/schemas/Rectangle_str_" - }, - { - "$ref": "#/components/schemas/Polygon_str_" - }, - { - "$ref": "#/components/schemas/Circle_str_" - }, - { - "$ref": "#/components/schemas/Ellipse_str_" - } - ], - "discriminator": { - "propertyName": "type", - "mapping": { - "Circle": "#/components/schemas/Circle_str_", - "CombinationOf": "#/components/schemas/CombinationOf_str_-Output", - "DifferenceOf": "#/components/schemas/DifferenceOf_str_-Output", - "Ellipse": "#/components/schemas/Ellipse_str_", - "IntersectionOf": "#/components/schemas/IntersectionOf_str_-Output", - "Polygon": "#/components/schemas/Polygon_str_", - "Range": "#/components/schemas/Range_str_", - "Rectangle": "#/components/schemas/Rectangle_str_", - "SymmetricDifferenceOf": "#/components/schemas/SymmetricDifferenceOf_str_-Output", - "UnionOf": "#/components/schemas/UnionOf_str_-Output" - } - } - }, - "Repeat_str_": { - "properties": { - "num": { - "type": "integer", - "minimum": 1.0, - "title": "Num", - "description": "Number of frames to produce" - }, - "gap": { - "type": "boolean", - "title": "Gap", - "description": "If False and the slowest of the stack of Dimension is snaked then the end and start of consecutive iterations of Spec will have no gap", - "default": true - }, - "type": { - "type": "string", - "const": "Repeat", - "title": "Type", - "default": "Repeat" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "num" - ], - "title": "Repeat", - "description": "Repeat an empty frame num times.\n\nCan be used on the outside of a scan to repeat the same scan many times.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = 2 * ~Line.bounded(\"x\", 3, 4, 1)\n\nIf you want snaked axes to have no gap between iterations you can do:\n\n.. example_spec::\n\n from scanspec.specs import Line, Repeat\n\n spec = Repeat(2, gap=False) * ~Line.bounded(\"x\", 3, 4, 1)\n\n.. note:: There is no turnaround arrow at x=4" - }, - "SmallestStepResponse": { - "properties": { - "absolute": { - "type": "number", - "title": "Absolute", - "description": "Absolute smallest distance between two points on a single axis" - }, - "per_axis": { - "additionalProperties": { - "type": "number" - }, - "type": "object", - "title": "Per Axis", - "description": "Smallest distance between two points on each axis" - } - }, - "type": "object", - "required": [ - "absolute", - "per_axis" - ], - "title": "SmallestStepResponse", - "description": "Information about the smallest steps between points in a spec." - }, - "Snake_str_-Input": { - "properties": { - "spec": { - "$ref": "#/components/schemas/Spec-Input", - "description": "The Spec to run in reverse every other iteration" - }, - "type": { - "type": "string", - "const": "Snake", - "title": "Type", - "default": "Snake" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "spec" - ], - "title": "Snake", - "description": "Run the Spec in reverse on every other iteration when nested.\n\nTypically created with the ``~`` operator.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"y\", 1, 3, 3) * ~Line(\"x\", 3, 5, 5)" - }, - "Snake_str_-Output": { - "properties": { - "spec": { - "$ref": "#/components/schemas/Spec-Output", - "description": "The Spec to run in reverse every other iteration" - }, - "type": { - "type": "string", - "const": "Snake", - "title": "Type", - "default": "Snake" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "spec" - ], - "title": "Snake", - "description": "Run the Spec in reverse on every other iteration when nested.\n\nTypically created with the ``~`` operator.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"y\", 1, 3, 3) * ~Line(\"x\", 3, 5, 5)" - }, - "Spec-Input": { - "oneOf": [ - { - "$ref": "#/components/schemas/Product_str_-Input" - }, - { - "$ref": "#/components/schemas/Repeat_str_" - }, - { - "$ref": "#/components/schemas/Zip_str_-Input" - }, - { - "$ref": "#/components/schemas/Mask_str_-Input" - }, - { - "$ref": "#/components/schemas/Snake_str_-Input" - }, - { - "$ref": "#/components/schemas/Concat_str_-Input" - }, - { - "$ref": "#/components/schemas/Squash_str_-Input" - }, - { - "$ref": "#/components/schemas/Line_str_" - }, - { - "$ref": "#/components/schemas/Duration_str_" - }, - { - "$ref": "#/components/schemas/Static_str_" - }, - { - "$ref": "#/components/schemas/Spiral_str_" - } - ], - "discriminator": { - "propertyName": "type", - "mapping": { - "Concat": "#/components/schemas/Concat_str_-Input", - "Duration": "#/components/schemas/Duration_str_", - "Line": "#/components/schemas/Line_str_", - "Mask": "#/components/schemas/Mask_str_-Input", - "Product": "#/components/schemas/Product_str_-Input", - "Repeat": "#/components/schemas/Repeat_str_", - "Snake": "#/components/schemas/Snake_str_-Input", - "Spiral": "#/components/schemas/Spiral_str_", - "Squash": "#/components/schemas/Squash_str_-Input", - "Static": "#/components/schemas/Static_str_", - "Zip": "#/components/schemas/Zip_str_-Input" - } - } - }, - "Spec-Output": { - "oneOf": [ - { - "$ref": "#/components/schemas/Product_str_-Output" - }, - { - "$ref": "#/components/schemas/Repeat_str_" - }, - { - "$ref": "#/components/schemas/Zip_str_-Output" - }, - { - "$ref": "#/components/schemas/Mask_str_-Output" - }, - { - "$ref": "#/components/schemas/Snake_str_-Output" - }, - { - "$ref": "#/components/schemas/Concat_str_-Output" - }, - { - "$ref": "#/components/schemas/Squash_str_-Output" - }, - { - "$ref": "#/components/schemas/Line_str_" - }, - { - "$ref": "#/components/schemas/Duration_str_" - }, - { - "$ref": "#/components/schemas/Static_str_" - }, - { - "$ref": "#/components/schemas/Spiral_str_" - } - ], - "discriminator": { - "propertyName": "type", - "mapping": { - "Concat": "#/components/schemas/Concat_str_-Output", - "Duration": "#/components/schemas/Duration_str_", - "Line": "#/components/schemas/Line_str_", - "Mask": "#/components/schemas/Mask_str_-Output", - "Product": "#/components/schemas/Product_str_-Output", - "Repeat": "#/components/schemas/Repeat_str_", - "Snake": "#/components/schemas/Snake_str_-Output", - "Spiral": "#/components/schemas/Spiral_str_", - "Squash": "#/components/schemas/Squash_str_-Output", - "Static": "#/components/schemas/Static_str_", - "Zip": "#/components/schemas/Zip_str_-Output" - } - } - }, - "Spiral_str_": { - "properties": { - "x_axis": { - "type": "string", - "title": "X Axis", - "description": "An identifier for what to move for x" - }, - "y_axis": { - "type": "string", - "title": "Y Axis", - "description": "An identifier for what to move for y" - }, - "x_start": { - "type": "number", - "title": "X Start", - "description": "x centre of the spiral" - }, - "y_start": { - "type": "number", - "title": "Y Start", - "description": "y centre of the spiral" - }, - "x_range": { - "type": "number", - "title": "X Range", - "description": "x width of the spiral" - }, - "y_range": { - "type": "number", - "title": "Y Range", - "description": "y width of the spiral" - }, - "num": { - "type": "integer", - "minimum": 1.0, - "title": "Num", - "description": "Number of frames to produce" - }, - "rotate": { - "type": "number", - "title": "Rotate", - "description": "How much to rotate the angle of the spiral", - "default": 0.0 - }, - "type": { - "type": "string", - "const": "Spiral", - "title": "Type", - "default": "Spiral" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "x_axis", - "y_axis", - "x_start", - "y_start", - "x_range", - "y_range", - "num" - ], - "title": "Spiral", - "description": "Archimedean spiral of \"x_axis\" and \"y_axis\".\n\nStarts at centre point (\"x_start\", \"y_start\") with angle \"rotate\". Produces\n\"num\" points in a spiral spanning width of \"x_range\" and height of \"y_range\"\n\n.. example_spec::\n\n from scanspec.specs import Spiral\n\n spec = Spiral(\"x\", \"y\", 1, 5, 10, 50, 30)" - }, - "Squash_str_-Input": { - "properties": { - "spec": { - "$ref": "#/components/schemas/Spec-Input", - "description": "The Spec to squash the dimensions of" - }, - "check_path_changes": { - "type": "boolean", - "title": "Check Path Changes", - "description": "If True path through scan will not be modified by squash", - "default": true - }, - "type": { - "type": "string", - "const": "Squash", - "title": "Type", - "default": "Squash" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "spec" - ], - "title": "Squash", - "description": "Squash a stack of Dimension together into a single expanded Dimension object.\n\nSee Also:\n `why-squash-can-change-path`\n\n.. example_spec::\n\n from scanspec.specs import Line, Squash\n\n spec = Squash(Line(\"y\", 1, 2, 3) * Line(\"x\", 0, 1, 4))" - }, - "Squash_str_-Output": { - "properties": { - "spec": { - "$ref": "#/components/schemas/Spec-Output", - "description": "The Spec to squash the dimensions of" - }, - "check_path_changes": { - "type": "boolean", - "title": "Check Path Changes", - "description": "If True path through scan will not be modified by squash", - "default": true - }, - "type": { - "type": "string", - "const": "Squash", - "title": "Type", - "default": "Squash" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "spec" - ], - "title": "Squash", - "description": "Squash a stack of Dimension together into a single expanded Dimension object.\n\nSee Also:\n `why-squash-can-change-path`\n\n.. example_spec::\n\n from scanspec.specs import Line, Squash\n\n spec = Squash(Line(\"y\", 1, 2, 3) * Line(\"x\", 0, 1, 4))" - }, - "Static_str_": { - "properties": { - "axis": { - "type": "string", - "title": "Axis", - "description": "An identifier for what to move" - }, - "value": { - "type": "number", - "title": "Value", - "description": "The value at each point" - }, - "num": { - "type": "integer", - "minimum": 1.0, - "title": "Num", - "description": "Number of frames to produce", - "default": 1 - }, - "type": { - "type": "string", - "const": "Static", - "title": "Type", - "default": "Static" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "axis", - "value" - ], - "title": "Static", - "description": "A static frame, repeated num times, with axis at value.\n\nCan be used to set axis=value at every point in a scan.\n\n.. example_spec::\n\n from scanspec.specs import Line, Static\n\n spec = Line(\"y\", 1, 2, 3).zip(Static(\"x\", 3))" - }, - "SymmetricDifferenceOf_str_-Input": { - "properties": { - "left": { - "$ref": "#/components/schemas/Region-Input", - "description": "The left-hand Region to combine" - }, - "right": { - "$ref": "#/components/schemas/Region-Input", - "description": "The right-hand Region to combine" - }, - "type": { - "type": "string", - "const": "SymmetricDifferenceOf", - "title": "Type", - "default": "SymmetricDifferenceOf" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "left", - "right" - ], - "title": "SymmetricDifferenceOf", - "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])" - }, - "SymmetricDifferenceOf_str_-Output": { - "properties": { - "left": { - "$ref": "#/components/schemas/Region-Output", - "description": "The left-hand Region to combine" - }, - "right": { - "$ref": "#/components/schemas/Region-Output", - "description": "The right-hand Region to combine" - }, - "type": { - "type": "string", - "const": "SymmetricDifferenceOf", - "title": "Type", - "default": "SymmetricDifferenceOf" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "left", - "right" - ], - "title": "SymmetricDifferenceOf", - "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])" - }, - "UnionOf_str_-Input": { - "properties": { - "left": { - "$ref": "#/components/schemas/Region-Input", - "description": "The left-hand Region to combine" - }, - "right": { - "$ref": "#/components/schemas/Region-Input", - "description": "The right-hand Region to combine" - }, - "type": { - "type": "string", - "const": "UnionOf", - "title": "Type", - "default": "UnionOf" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "left", - "right" - ], - "title": "UnionOf", - "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])" - }, - "UnionOf_str_-Output": { - "properties": { - "left": { - "$ref": "#/components/schemas/Region-Output", - "description": "The left-hand Region to combine" - }, - "right": { - "$ref": "#/components/schemas/Region-Output", - "description": "The right-hand Region to combine" - }, - "type": { - "type": "string", - "const": "UnionOf", - "title": "Type", - "default": "UnionOf" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "left", - "right" - ], - "title": "UnionOf", - "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])" - }, - "ValidResponse": { - "properties": { - "input_spec": { - "$ref": "#/components/schemas/Spec-Output", - "description": "The input scanspec" - }, - "valid_spec": { - "$ref": "#/components/schemas/Spec-Output", - "description": "The validated version of the spec" - } - }, - "type": "object", - "required": [ - "input_spec", - "valid_spec" - ], - "title": "ValidResponse", - "description": "Response model for spec validation." - }, - "ValidationError": { - "properties": { - "loc": { - "items": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "integer" - } - ] - }, - "type": "array", - "title": "Location" - }, - "msg": { - "type": "string", - "title": "Message" - }, - "type": { - "type": "string", - "title": "Error Type" - } - }, - "type": "object", - "required": [ - "loc", - "msg", - "type" - ], - "title": "ValidationError" - }, - "Zip_str_-Input": { - "properties": { - "left": { - "$ref": "#/components/schemas/Spec-Input", - "description": "The left-hand Spec to Zip, will appear earlier in axes" - }, - "right": { - "$ref": "#/components/schemas/Spec-Input", - "description": "The right-hand Spec to Zip, will appear later in axes" - }, - "type": { - "type": "string", - "const": "Zip", - "title": "Type", - "default": "Zip" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "left", - "right" - ], - "title": "Zip", - "description": "Run two Specs in parallel, merging their midpoints together.\n\nTypically formed using `Spec.zip`.\n\nStacks of Dimension are merged by:\n\n- If right creates a stack of a single Dimension object of size 1, expand it to\n the size of the fastest Dimension object created by left\n- Merge individual Dimension objects together from fastest to slowest\n\nThis means that Zipping a Spec producing stack [l2, l1] with a Spec\nproducing stack [r1] will assert len(l1)==len(r1), and produce\nstack [l2, l1.zip(r1)].\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"z\", 1, 2, 3) * Line(\"y\", 3, 4, 5).zip(Line(\"x\", 4, 5, 5))" - }, - "Zip_str_-Output": { - "properties": { - "left": { - "$ref": "#/components/schemas/Spec-Output", - "description": "The left-hand Spec to Zip, will appear earlier in axes" - }, - "right": { - "$ref": "#/components/schemas/Spec-Output", - "description": "The right-hand Spec to Zip, will appear later in axes" - }, - "type": { - "type": "string", - "const": "Zip", - "title": "Type", - "default": "Zip" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "left", - "right" - ], - "title": "Zip", - "description": "Run two Specs in parallel, merging their midpoints together.\n\nTypically formed using `Spec.zip`.\n\nStacks of Dimension are merged by:\n\n- If right creates a stack of a single Dimension object of size 1, expand it to\n the size of the fastest Dimension object created by left\n- Merge individual Dimension objects together from fastest to slowest\n\nThis means that Zipping a Spec producing stack [l2, l1] with a Spec\nproducing stack [r1] will assert len(l1)==len(r1), and produce\nstack [l2, l1.zip(r1)].\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"z\", 1, 2, 3) * Line(\"y\", 3, 4, 5).zip(Line(\"x\", 4, 5, 5))" - } - } - } -} +{"openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.1"}, "paths": {"/valid": {"post": {"summary": "Valid", "description": "Validate wether a ScanSpec[str] can produce a viable scan.\n\nArgs:\n spec: The scanspec to validate\n\nReturns:\n ValidResponse: A canonical version of the spec if it is valid.\n An error otherwise.", "operationId": "valid_valid_post", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Spec-Input", "examples": [{"outer": {"axis": "y", "start": 0.0, "stop": 10.0, "num": 3, "type": "Line"}, "inner": {"axis": "x", "start": 0.0, "stop": 10.0, "num": 4, "type": "Line"}, "type": "Product"}]}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ValidResponse"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/midpoints": {"post": {"summary": "Midpoints", "description": "Generate midpoints from a scanspec.\n\nA scanspec can produce bounded points (i.e. a point is valid if an\naxis is between a minimum and and a maximum, see /bounds). The midpoints\nare the middle of each set of bounds.\n\nArgs:\n request: Scanspec and formatting info.\n\nReturns:\n MidpointsResponse: Midpoints of the scan", "operationId": "midpoints_midpoints_post", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/PointsRequest", "examples": [{"spec": {"outer": {"axis": "y", "start": 0.0, "stop": 10.0, "num": 3, "type": "Line"}, "inner": {"axis": "x", "start": 0.0, "stop": 10.0, "num": 4, "type": "Line"}, "type": "Product"}, "max_frames": 1024, "format": "FLOAT_LIST"}]}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MidpointsResponse"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/bounds": {"post": {"summary": "Bounds", "description": "Generate bounds from a scanspec.\n\nA scanspec can produce points with lower and upper bounds.\n\nArgs:\n request: Scanspec and formatting info.\n\nReturns:\n BoundsResponse: Bounds of the scan", "operationId": "bounds_bounds_post", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/PointsRequest", "examples": [{"spec": {"outer": {"axis": "y", "start": 0.0, "stop": 10.0, "num": 3, "type": "Line"}, "inner": {"axis": "x", "start": 0.0, "stop": 10.0, "num": 4, "type": "Line"}, "type": "Product"}, "max_frames": 1024, "format": "FLOAT_LIST"}]}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BoundsResponse"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/gap": {"post": {"summary": "Gap", "description": "Generate gaps from a scanspec.\n\nA scanspec may indicate if there is a gap between two frames.\nThe array returned corresponds to whether or not there is a gap\nafter each frame.\n\nArgs:\n spec: Scanspec and formatting info.\n\nReturns:\n GapResponse: Bounds of the scan", "operationId": "gap_gap_post", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Spec-Input", "examples": [{"outer": {"axis": "y", "start": 0.0, "stop": 10.0, "num": 3, "type": "Line"}, "inner": {"axis": "x", "start": 0.0, "stop": 10.0, "num": 4, "type": "Line"}, "type": "Product"}]}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/GapResponse"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/smalleststep": {"post": {"summary": "Smallest Step", "description": "Calculate the smallest step in a scan, both absolutely and per-axis.\n\nIgnore any steps of size 0.\n\nArgs:\n spec: The spec of the scan\n\nReturns:\n SmallestStepResponse: A description of the smallest steps in the spec", "operationId": "smallest_step_smalleststep_post", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Spec-Input", "examples": [{"outer": {"axis": "y", "start": 0.0, "stop": 10.0, "num": 3, "type": "Line"}, "inner": {"axis": "x", "start": 0.0, "stop": 10.0, "num": 4, "type": "Line"}, "type": "Product"}]}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SmallestStepResponse"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}}, "components": {"schemas": {"BoundsResponse": {"properties": {"total_frames": {"type": "integer", "title": "Total Frames", "description": "Total number of frames in spec"}, "returned_frames": {"type": "integer", "title": "Returned Frames", "description": "Total of number of frames in this response, may be less than total_frames due to downsampling etc."}, "format": {"$ref": "#/components/schemas/PointsFormat", "description": "Format of returned point data"}, "lower": {"additionalProperties": {"anyOf": [{"type": "string"}, {"items": {"type": "number"}, "type": "array"}]}, "type": "object", "title": "Lower", "description": "Lower bounds of scan frames if different from midpoints"}, "upper": {"additionalProperties": {"anyOf": [{"type": "string"}, {"items": {"type": "number"}, "type": "array"}]}, "type": "object", "title": "Upper", "description": "Upper bounds of scan frames if different from midpoints"}}, "type": "object", "required": ["total_frames", "returned_frames", "format", "lower", "upper"], "title": "BoundsResponse", "description": "Bounds of a generated scan."}, "Circle_str_": {"properties": {"x_axis": {"type": "string", "title": "X Axis", "description": "The name matching the x axis of the spec"}, "y_axis": {"type": "string", "title": "Y Axis", "description": "The name matching the y axis of the spec"}, "x_middle": {"type": "number", "title": "X Middle", "description": "The central x point of the circle"}, "y_middle": {"type": "number", "title": "Y Middle", "description": "The central y point of the circle"}, "radius": {"type": "number", "exclusiveMinimum": 0.0, "title": "Radius", "description": "Radius of the circle"}, "type": {"type": "string", "const": "Circle", "title": "Type", "default": "Circle"}}, "additionalProperties": false, "type": "object", "required": ["x_axis", "y_axis", "x_middle", "y_middle", "radius"], "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)"}, "CombinationOf_str_-Input": {"properties": {"left": {"$ref": "#/components/schemas/Region-Input", "description": "The left-hand Region to combine"}, "right": {"$ref": "#/components/schemas/Region-Input", "description": "The right-hand Region to combine"}, "type": {"type": "string", "const": "CombinationOf", "title": "Type", "default": "CombinationOf"}}, "additionalProperties": false, "type": "object", "required": ["left", "right"], "title": "CombinationOf", "description": "Abstract baseclass for a combination of two regions, left and right."}, "CombinationOf_str_-Output": {"properties": {"left": {"$ref": "#/components/schemas/Region-Output", "description": "The left-hand Region to combine"}, "right": {"$ref": "#/components/schemas/Region-Output", "description": "The right-hand Region to combine"}, "type": {"type": "string", "const": "CombinationOf", "title": "Type", "default": "CombinationOf"}}, "additionalProperties": false, "type": "object", "required": ["left", "right"], "title": "CombinationOf", "description": "Abstract baseclass for a combination of two regions, left and right."}, "Concat_str_-Input": {"properties": {"left": {"$ref": "#/components/schemas/Spec-Input", "description": "The left-hand Spec to Concat, midpoints will appear earlier"}, "right": {"$ref": "#/components/schemas/Spec-Input", "description": "The right-hand Spec to Concat, midpoints will appear later"}, "gap": {"type": "boolean", "title": "Gap", "description": "If True, force a gap in the output at the join", "default": false}, "check_path_changes": {"type": "boolean", "title": "Check Path Changes", "description": "If True path through scan will not be modified by squash", "default": true}, "type": {"type": "string", "const": "Concat", "title": "Type", "default": "Concat"}}, "additionalProperties": false, "type": "object", "required": ["left", "right"], "title": "Concat", "description": "Concatenate two Specs together, running one after the other.\n\nEach Dimension of left and right must contain the same axes. Typically\nformed using `Spec.concat`.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"x\", 1, 3, 3).concat(Line(\"x\", 4, 5, 5))"}, "Concat_str_-Output": {"properties": {"left": {"$ref": "#/components/schemas/Spec-Output", "description": "The left-hand Spec to Concat, midpoints will appear earlier"}, "right": {"$ref": "#/components/schemas/Spec-Output", "description": "The right-hand Spec to Concat, midpoints will appear later"}, "gap": {"type": "boolean", "title": "Gap", "description": "If True, force a gap in the output at the join", "default": false}, "check_path_changes": {"type": "boolean", "title": "Check Path Changes", "description": "If True path through scan will not be modified by squash", "default": true}, "type": {"type": "string", "const": "Concat", "title": "Type", "default": "Concat"}}, "additionalProperties": false, "type": "object", "required": ["left", "right"], "title": "Concat", "description": "Concatenate two Specs together, running one after the other.\n\nEach Dimension of left and right must contain the same axes. Typically\nformed using `Spec.concat`.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"x\", 1, 3, 3).concat(Line(\"x\", 4, 5, 5))"}, "DifferenceOf_str_-Input": {"properties": {"left": {"$ref": "#/components/schemas/Region-Input", "description": "The left-hand Region to combine"}, "right": {"$ref": "#/components/schemas/Region-Input", "description": "The right-hand Region to combine"}, "type": {"type": "string", "const": "DifferenceOf", "title": "Type", "default": "DifferenceOf"}}, "additionalProperties": false, "type": "object", "required": ["left", "right"], "title": "DifferenceOf", "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])"}, "DifferenceOf_str_-Output": {"properties": {"left": {"$ref": "#/components/schemas/Region-Output", "description": "The left-hand Region to combine"}, "right": {"$ref": "#/components/schemas/Region-Output", "description": "The right-hand Region to combine"}, "type": {"type": "string", "const": "DifferenceOf", "title": "Type", "default": "DifferenceOf"}}, "additionalProperties": false, "type": "object", "required": ["left", "right"], "title": "DifferenceOf", "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])"}, "Duration_str_": {"properties": {"duration": {"type": "number", "title": "Duration", "description": "The value at each point"}, "num": {"type": "integer", "minimum": 1.0, "title": "Num", "description": "Number of frames to produce", "default": 1}, "type": {"type": "string", "const": "Duration", "title": "Type", "default": "Duration"}}, "additionalProperties": false, "type": "object", "required": ["duration"], "title": "Duration", "description": "A special spec used to hold information about the duration of each frame."}, "Ellipse_str_": {"properties": {"x_axis": {"type": "string", "title": "X Axis", "description": "The name matching the x axis of the spec"}, "y_axis": {"type": "string", "title": "Y Axis", "description": "The name matching the y axis of the spec"}, "x_middle": {"type": "number", "title": "X Middle", "description": "The central x point of the ellipse"}, "y_middle": {"type": "number", "title": "Y Middle", "description": "The central y point of the ellipse"}, "x_radius": {"type": "number", "exclusiveMinimum": 0.0, "title": "X Radius", "description": "The radius along the x axis of the ellipse"}, "y_radius": {"type": "number", "exclusiveMinimum": 0.0, "title": "Y Radius", "description": "The radius along the y axis of the ellipse"}, "angle": {"type": "number", "title": "Angle", "description": "The angle of the ellipse (degrees)", "default": 0.0}, "type": {"type": "string", "const": "Ellipse", "title": "Type", "default": "Ellipse"}}, "additionalProperties": false, "type": "object", "required": ["x_axis", "y_axis", "x_middle", "y_middle", "x_radius", "y_radius"], "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)"}, "GapResponse": {"properties": {"gap": {"items": {"type": "boolean"}, "type": "array", "title": "Gap", "description": "Boolean array indicating if there is a gap between each frame"}}, "type": "object", "required": ["gap"], "title": "GapResponse", "description": "Presence of gaps in a generated scan."}, "HTTPValidationError": {"properties": {"detail": {"items": {"$ref": "#/components/schemas/ValidationError"}, "type": "array", "title": "Detail"}}, "type": "object", "title": "HTTPValidationError"}, "IntersectionOf_str_-Input": {"properties": {"left": {"$ref": "#/components/schemas/Region-Input", "description": "The left-hand Region to combine"}, "right": {"$ref": "#/components/schemas/Region-Input", "description": "The right-hand Region to combine"}, "type": {"type": "string", "const": "IntersectionOf", "title": "Type", "default": "IntersectionOf"}}, "additionalProperties": false, "type": "object", "required": ["left", "right"], "title": "IntersectionOf", "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])"}, "IntersectionOf_str_-Output": {"properties": {"left": {"$ref": "#/components/schemas/Region-Output", "description": "The left-hand Region to combine"}, "right": {"$ref": "#/components/schemas/Region-Output", "description": "The right-hand Region to combine"}, "type": {"type": "string", "const": "IntersectionOf", "title": "Type", "default": "IntersectionOf"}}, "additionalProperties": false, "type": "object", "required": ["left", "right"], "title": "IntersectionOf", "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])"}, "Line_str_": {"properties": {"axis": {"type": "string", "title": "Axis", "description": "An identifier for what to move"}, "start": {"type": "number", "title": "Start", "description": "Midpoint of the first point of the line"}, "stop": {"type": "number", "title": "Stop", "description": "Midpoint of the last point of the line"}, "num": {"type": "integer", "minimum": 1.0, "title": "Num", "description": "Number of frames to produce"}, "type": {"type": "string", "const": "Line", "title": "Type", "default": "Line"}}, "additionalProperties": false, "type": "object", "required": ["axis", "start", "stop", "num"], "title": "Line", "description": "Linearly spaced frames with start and stop as first and last midpoints.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"x\", 1, 2, 5)"}, "Mask_str_-Input": {"properties": {"spec": {"$ref": "#/components/schemas/Spec-Input", "description": "The Spec containing the source midpoints"}, "region": {"$ref": "#/components/schemas/Region-Input", "description": "The Region that midpoints will be inside"}, "check_path_changes": {"type": "boolean", "title": "Check Path Changes", "description": "If True path through scan will not be modified by squash", "default": true}, "type": {"type": "string", "const": "Mask", "title": "Type", "default": "Mask"}}, "additionalProperties": false, "type": "object", "required": ["spec", "region"], "title": "Mask", "description": "Restrict Spec to only midpoints that fall inside the given Region.\n\nTypically created with the ``&`` operator. It also pushes down the\n``& | ^ -`` operators to its `Region` to avoid the need for brackets on\ncombinations of Regions.\n\nIf a Region spans multiple Dimension objects, they will be squashed together.\n\n.. example_spec::\n\n from scanspec.regions import Circle\n from scanspec.specs import Line\n\n spec = Line(\"y\", 1, 3, 3) * Line(\"x\", 3, 5, 5) & Circle(\"x\", \"y\", 4, 2, 1.2)\n\nSee Also: `why-squash-can-change-path`"}, "Mask_str_-Output": {"properties": {"spec": {"$ref": "#/components/schemas/Spec-Output", "description": "The Spec containing the source midpoints"}, "region": {"$ref": "#/components/schemas/Region-Output", "description": "The Region that midpoints will be inside"}, "check_path_changes": {"type": "boolean", "title": "Check Path Changes", "description": "If True path through scan will not be modified by squash", "default": true}, "type": {"type": "string", "const": "Mask", "title": "Type", "default": "Mask"}}, "additionalProperties": false, "type": "object", "required": ["spec", "region"], "title": "Mask", "description": "Restrict Spec to only midpoints that fall inside the given Region.\n\nTypically created with the ``&`` operator. It also pushes down the\n``& | ^ -`` operators to its `Region` to avoid the need for brackets on\ncombinations of Regions.\n\nIf a Region spans multiple Dimension objects, they will be squashed together.\n\n.. example_spec::\n\n from scanspec.regions import Circle\n from scanspec.specs import Line\n\n spec = Line(\"y\", 1, 3, 3) * Line(\"x\", 3, 5, 5) & Circle(\"x\", \"y\", 4, 2, 1.2)\n\nSee Also: `why-squash-can-change-path`"}, "MidpointsResponse": {"properties": {"total_frames": {"type": "integer", "title": "Total Frames", "description": "Total number of frames in spec"}, "returned_frames": {"type": "integer", "title": "Returned Frames", "description": "Total of number of frames in this response, may be less than total_frames due to downsampling etc."}, "format": {"$ref": "#/components/schemas/PointsFormat", "description": "Format of returned point data"}, "midpoints": {"additionalProperties": {"anyOf": [{"type": "string"}, {"items": {"type": "number"}, "type": "array"}]}, "type": "object", "title": "Midpoints", "description": "The midpoints of scan frames for each axis"}}, "type": "object", "required": ["total_frames", "returned_frames", "format", "midpoints"], "title": "MidpointsResponse", "description": "Midpoints of a generated scan."}, "PointsFormat": {"type": "string", "enum": ["STRING", "FLOAT_LIST", "BASE64_ENCODED"], "title": "PointsFormat", "description": "Formats in which we can return points."}, "PointsRequest": {"properties": {"spec": {"$ref": "#/components/schemas/Spec-Input", "description": "The spec from which to generate points"}, "max_frames": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Max Frames", "description": "The maximum number of points to return, if None will return as many as calculated", "default": 100000}, "format": {"$ref": "#/components/schemas/PointsFormat", "description": "The format in which to output the points data", "default": "FLOAT_LIST"}}, "type": "object", "required": ["spec"], "title": "PointsRequest", "description": "A request for generated scan points."}, "Polygon_str_": {"properties": {"x_axis": {"type": "string", "title": "X Axis", "description": "The name matching the x axis of the spec"}, "y_axis": {"type": "string", "title": "Y Axis", "description": "The name matching the y axis of the spec"}, "x_verts": {"items": {"type": "number"}, "type": "array", "minItems": 3, "title": "X Verts", "description": "The Nx1 x coordinates of the polygons vertices"}, "y_verts": {"items": {"type": "number"}, "type": "array", "minItems": 3, "title": "Y Verts", "description": "The Nx1 y coordinates of the polygons vertices"}, "type": {"type": "string", "const": "Polygon", "title": "Type", "default": "Polygon"}}, "additionalProperties": false, "type": "object", "required": ["x_axis", "y_axis", "x_verts", "y_verts"], "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])"}, "Product_str_-Input": {"properties": {"outer": {"$ref": "#/components/schemas/Spec-Input", "description": "Will be executed once"}, "inner": {"$ref": "#/components/schemas/Spec-Input", "description": "Will be executed len(outer) times"}, "type": {"type": "string", "const": "Product", "title": "Type", "default": "Product"}}, "additionalProperties": false, "type": "object", "required": ["outer", "inner"], "title": "Product", "description": "Outer product of two Specs, nesting inner within outer.\n\nThis means that inner will run in its entirety at each point in outer.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"y\", 1, 2, 3) * Line(\"x\", 3, 4, 12)"}, "Product_str_-Output": {"properties": {"outer": {"$ref": "#/components/schemas/Spec-Output", "description": "Will be executed once"}, "inner": {"$ref": "#/components/schemas/Spec-Output", "description": "Will be executed len(outer) times"}, "type": {"type": "string", "const": "Product", "title": "Type", "default": "Product"}}, "additionalProperties": false, "type": "object", "required": ["outer", "inner"], "title": "Product", "description": "Outer product of two Specs, nesting inner within outer.\n\nThis means that inner will run in its entirety at each point in outer.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"y\", 1, 2, 3) * Line(\"x\", 3, 4, 12)"}, "Range_str_": {"properties": {"axis": {"type": "string", "title": "Axis", "description": "The name matching the axis to mask in spec"}, "min": {"type": "number", "title": "Min", "description": "The minimum inclusive value in the region"}, "max": {"type": "number", "title": "Max", "description": "The minimum inclusive value in the region"}, "type": {"type": "string", "const": "Range", "title": "Type", "default": "Range"}}, "additionalProperties": false, "type": "object", "required": ["axis", "min", "max"], "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])"}, "Rectangle_str_": {"properties": {"x_axis": {"type": "string", "title": "X Axis", "description": "The name matching the x axis of the spec"}, "y_axis": {"type": "string", "title": "Y Axis", "description": "The name matching the y axis of the spec"}, "x_min": {"type": "number", "title": "X Min", "description": "Minimum inclusive x value in the region"}, "y_min": {"type": "number", "title": "Y Min", "description": "Minimum inclusive y value in the region"}, "x_max": {"type": "number", "title": "X Max", "description": "Maximum inclusive x value in the region"}, "y_max": {"type": "number", "title": "Y Max", "description": "Maximum inclusive y value in the region"}, "angle": {"type": "number", "title": "Angle", "description": "Clockwise rotation angle of the rectangle", "default": 0.0}, "type": {"type": "string", "const": "Rectangle", "title": "Type", "default": "Rectangle"}}, "additionalProperties": false, "type": "object", "required": ["x_axis", "y_axis", "x_min", "y_min", "x_max", "y_max"], "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)"}, "Region-Input": {"oneOf": [{"$ref": "#/components/schemas/CombinationOf_str_-Input"}, {"$ref": "#/components/schemas/UnionOf_str_-Input"}, {"$ref": "#/components/schemas/IntersectionOf_str_-Input"}, {"$ref": "#/components/schemas/DifferenceOf_str_-Input"}, {"$ref": "#/components/schemas/SymmetricDifferenceOf_str_-Input"}, {"$ref": "#/components/schemas/Range_str_"}, {"$ref": "#/components/schemas/Rectangle_str_"}, {"$ref": "#/components/schemas/Polygon_str_"}, {"$ref": "#/components/schemas/Circle_str_"}, {"$ref": "#/components/schemas/Ellipse_str_"}], "discriminator": {"propertyName": "type", "mapping": {"Circle": "#/components/schemas/Circle_str_", "CombinationOf": "#/components/schemas/CombinationOf_str_-Input", "DifferenceOf": "#/components/schemas/DifferenceOf_str_-Input", "Ellipse": "#/components/schemas/Ellipse_str_", "IntersectionOf": "#/components/schemas/IntersectionOf_str_-Input", "Polygon": "#/components/schemas/Polygon_str_", "Range": "#/components/schemas/Range_str_", "Rectangle": "#/components/schemas/Rectangle_str_", "SymmetricDifferenceOf": "#/components/schemas/SymmetricDifferenceOf_str_-Input", "UnionOf": "#/components/schemas/UnionOf_str_-Input"}}}, "Region-Output": {"oneOf": [{"$ref": "#/components/schemas/CombinationOf_str_-Output"}, {"$ref": "#/components/schemas/UnionOf_str_-Output"}, {"$ref": "#/components/schemas/IntersectionOf_str_-Output"}, {"$ref": "#/components/schemas/DifferenceOf_str_-Output"}, {"$ref": "#/components/schemas/SymmetricDifferenceOf_str_-Output"}, {"$ref": "#/components/schemas/Range_str_"}, {"$ref": "#/components/schemas/Rectangle_str_"}, {"$ref": "#/components/schemas/Polygon_str_"}, {"$ref": "#/components/schemas/Circle_str_"}, {"$ref": "#/components/schemas/Ellipse_str_"}], "discriminator": {"propertyName": "type", "mapping": {"Circle": "#/components/schemas/Circle_str_", "CombinationOf": "#/components/schemas/CombinationOf_str_-Output", "DifferenceOf": "#/components/schemas/DifferenceOf_str_-Output", "Ellipse": "#/components/schemas/Ellipse_str_", "IntersectionOf": "#/components/schemas/IntersectionOf_str_-Output", "Polygon": "#/components/schemas/Polygon_str_", "Range": "#/components/schemas/Range_str_", "Rectangle": "#/components/schemas/Rectangle_str_", "SymmetricDifferenceOf": "#/components/schemas/SymmetricDifferenceOf_str_-Output", "UnionOf": "#/components/schemas/UnionOf_str_-Output"}}}, "Repeat_str_": {"properties": {"num": {"type": "integer", "minimum": 1.0, "title": "Num", "description": "Number of frames to produce"}, "gap": {"type": "boolean", "title": "Gap", "description": "If False and the slowest of the stack of Dimension is snaked then the end and start of consecutive iterations of Spec will have no gap", "default": true}, "type": {"type": "string", "const": "Repeat", "title": "Type", "default": "Repeat"}}, "additionalProperties": false, "type": "object", "required": ["num"], "title": "Repeat", "description": "Repeat an empty frame num times.\n\nCan be used on the outside of a scan to repeat the same scan many times.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = 2 * ~Line.bounded(\"x\", 3, 4, 1)\n\nIf you want snaked axes to have no gap between iterations you can do:\n\n.. example_spec::\n\n from scanspec.specs import Line, Repeat\n\n spec = Repeat(2, gap=False) * ~Line.bounded(\"x\", 3, 4, 1)\n\n.. note:: There is no turnaround arrow at x=4"}, "SmallestStepResponse": {"properties": {"absolute": {"type": "number", "title": "Absolute", "description": "Absolute smallest distance between two points on a single axis"}, "per_axis": {"additionalProperties": {"type": "number"}, "type": "object", "title": "Per Axis", "description": "Smallest distance between two points on each axis"}}, "type": "object", "required": ["absolute", "per_axis"], "title": "SmallestStepResponse", "description": "Information about the smallest steps between points in a spec."}, "Snake_str_-Input": {"properties": {"spec": {"$ref": "#/components/schemas/Spec-Input", "description": "The Spec to run in reverse every other iteration"}, "type": {"type": "string", "const": "Snake", "title": "Type", "default": "Snake"}}, "additionalProperties": false, "type": "object", "required": ["spec"], "title": "Snake", "description": "Run the Spec in reverse on every other iteration when nested.\n\nTypically created with the ``~`` operator.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"y\", 1, 3, 3) * ~Line(\"x\", 3, 5, 5)"}, "Snake_str_-Output": {"properties": {"spec": {"$ref": "#/components/schemas/Spec-Output", "description": "The Spec to run in reverse every other iteration"}, "type": {"type": "string", "const": "Snake", "title": "Type", "default": "Snake"}}, "additionalProperties": false, "type": "object", "required": ["spec"], "title": "Snake", "description": "Run the Spec in reverse on every other iteration when nested.\n\nTypically created with the ``~`` operator.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"y\", 1, 3, 3) * ~Line(\"x\", 3, 5, 5)"}, "Spec-Input": {"oneOf": [{"$ref": "#/components/schemas/Product_str_-Input"}, {"$ref": "#/components/schemas/Repeat_str_"}, {"$ref": "#/components/schemas/Zip_str_-Input"}, {"$ref": "#/components/schemas/Mask_str_-Input"}, {"$ref": "#/components/schemas/Snake_str_-Input"}, {"$ref": "#/components/schemas/Concat_str_-Input"}, {"$ref": "#/components/schemas/Squash_str_-Input"}, {"$ref": "#/components/schemas/Line_str_"}, {"$ref": "#/components/schemas/Duration_str_"}, {"$ref": "#/components/schemas/Static_str_"}, {"$ref": "#/components/schemas/Spiral_str_"}], "discriminator": {"propertyName": "type", "mapping": {"Concat": "#/components/schemas/Concat_str_-Input", "Duration": "#/components/schemas/Duration_str_", "Line": "#/components/schemas/Line_str_", "Mask": "#/components/schemas/Mask_str_-Input", "Product": "#/components/schemas/Product_str_-Input", "Repeat": "#/components/schemas/Repeat_str_", "Snake": "#/components/schemas/Snake_str_-Input", "Spiral": "#/components/schemas/Spiral_str_", "Squash": "#/components/schemas/Squash_str_-Input", "Static": "#/components/schemas/Static_str_", "Zip": "#/components/schemas/Zip_str_-Input"}}}, "Spec-Output": {"oneOf": [{"$ref": "#/components/schemas/Product_str_-Output"}, {"$ref": "#/components/schemas/Repeat_str_"}, {"$ref": "#/components/schemas/Zip_str_-Output"}, {"$ref": "#/components/schemas/Mask_str_-Output"}, {"$ref": "#/components/schemas/Snake_str_-Output"}, {"$ref": "#/components/schemas/Concat_str_-Output"}, {"$ref": "#/components/schemas/Squash_str_-Output"}, {"$ref": "#/components/schemas/Line_str_"}, {"$ref": "#/components/schemas/Duration_str_"}, {"$ref": "#/components/schemas/Static_str_"}, {"$ref": "#/components/schemas/Spiral_str_"}], "discriminator": {"propertyName": "type", "mapping": {"Concat": "#/components/schemas/Concat_str_-Output", "Duration": "#/components/schemas/Duration_str_", "Line": "#/components/schemas/Line_str_", "Mask": "#/components/schemas/Mask_str_-Output", "Product": "#/components/schemas/Product_str_-Output", "Repeat": "#/components/schemas/Repeat_str_", "Snake": "#/components/schemas/Snake_str_-Output", "Spiral": "#/components/schemas/Spiral_str_", "Squash": "#/components/schemas/Squash_str_-Output", "Static": "#/components/schemas/Static_str_", "Zip": "#/components/schemas/Zip_str_-Output"}}}, "Spiral_str_": {"properties": {"x_axis": {"type": "string", "title": "X Axis", "description": "An identifier for what to move for x"}, "y_axis": {"type": "string", "title": "Y Axis", "description": "An identifier for what to move for y"}, "x_start": {"type": "number", "title": "X Start", "description": "x centre of the spiral"}, "y_start": {"type": "number", "title": "Y Start", "description": "y centre of the spiral"}, "x_range": {"type": "number", "title": "X Range", "description": "x width of the spiral"}, "y_range": {"type": "number", "title": "Y Range", "description": "y width of the spiral"}, "num": {"type": "integer", "minimum": 1.0, "title": "Num", "description": "Number of frames to produce"}, "rotate": {"type": "number", "title": "Rotate", "description": "How much to rotate the angle of the spiral", "default": 0.0}, "type": {"type": "string", "const": "Spiral", "title": "Type", "default": "Spiral"}}, "additionalProperties": false, "type": "object", "required": ["x_axis", "y_axis", "x_start", "y_start", "x_range", "y_range", "num"], "title": "Spiral", "description": "Archimedean spiral of \"x_axis\" and \"y_axis\".\n\nStarts at centre point (\"x_start\", \"y_start\") with angle \"rotate\". Produces\n\"num\" points in a spiral spanning width of \"x_range\" and height of \"y_range\"\n\n.. example_spec::\n\n from scanspec.specs import Spiral\n\n spec = Spiral(\"x\", \"y\", 1, 5, 10, 50, 30)"}, "Squash_str_-Input": {"properties": {"spec": {"$ref": "#/components/schemas/Spec-Input", "description": "The Spec to squash the dimensions of"}, "check_path_changes": {"type": "boolean", "title": "Check Path Changes", "description": "If True path through scan will not be modified by squash", "default": true}, "type": {"type": "string", "const": "Squash", "title": "Type", "default": "Squash"}}, "additionalProperties": false, "type": "object", "required": ["spec"], "title": "Squash", "description": "Squash a stack of Dimension together into a single expanded Dimension object.\n\nSee Also:\n `why-squash-can-change-path`\n\n.. example_spec::\n\n from scanspec.specs import Line, Squash\n\n spec = Squash(Line(\"y\", 1, 2, 3) * Line(\"x\", 0, 1, 4))"}, "Squash_str_-Output": {"properties": {"spec": {"$ref": "#/components/schemas/Spec-Output", "description": "The Spec to squash the dimensions of"}, "check_path_changes": {"type": "boolean", "title": "Check Path Changes", "description": "If True path through scan will not be modified by squash", "default": true}, "type": {"type": "string", "const": "Squash", "title": "Type", "default": "Squash"}}, "additionalProperties": false, "type": "object", "required": ["spec"], "title": "Squash", "description": "Squash a stack of Dimension together into a single expanded Dimension object.\n\nSee Also:\n `why-squash-can-change-path`\n\n.. example_spec::\n\n from scanspec.specs import Line, Squash\n\n spec = Squash(Line(\"y\", 1, 2, 3) * Line(\"x\", 0, 1, 4))"}, "Static_str_": {"properties": {"axis": {"type": "string", "title": "Axis", "description": "An identifier for what to move"}, "value": {"type": "number", "title": "Value", "description": "The value at each point"}, "num": {"type": "integer", "minimum": 1.0, "title": "Num", "description": "Number of frames to produce", "default": 1}, "type": {"type": "string", "const": "Static", "title": "Type", "default": "Static"}}, "additionalProperties": false, "type": "object", "required": ["axis", "value"], "title": "Static", "description": "A static frame, repeated num times, with axis at value.\n\nCan be used to set axis=value at every point in a scan.\n\n.. example_spec::\n\n from scanspec.specs import Line, Static\n\n spec = Line(\"y\", 1, 2, 3).zip(Static(\"x\", 3))"}, "SymmetricDifferenceOf_str_-Input": {"properties": {"left": {"$ref": "#/components/schemas/Region-Input", "description": "The left-hand Region to combine"}, "right": {"$ref": "#/components/schemas/Region-Input", "description": "The right-hand Region to combine"}, "type": {"type": "string", "const": "SymmetricDifferenceOf", "title": "Type", "default": "SymmetricDifferenceOf"}}, "additionalProperties": false, "type": "object", "required": ["left", "right"], "title": "SymmetricDifferenceOf", "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])"}, "SymmetricDifferenceOf_str_-Output": {"properties": {"left": {"$ref": "#/components/schemas/Region-Output", "description": "The left-hand Region to combine"}, "right": {"$ref": "#/components/schemas/Region-Output", "description": "The right-hand Region to combine"}, "type": {"type": "string", "const": "SymmetricDifferenceOf", "title": "Type", "default": "SymmetricDifferenceOf"}}, "additionalProperties": false, "type": "object", "required": ["left", "right"], "title": "SymmetricDifferenceOf", "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])"}, "UnionOf_str_-Input": {"properties": {"left": {"$ref": "#/components/schemas/Region-Input", "description": "The left-hand Region to combine"}, "right": {"$ref": "#/components/schemas/Region-Input", "description": "The right-hand Region to combine"}, "type": {"type": "string", "const": "UnionOf", "title": "Type", "default": "UnionOf"}}, "additionalProperties": false, "type": "object", "required": ["left", "right"], "title": "UnionOf", "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])"}, "UnionOf_str_-Output": {"properties": {"left": {"$ref": "#/components/schemas/Region-Output", "description": "The left-hand Region to combine"}, "right": {"$ref": "#/components/schemas/Region-Output", "description": "The right-hand Region to combine"}, "type": {"type": "string", "const": "UnionOf", "title": "Type", "default": "UnionOf"}}, "additionalProperties": false, "type": "object", "required": ["left", "right"], "title": "UnionOf", "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])"}, "ValidResponse": {"properties": {"input_spec": {"$ref": "#/components/schemas/Spec-Output", "description": "The input scanspec"}, "valid_spec": {"$ref": "#/components/schemas/Spec-Output", "description": "The validated version of the spec"}}, "type": "object", "required": ["input_spec", "valid_spec"], "title": "ValidResponse", "description": "Response model for spec validation."}, "ValidationError": {"properties": {"loc": {"items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, "type": "array", "title": "Location"}, "msg": {"type": "string", "title": "Message"}, "type": {"type": "string", "title": "Error Type"}}, "type": "object", "required": ["loc", "msg", "type"], "title": "ValidationError"}, "Zip_str_-Input": {"properties": {"left": {"$ref": "#/components/schemas/Spec-Input", "description": "The left-hand Spec to Zip, will appear earlier in axes"}, "right": {"$ref": "#/components/schemas/Spec-Input", "description": "The right-hand Spec to Zip, will appear later in axes"}, "type": {"type": "string", "const": "Zip", "title": "Type", "default": "Zip"}}, "additionalProperties": false, "type": "object", "required": ["left", "right"], "title": "Zip", "description": "Run two Specs in parallel, merging their midpoints together.\n\nTypically formed using `Spec.zip`.\n\nStacks of Dimension are merged by:\n\n- If right creates a stack of a single Dimension object of size 1, expand it to\n the size of the fastest Dimension object created by left\n- Merge individual Dimension objects together from fastest to slowest\n\nThis means that Zipping a Spec producing stack [l2, l1] with a Spec\nproducing stack [r1] will assert len(l1)==len(r1), and produce\nstack [l2, l1.zip(r1)].\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"z\", 1, 2, 3) * Line(\"y\", 3, 4, 5).zip(Line(\"x\", 4, 5, 5))"}, "Zip_str_-Output": {"properties": {"left": {"$ref": "#/components/schemas/Spec-Output", "description": "The left-hand Spec to Zip, will appear earlier in axes"}, "right": {"$ref": "#/components/schemas/Spec-Output", "description": "The right-hand Spec to Zip, will appear later in axes"}, "type": {"type": "string", "const": "Zip", "title": "Type", "default": "Zip"}}, "additionalProperties": false, "type": "object", "required": ["left", "right"], "title": "Zip", "description": "Run two Specs in parallel, merging their midpoints together.\n\nTypically formed using `Spec.zip`.\n\nStacks of Dimension are merged by:\n\n- If right creates a stack of a single Dimension object of size 1, expand it to\n the size of the fastest Dimension object created by left\n- Merge individual Dimension objects together from fastest to slowest\n\nThis means that Zipping a Spec producing stack [l2, l1] with a Spec\nproducing stack [r1] will assert len(l1)==len(r1), and produce\nstack [l2, l1.zip(r1)].\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"z\", 1, 2, 3) * Line(\"y\", 3, 4, 5).zip(Line(\"x\", 4, 5, 5))"}}}} diff --git a/src/scanspec/core.py b/src/scanspec/core.py index 1fe24170..9bf2ba3d 100644 --- a/src/scanspec/core.py +++ b/src/scanspec/core.py @@ -367,7 +367,9 @@ def __init__( #: 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: + # If midpoints are provided and we don't have a gap array + # calculate it based on the midpoints + elif gap is None and len(self.midpoints) > 0: # Need to calculate gap as not passed one # We have a gap if upper[i] != lower[i+1] for any axes axes_gap = [ @@ -377,21 +379,24 @@ def __init__( ) ] self.gap = np.logical_or.reduce(axes_gap) + # If only duratiotn is provided we need to make gap have the same shape + # as the duration provided for the __len__ mmethod + elif gap is None and self.duration is not None and len(self.midpoints) == 0: + self.gap = np.full(len(self.duration), False) # 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)}" ) - if len(midpoints) > 0: - # 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)) - else: - lengths = {len(self.duration)} if self.duration is not None else {0} + # 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)) + if self.duration is not None: + lengths = {len(self.duration)} assert len(lengths) <= 1, f"Mismatching lengths {list(lengths)}" def axes(self) -> list[Axis]: @@ -509,8 +514,11 @@ def zip_gap(gaps: Sequence[GapArray]) -> GapArray: def zip_duration( durations: Sequence[DurationArray | None], ) -> DurationArray | None: - # assert len(durations) == 1, "Can't have more than one durations array" - return durations[-1] + # Check if there are more than one duration being zipped + specified_durations = [d for d in durations if d is not None] + if len(specified_durations) != 1: + raise ValueError("Can't have more than one durations array") + return specified_durations[0] return _merge_frames( self, @@ -565,7 +573,7 @@ def stack2dimension( {}, {}, np.zeros(indices.shape, dtype=np.bool_), - np.zeros(indices.shape, dtype=np.float64), + None, ) # Example numbers below from a 2x3x4 ZxYxX scan for i, frames in enumerate(dimensions): diff --git a/src/scanspec/specs.py b/src/scanspec/specs.py index 9e7ce0fb..51c1fb27 100644 --- a/src/scanspec/specs.py +++ b/src/scanspec/specs.py @@ -244,13 +244,6 @@ def calculate( # noqa: D102 # Take the 0th element N times to make a repeated Dimension object indices = np.zeros(len(frames_left[-1]), dtype=np.int8) repeated = frames_right[0].extract(indices) - # Check if frames_right is a Duration type frame - if ( - len(repeated.midpoints) == 0 - and repeated.duration is not None - and len(repeated.duration) != 0 - ): - repeated.gap = np.full(len(repeated.duration), False) if isinstance(frames_left[-1], SnakedDimension): repeated = SnakedDimension.from_frames(repeated) @@ -561,20 +554,12 @@ def bounded( class Duration(Spec[Axis]): """A special spec used to hold information about the duration of each frame.""" - value: float = Field(description="The value at each point") + duration: float = Field(description="The value at each point") num: int = Field(ge=1, description="Number of frames to produce", default=1) def axes(self) -> list[Axis]: # noqa: D102 return [] - @classmethod - def redefine( - cls: type[Duration[Any]], - duration: float = Field(description="The duration of each static point"), - num: int = Field(ge=1, description="Number of frames to produce", default=1), - ) -> Duration[str]: - return Duration(duration, num) - def calculate( self, bounds: bool = True, nested: bool = False ) -> list[Dimension[Axis]]: @@ -583,8 +568,8 @@ def calculate( midpoints={}, lower=None, upper=None, - gap=np.full(self.num, False), - duration=np.full(self.num, self.value), + gap=None, + duration=np.full(self.num, self.duration), ) ] @@ -754,7 +739,7 @@ def fly(spec: Spec[Axis], duration: float) -> Spec[Axis | str]: spec = fly(Line("x", 1, 2, 3), 0.1) """ - return spec.zip(Duration(duration, 1)) + return spec.zip(Duration(duration)) def step(spec: Spec[Axis], duration: float, num: int = 1) -> Spec[Axis | str]: @@ -773,7 +758,7 @@ def step(spec: Spec[Axis], duration: float, num: int = 1) -> Spec[Axis | str]: spec = step(Line("x", 1, 2, 3), 0.1) """ - return spec * Duration(duration, 1) + return spec * Duration(duration) def get_constant_duration(frames: list[Dimension[Any]]) -> float | None: diff --git a/tests/test_specs.py b/tests/test_specs.py index 69aea3e5..e35c3b36 100644 --- a/tests/test_specs.py +++ b/tests/test_specs.py @@ -64,9 +64,7 @@ def test_two_point_stepped_line() -> None: dimx, dimt = inst.calculate() assert dimx.midpoints == dimx.lower == dimx.upper == {x: approx([0, 1])} assert dimt.midpoints == dimt.lower == dimt.upper == {} - assert dimt.duration == approx( - [0.1] - ) # Don't think this is how it's supposed to work + assert dimt.duration == approx([0.1]) assert inst.frames().gap == ints("11") From 3c51577d01f4fabbfa61306e20cb3fe62625a73c Mon Sep 17 00:00:00 2001 From: Luis Segalla Date: Tue, 15 Jul 2025 16:00:49 +0100 Subject: [PATCH 04/38] Removed DURATION and updated concat concat method --- src/scanspec/core.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/scanspec/core.py b/src/scanspec/core.py index 9bf2ba3d..1f6cf9cb 100644 --- a/src/scanspec/core.py +++ b/src/scanspec/core.py @@ -55,9 +55,6 @@ def get_original_bases(cls: type, /) -> tuple[Any, ...]: "Slice", ] -#: Can be used as a special key to indicate how long each point should be -DURATION = "DURATION" - #: Used to ensure pydantic dataclasses error if given extra arguments StrictConfig: ConfigDict = {"extra": "forbid", "arbitrary_types_allowed": True} @@ -331,6 +328,7 @@ class Dimension(Generic[Axis]): 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 + duration: If supplied, defines the duration between each lower and upper point. Typically used in two ways: @@ -380,7 +378,7 @@ def __init__( ] self.gap = np.logical_or.reduce(axes_gap) # If only duratiotn is provided we need to make gap have the same shape - # as the duration provided for the __len__ mmethod + # as the duration provided for the __len__ method elif gap is None and self.duration is not None and len(self.midpoints) == 0: self.gap = np.full(len(self.duration), False) # Check all axes and ordering are the same @@ -486,7 +484,21 @@ def concat_gap(gaps: Sequence[GapArray]) -> GapArray: 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) + def concat_duration( + durations: Sequence[DurationArray | None], + ) -> DurationArray | None: + # Check if there are more than one duration being zipped + specified_durations = [d for d in durations if d is not None] + d = np.concatenate(specified_durations) + return d + + return _merge_frames( + self, + other, + dict_merge=concat_dict, + gap_merge=concat_gap, + duration_merge=concat_duration, + ) def zip(self, other: Dimension[Axis]) -> Dimension[Axis]: """Return a new Dimension object merging self and other. From 13f38b318449b5e10b0d9ec8288ee297e816f260 Mon Sep 17 00:00:00 2001 From: Luis Segalla Date: Tue, 15 Jul 2025 16:01:15 +0100 Subject: [PATCH 05/38] docstring tidy up --- docs/explanations/technical-terms.rst | 4 ++-- docs/explanations/why-squash-can-change-path.rst | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/explanations/technical-terms.rst b/docs/explanations/technical-terms.rst index 6e46b064..098691a8 100644 --- a/docs/explanations/technical-terms.rst +++ b/docs/explanations/technical-terms.rst @@ -30,7 +30,7 @@ as `Dimension.lower`, `Dimension.midpoints` and `Dimension.upper`. .. _frame_: Dimension ------ +--------- A vector of three `Points ` in scan space: lower, midpoint, upper. They describe the trajectory that should be taken while a detector is active while @@ -40,7 +40,7 @@ section. .. _stack_: Stack of Dimensions ---------------- +------------------- A repeatable, possibly snaking, series of `Dimension` along a number of `Axes `. In the diagram above, the whole Line produces a single `Dimension` diff --git a/docs/explanations/why-squash-can-change-path.rst b/docs/explanations/why-squash-can-change-path.rst index 43130d46..7323c5fb 100644 --- a/docs/explanations/why-squash-can-change-path.rst +++ b/docs/explanations/why-squash-can-change-path.rst @@ -17,7 +17,7 @@ is the case. will fail with `ValueError` Squash unsnaked axis into a snaked Dimensions ------------------------------------------ +--------------------------------------------- Squashing Dimension objects together will take the snake setting of the slowest moving Dimension object in the squash. If this squashed Dimension object is nested From 301d34a9065d58807c503f33ff16526f615bcccc Mon Sep 17 00:00:00 2001 From: Luis Segalla Date: Fri, 18 Jul 2025 13:45:21 +0100 Subject: [PATCH 06/38] Updated schema --- schema.json | 1857 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 1856 insertions(+), 1 deletion(-) diff --git a/schema.json b/schema.json index 650e612a..e897e03b 100644 --- a/schema.json +++ b/schema.json @@ -1 +1,1856 @@ -{"openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.1"}, "paths": {"/valid": {"post": {"summary": "Valid", "description": "Validate wether a ScanSpec[str] can produce a viable scan.\n\nArgs:\n spec: The scanspec to validate\n\nReturns:\n ValidResponse: A canonical version of the spec if it is valid.\n An error otherwise.", "operationId": "valid_valid_post", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Spec-Input", "examples": [{"outer": {"axis": "y", "start": 0.0, "stop": 10.0, "num": 3, "type": "Line"}, "inner": {"axis": "x", "start": 0.0, "stop": 10.0, "num": 4, "type": "Line"}, "type": "Product"}]}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ValidResponse"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/midpoints": {"post": {"summary": "Midpoints", "description": "Generate midpoints from a scanspec.\n\nA scanspec can produce bounded points (i.e. a point is valid if an\naxis is between a minimum and and a maximum, see /bounds). The midpoints\nare the middle of each set of bounds.\n\nArgs:\n request: Scanspec and formatting info.\n\nReturns:\n MidpointsResponse: Midpoints of the scan", "operationId": "midpoints_midpoints_post", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/PointsRequest", "examples": [{"spec": {"outer": {"axis": "y", "start": 0.0, "stop": 10.0, "num": 3, "type": "Line"}, "inner": {"axis": "x", "start": 0.0, "stop": 10.0, "num": 4, "type": "Line"}, "type": "Product"}, "max_frames": 1024, "format": "FLOAT_LIST"}]}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MidpointsResponse"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/bounds": {"post": {"summary": "Bounds", "description": "Generate bounds from a scanspec.\n\nA scanspec can produce points with lower and upper bounds.\n\nArgs:\n request: Scanspec and formatting info.\n\nReturns:\n BoundsResponse: Bounds of the scan", "operationId": "bounds_bounds_post", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/PointsRequest", "examples": [{"spec": {"outer": {"axis": "y", "start": 0.0, "stop": 10.0, "num": 3, "type": "Line"}, "inner": {"axis": "x", "start": 0.0, "stop": 10.0, "num": 4, "type": "Line"}, "type": "Product"}, "max_frames": 1024, "format": "FLOAT_LIST"}]}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BoundsResponse"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/gap": {"post": {"summary": "Gap", "description": "Generate gaps from a scanspec.\n\nA scanspec may indicate if there is a gap between two frames.\nThe array returned corresponds to whether or not there is a gap\nafter each frame.\n\nArgs:\n spec: Scanspec and formatting info.\n\nReturns:\n GapResponse: Bounds of the scan", "operationId": "gap_gap_post", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Spec-Input", "examples": [{"outer": {"axis": "y", "start": 0.0, "stop": 10.0, "num": 3, "type": "Line"}, "inner": {"axis": "x", "start": 0.0, "stop": 10.0, "num": 4, "type": "Line"}, "type": "Product"}]}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/GapResponse"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}, "/smalleststep": {"post": {"summary": "Smallest Step", "description": "Calculate the smallest step in a scan, both absolutely and per-axis.\n\nIgnore any steps of size 0.\n\nArgs:\n spec: The spec of the scan\n\nReturns:\n SmallestStepResponse: A description of the smallest steps in the spec", "operationId": "smallest_step_smalleststep_post", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Spec-Input", "examples": [{"outer": {"axis": "y", "start": 0.0, "stop": 10.0, "num": 3, "type": "Line"}, "inner": {"axis": "x", "start": 0.0, "stop": 10.0, "num": 4, "type": "Line"}, "type": "Product"}]}}}, "required": true}, "responses": {"200": {"description": "Successful Response", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SmallestStepResponse"}}}}, "422": {"description": "Validation Error", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/HTTPValidationError"}}}}}}}}, "components": {"schemas": {"BoundsResponse": {"properties": {"total_frames": {"type": "integer", "title": "Total Frames", "description": "Total number of frames in spec"}, "returned_frames": {"type": "integer", "title": "Returned Frames", "description": "Total of number of frames in this response, may be less than total_frames due to downsampling etc."}, "format": {"$ref": "#/components/schemas/PointsFormat", "description": "Format of returned point data"}, "lower": {"additionalProperties": {"anyOf": [{"type": "string"}, {"items": {"type": "number"}, "type": "array"}]}, "type": "object", "title": "Lower", "description": "Lower bounds of scan frames if different from midpoints"}, "upper": {"additionalProperties": {"anyOf": [{"type": "string"}, {"items": {"type": "number"}, "type": "array"}]}, "type": "object", "title": "Upper", "description": "Upper bounds of scan frames if different from midpoints"}}, "type": "object", "required": ["total_frames", "returned_frames", "format", "lower", "upper"], "title": "BoundsResponse", "description": "Bounds of a generated scan."}, "Circle_str_": {"properties": {"x_axis": {"type": "string", "title": "X Axis", "description": "The name matching the x axis of the spec"}, "y_axis": {"type": "string", "title": "Y Axis", "description": "The name matching the y axis of the spec"}, "x_middle": {"type": "number", "title": "X Middle", "description": "The central x point of the circle"}, "y_middle": {"type": "number", "title": "Y Middle", "description": "The central y point of the circle"}, "radius": {"type": "number", "exclusiveMinimum": 0.0, "title": "Radius", "description": "Radius of the circle"}, "type": {"type": "string", "const": "Circle", "title": "Type", "default": "Circle"}}, "additionalProperties": false, "type": "object", "required": ["x_axis", "y_axis", "x_middle", "y_middle", "radius"], "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)"}, "CombinationOf_str_-Input": {"properties": {"left": {"$ref": "#/components/schemas/Region-Input", "description": "The left-hand Region to combine"}, "right": {"$ref": "#/components/schemas/Region-Input", "description": "The right-hand Region to combine"}, "type": {"type": "string", "const": "CombinationOf", "title": "Type", "default": "CombinationOf"}}, "additionalProperties": false, "type": "object", "required": ["left", "right"], "title": "CombinationOf", "description": "Abstract baseclass for a combination of two regions, left and right."}, "CombinationOf_str_-Output": {"properties": {"left": {"$ref": "#/components/schemas/Region-Output", "description": "The left-hand Region to combine"}, "right": {"$ref": "#/components/schemas/Region-Output", "description": "The right-hand Region to combine"}, "type": {"type": "string", "const": "CombinationOf", "title": "Type", "default": "CombinationOf"}}, "additionalProperties": false, "type": "object", "required": ["left", "right"], "title": "CombinationOf", "description": "Abstract baseclass for a combination of two regions, left and right."}, "Concat_str_-Input": {"properties": {"left": {"$ref": "#/components/schemas/Spec-Input", "description": "The left-hand Spec to Concat, midpoints will appear earlier"}, "right": {"$ref": "#/components/schemas/Spec-Input", "description": "The right-hand Spec to Concat, midpoints will appear later"}, "gap": {"type": "boolean", "title": "Gap", "description": "If True, force a gap in the output at the join", "default": false}, "check_path_changes": {"type": "boolean", "title": "Check Path Changes", "description": "If True path through scan will not be modified by squash", "default": true}, "type": {"type": "string", "const": "Concat", "title": "Type", "default": "Concat"}}, "additionalProperties": false, "type": "object", "required": ["left", "right"], "title": "Concat", "description": "Concatenate two Specs together, running one after the other.\n\nEach Dimension of left and right must contain the same axes. Typically\nformed using `Spec.concat`.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"x\", 1, 3, 3).concat(Line(\"x\", 4, 5, 5))"}, "Concat_str_-Output": {"properties": {"left": {"$ref": "#/components/schemas/Spec-Output", "description": "The left-hand Spec to Concat, midpoints will appear earlier"}, "right": {"$ref": "#/components/schemas/Spec-Output", "description": "The right-hand Spec to Concat, midpoints will appear later"}, "gap": {"type": "boolean", "title": "Gap", "description": "If True, force a gap in the output at the join", "default": false}, "check_path_changes": {"type": "boolean", "title": "Check Path Changes", "description": "If True path through scan will not be modified by squash", "default": true}, "type": {"type": "string", "const": "Concat", "title": "Type", "default": "Concat"}}, "additionalProperties": false, "type": "object", "required": ["left", "right"], "title": "Concat", "description": "Concatenate two Specs together, running one after the other.\n\nEach Dimension of left and right must contain the same axes. Typically\nformed using `Spec.concat`.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"x\", 1, 3, 3).concat(Line(\"x\", 4, 5, 5))"}, "DifferenceOf_str_-Input": {"properties": {"left": {"$ref": "#/components/schemas/Region-Input", "description": "The left-hand Region to combine"}, "right": {"$ref": "#/components/schemas/Region-Input", "description": "The right-hand Region to combine"}, "type": {"type": "string", "const": "DifferenceOf", "title": "Type", "default": "DifferenceOf"}}, "additionalProperties": false, "type": "object", "required": ["left", "right"], "title": "DifferenceOf", "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])"}, "DifferenceOf_str_-Output": {"properties": {"left": {"$ref": "#/components/schemas/Region-Output", "description": "The left-hand Region to combine"}, "right": {"$ref": "#/components/schemas/Region-Output", "description": "The right-hand Region to combine"}, "type": {"type": "string", "const": "DifferenceOf", "title": "Type", "default": "DifferenceOf"}}, "additionalProperties": false, "type": "object", "required": ["left", "right"], "title": "DifferenceOf", "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])"}, "Duration_str_": {"properties": {"duration": {"type": "number", "title": "Duration", "description": "The value at each point"}, "num": {"type": "integer", "minimum": 1.0, "title": "Num", "description": "Number of frames to produce", "default": 1}, "type": {"type": "string", "const": "Duration", "title": "Type", "default": "Duration"}}, "additionalProperties": false, "type": "object", "required": ["duration"], "title": "Duration", "description": "A special spec used to hold information about the duration of each frame."}, "Ellipse_str_": {"properties": {"x_axis": {"type": "string", "title": "X Axis", "description": "The name matching the x axis of the spec"}, "y_axis": {"type": "string", "title": "Y Axis", "description": "The name matching the y axis of the spec"}, "x_middle": {"type": "number", "title": "X Middle", "description": "The central x point of the ellipse"}, "y_middle": {"type": "number", "title": "Y Middle", "description": "The central y point of the ellipse"}, "x_radius": {"type": "number", "exclusiveMinimum": 0.0, "title": "X Radius", "description": "The radius along the x axis of the ellipse"}, "y_radius": {"type": "number", "exclusiveMinimum": 0.0, "title": "Y Radius", "description": "The radius along the y axis of the ellipse"}, "angle": {"type": "number", "title": "Angle", "description": "The angle of the ellipse (degrees)", "default": 0.0}, "type": {"type": "string", "const": "Ellipse", "title": "Type", "default": "Ellipse"}}, "additionalProperties": false, "type": "object", "required": ["x_axis", "y_axis", "x_middle", "y_middle", "x_radius", "y_radius"], "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)"}, "GapResponse": {"properties": {"gap": {"items": {"type": "boolean"}, "type": "array", "title": "Gap", "description": "Boolean array indicating if there is a gap between each frame"}}, "type": "object", "required": ["gap"], "title": "GapResponse", "description": "Presence of gaps in a generated scan."}, "HTTPValidationError": {"properties": {"detail": {"items": {"$ref": "#/components/schemas/ValidationError"}, "type": "array", "title": "Detail"}}, "type": "object", "title": "HTTPValidationError"}, "IntersectionOf_str_-Input": {"properties": {"left": {"$ref": "#/components/schemas/Region-Input", "description": "The left-hand Region to combine"}, "right": {"$ref": "#/components/schemas/Region-Input", "description": "The right-hand Region to combine"}, "type": {"type": "string", "const": "IntersectionOf", "title": "Type", "default": "IntersectionOf"}}, "additionalProperties": false, "type": "object", "required": ["left", "right"], "title": "IntersectionOf", "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])"}, "IntersectionOf_str_-Output": {"properties": {"left": {"$ref": "#/components/schemas/Region-Output", "description": "The left-hand Region to combine"}, "right": {"$ref": "#/components/schemas/Region-Output", "description": "The right-hand Region to combine"}, "type": {"type": "string", "const": "IntersectionOf", "title": "Type", "default": "IntersectionOf"}}, "additionalProperties": false, "type": "object", "required": ["left", "right"], "title": "IntersectionOf", "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])"}, "Line_str_": {"properties": {"axis": {"type": "string", "title": "Axis", "description": "An identifier for what to move"}, "start": {"type": "number", "title": "Start", "description": "Midpoint of the first point of the line"}, "stop": {"type": "number", "title": "Stop", "description": "Midpoint of the last point of the line"}, "num": {"type": "integer", "minimum": 1.0, "title": "Num", "description": "Number of frames to produce"}, "type": {"type": "string", "const": "Line", "title": "Type", "default": "Line"}}, "additionalProperties": false, "type": "object", "required": ["axis", "start", "stop", "num"], "title": "Line", "description": "Linearly spaced frames with start and stop as first and last midpoints.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"x\", 1, 2, 5)"}, "Mask_str_-Input": {"properties": {"spec": {"$ref": "#/components/schemas/Spec-Input", "description": "The Spec containing the source midpoints"}, "region": {"$ref": "#/components/schemas/Region-Input", "description": "The Region that midpoints will be inside"}, "check_path_changes": {"type": "boolean", "title": "Check Path Changes", "description": "If True path through scan will not be modified by squash", "default": true}, "type": {"type": "string", "const": "Mask", "title": "Type", "default": "Mask"}}, "additionalProperties": false, "type": "object", "required": ["spec", "region"], "title": "Mask", "description": "Restrict Spec to only midpoints that fall inside the given Region.\n\nTypically created with the ``&`` operator. It also pushes down the\n``& | ^ -`` operators to its `Region` to avoid the need for brackets on\ncombinations of Regions.\n\nIf a Region spans multiple Dimension objects, they will be squashed together.\n\n.. example_spec::\n\n from scanspec.regions import Circle\n from scanspec.specs import Line\n\n spec = Line(\"y\", 1, 3, 3) * Line(\"x\", 3, 5, 5) & Circle(\"x\", \"y\", 4, 2, 1.2)\n\nSee Also: `why-squash-can-change-path`"}, "Mask_str_-Output": {"properties": {"spec": {"$ref": "#/components/schemas/Spec-Output", "description": "The Spec containing the source midpoints"}, "region": {"$ref": "#/components/schemas/Region-Output", "description": "The Region that midpoints will be inside"}, "check_path_changes": {"type": "boolean", "title": "Check Path Changes", "description": "If True path through scan will not be modified by squash", "default": true}, "type": {"type": "string", "const": "Mask", "title": "Type", "default": "Mask"}}, "additionalProperties": false, "type": "object", "required": ["spec", "region"], "title": "Mask", "description": "Restrict Spec to only midpoints that fall inside the given Region.\n\nTypically created with the ``&`` operator. It also pushes down the\n``& | ^ -`` operators to its `Region` to avoid the need for brackets on\ncombinations of Regions.\n\nIf a Region spans multiple Dimension objects, they will be squashed together.\n\n.. example_spec::\n\n from scanspec.regions import Circle\n from scanspec.specs import Line\n\n spec = Line(\"y\", 1, 3, 3) * Line(\"x\", 3, 5, 5) & Circle(\"x\", \"y\", 4, 2, 1.2)\n\nSee Also: `why-squash-can-change-path`"}, "MidpointsResponse": {"properties": {"total_frames": {"type": "integer", "title": "Total Frames", "description": "Total number of frames in spec"}, "returned_frames": {"type": "integer", "title": "Returned Frames", "description": "Total of number of frames in this response, may be less than total_frames due to downsampling etc."}, "format": {"$ref": "#/components/schemas/PointsFormat", "description": "Format of returned point data"}, "midpoints": {"additionalProperties": {"anyOf": [{"type": "string"}, {"items": {"type": "number"}, "type": "array"}]}, "type": "object", "title": "Midpoints", "description": "The midpoints of scan frames for each axis"}}, "type": "object", "required": ["total_frames", "returned_frames", "format", "midpoints"], "title": "MidpointsResponse", "description": "Midpoints of a generated scan."}, "PointsFormat": {"type": "string", "enum": ["STRING", "FLOAT_LIST", "BASE64_ENCODED"], "title": "PointsFormat", "description": "Formats in which we can return points."}, "PointsRequest": {"properties": {"spec": {"$ref": "#/components/schemas/Spec-Input", "description": "The spec from which to generate points"}, "max_frames": {"anyOf": [{"type": "integer"}, {"type": "null"}], "title": "Max Frames", "description": "The maximum number of points to return, if None will return as many as calculated", "default": 100000}, "format": {"$ref": "#/components/schemas/PointsFormat", "description": "The format in which to output the points data", "default": "FLOAT_LIST"}}, "type": "object", "required": ["spec"], "title": "PointsRequest", "description": "A request for generated scan points."}, "Polygon_str_": {"properties": {"x_axis": {"type": "string", "title": "X Axis", "description": "The name matching the x axis of the spec"}, "y_axis": {"type": "string", "title": "Y Axis", "description": "The name matching the y axis of the spec"}, "x_verts": {"items": {"type": "number"}, "type": "array", "minItems": 3, "title": "X Verts", "description": "The Nx1 x coordinates of the polygons vertices"}, "y_verts": {"items": {"type": "number"}, "type": "array", "minItems": 3, "title": "Y Verts", "description": "The Nx1 y coordinates of the polygons vertices"}, "type": {"type": "string", "const": "Polygon", "title": "Type", "default": "Polygon"}}, "additionalProperties": false, "type": "object", "required": ["x_axis", "y_axis", "x_verts", "y_verts"], "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])"}, "Product_str_-Input": {"properties": {"outer": {"$ref": "#/components/schemas/Spec-Input", "description": "Will be executed once"}, "inner": {"$ref": "#/components/schemas/Spec-Input", "description": "Will be executed len(outer) times"}, "type": {"type": "string", "const": "Product", "title": "Type", "default": "Product"}}, "additionalProperties": false, "type": "object", "required": ["outer", "inner"], "title": "Product", "description": "Outer product of two Specs, nesting inner within outer.\n\nThis means that inner will run in its entirety at each point in outer.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"y\", 1, 2, 3) * Line(\"x\", 3, 4, 12)"}, "Product_str_-Output": {"properties": {"outer": {"$ref": "#/components/schemas/Spec-Output", "description": "Will be executed once"}, "inner": {"$ref": "#/components/schemas/Spec-Output", "description": "Will be executed len(outer) times"}, "type": {"type": "string", "const": "Product", "title": "Type", "default": "Product"}}, "additionalProperties": false, "type": "object", "required": ["outer", "inner"], "title": "Product", "description": "Outer product of two Specs, nesting inner within outer.\n\nThis means that inner will run in its entirety at each point in outer.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"y\", 1, 2, 3) * Line(\"x\", 3, 4, 12)"}, "Range_str_": {"properties": {"axis": {"type": "string", "title": "Axis", "description": "The name matching the axis to mask in spec"}, "min": {"type": "number", "title": "Min", "description": "The minimum inclusive value in the region"}, "max": {"type": "number", "title": "Max", "description": "The minimum inclusive value in the region"}, "type": {"type": "string", "const": "Range", "title": "Type", "default": "Range"}}, "additionalProperties": false, "type": "object", "required": ["axis", "min", "max"], "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])"}, "Rectangle_str_": {"properties": {"x_axis": {"type": "string", "title": "X Axis", "description": "The name matching the x axis of the spec"}, "y_axis": {"type": "string", "title": "Y Axis", "description": "The name matching the y axis of the spec"}, "x_min": {"type": "number", "title": "X Min", "description": "Minimum inclusive x value in the region"}, "y_min": {"type": "number", "title": "Y Min", "description": "Minimum inclusive y value in the region"}, "x_max": {"type": "number", "title": "X Max", "description": "Maximum inclusive x value in the region"}, "y_max": {"type": "number", "title": "Y Max", "description": "Maximum inclusive y value in the region"}, "angle": {"type": "number", "title": "Angle", "description": "Clockwise rotation angle of the rectangle", "default": 0.0}, "type": {"type": "string", "const": "Rectangle", "title": "Type", "default": "Rectangle"}}, "additionalProperties": false, "type": "object", "required": ["x_axis", "y_axis", "x_min", "y_min", "x_max", "y_max"], "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)"}, "Region-Input": {"oneOf": [{"$ref": "#/components/schemas/CombinationOf_str_-Input"}, {"$ref": "#/components/schemas/UnionOf_str_-Input"}, {"$ref": "#/components/schemas/IntersectionOf_str_-Input"}, {"$ref": "#/components/schemas/DifferenceOf_str_-Input"}, {"$ref": "#/components/schemas/SymmetricDifferenceOf_str_-Input"}, {"$ref": "#/components/schemas/Range_str_"}, {"$ref": "#/components/schemas/Rectangle_str_"}, {"$ref": "#/components/schemas/Polygon_str_"}, {"$ref": "#/components/schemas/Circle_str_"}, {"$ref": "#/components/schemas/Ellipse_str_"}], "discriminator": {"propertyName": "type", "mapping": {"Circle": "#/components/schemas/Circle_str_", "CombinationOf": "#/components/schemas/CombinationOf_str_-Input", "DifferenceOf": "#/components/schemas/DifferenceOf_str_-Input", "Ellipse": "#/components/schemas/Ellipse_str_", "IntersectionOf": "#/components/schemas/IntersectionOf_str_-Input", "Polygon": "#/components/schemas/Polygon_str_", "Range": "#/components/schemas/Range_str_", "Rectangle": "#/components/schemas/Rectangle_str_", "SymmetricDifferenceOf": "#/components/schemas/SymmetricDifferenceOf_str_-Input", "UnionOf": "#/components/schemas/UnionOf_str_-Input"}}}, "Region-Output": {"oneOf": [{"$ref": "#/components/schemas/CombinationOf_str_-Output"}, {"$ref": "#/components/schemas/UnionOf_str_-Output"}, {"$ref": "#/components/schemas/IntersectionOf_str_-Output"}, {"$ref": "#/components/schemas/DifferenceOf_str_-Output"}, {"$ref": "#/components/schemas/SymmetricDifferenceOf_str_-Output"}, {"$ref": "#/components/schemas/Range_str_"}, {"$ref": "#/components/schemas/Rectangle_str_"}, {"$ref": "#/components/schemas/Polygon_str_"}, {"$ref": "#/components/schemas/Circle_str_"}, {"$ref": "#/components/schemas/Ellipse_str_"}], "discriminator": {"propertyName": "type", "mapping": {"Circle": "#/components/schemas/Circle_str_", "CombinationOf": "#/components/schemas/CombinationOf_str_-Output", "DifferenceOf": "#/components/schemas/DifferenceOf_str_-Output", "Ellipse": "#/components/schemas/Ellipse_str_", "IntersectionOf": "#/components/schemas/IntersectionOf_str_-Output", "Polygon": "#/components/schemas/Polygon_str_", "Range": "#/components/schemas/Range_str_", "Rectangle": "#/components/schemas/Rectangle_str_", "SymmetricDifferenceOf": "#/components/schemas/SymmetricDifferenceOf_str_-Output", "UnionOf": "#/components/schemas/UnionOf_str_-Output"}}}, "Repeat_str_": {"properties": {"num": {"type": "integer", "minimum": 1.0, "title": "Num", "description": "Number of frames to produce"}, "gap": {"type": "boolean", "title": "Gap", "description": "If False and the slowest of the stack of Dimension is snaked then the end and start of consecutive iterations of Spec will have no gap", "default": true}, "type": {"type": "string", "const": "Repeat", "title": "Type", "default": "Repeat"}}, "additionalProperties": false, "type": "object", "required": ["num"], "title": "Repeat", "description": "Repeat an empty frame num times.\n\nCan be used on the outside of a scan to repeat the same scan many times.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = 2 * ~Line.bounded(\"x\", 3, 4, 1)\n\nIf you want snaked axes to have no gap between iterations you can do:\n\n.. example_spec::\n\n from scanspec.specs import Line, Repeat\n\n spec = Repeat(2, gap=False) * ~Line.bounded(\"x\", 3, 4, 1)\n\n.. note:: There is no turnaround arrow at x=4"}, "SmallestStepResponse": {"properties": {"absolute": {"type": "number", "title": "Absolute", "description": "Absolute smallest distance between two points on a single axis"}, "per_axis": {"additionalProperties": {"type": "number"}, "type": "object", "title": "Per Axis", "description": "Smallest distance between two points on each axis"}}, "type": "object", "required": ["absolute", "per_axis"], "title": "SmallestStepResponse", "description": "Information about the smallest steps between points in a spec."}, "Snake_str_-Input": {"properties": {"spec": {"$ref": "#/components/schemas/Spec-Input", "description": "The Spec to run in reverse every other iteration"}, "type": {"type": "string", "const": "Snake", "title": "Type", "default": "Snake"}}, "additionalProperties": false, "type": "object", "required": ["spec"], "title": "Snake", "description": "Run the Spec in reverse on every other iteration when nested.\n\nTypically created with the ``~`` operator.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"y\", 1, 3, 3) * ~Line(\"x\", 3, 5, 5)"}, "Snake_str_-Output": {"properties": {"spec": {"$ref": "#/components/schemas/Spec-Output", "description": "The Spec to run in reverse every other iteration"}, "type": {"type": "string", "const": "Snake", "title": "Type", "default": "Snake"}}, "additionalProperties": false, "type": "object", "required": ["spec"], "title": "Snake", "description": "Run the Spec in reverse on every other iteration when nested.\n\nTypically created with the ``~`` operator.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"y\", 1, 3, 3) * ~Line(\"x\", 3, 5, 5)"}, "Spec-Input": {"oneOf": [{"$ref": "#/components/schemas/Product_str_-Input"}, {"$ref": "#/components/schemas/Repeat_str_"}, {"$ref": "#/components/schemas/Zip_str_-Input"}, {"$ref": "#/components/schemas/Mask_str_-Input"}, {"$ref": "#/components/schemas/Snake_str_-Input"}, {"$ref": "#/components/schemas/Concat_str_-Input"}, {"$ref": "#/components/schemas/Squash_str_-Input"}, {"$ref": "#/components/schemas/Line_str_"}, {"$ref": "#/components/schemas/Duration_str_"}, {"$ref": "#/components/schemas/Static_str_"}, {"$ref": "#/components/schemas/Spiral_str_"}], "discriminator": {"propertyName": "type", "mapping": {"Concat": "#/components/schemas/Concat_str_-Input", "Duration": "#/components/schemas/Duration_str_", "Line": "#/components/schemas/Line_str_", "Mask": "#/components/schemas/Mask_str_-Input", "Product": "#/components/schemas/Product_str_-Input", "Repeat": "#/components/schemas/Repeat_str_", "Snake": "#/components/schemas/Snake_str_-Input", "Spiral": "#/components/schemas/Spiral_str_", "Squash": "#/components/schemas/Squash_str_-Input", "Static": "#/components/schemas/Static_str_", "Zip": "#/components/schemas/Zip_str_-Input"}}}, "Spec-Output": {"oneOf": [{"$ref": "#/components/schemas/Product_str_-Output"}, {"$ref": "#/components/schemas/Repeat_str_"}, {"$ref": "#/components/schemas/Zip_str_-Output"}, {"$ref": "#/components/schemas/Mask_str_-Output"}, {"$ref": "#/components/schemas/Snake_str_-Output"}, {"$ref": "#/components/schemas/Concat_str_-Output"}, {"$ref": "#/components/schemas/Squash_str_-Output"}, {"$ref": "#/components/schemas/Line_str_"}, {"$ref": "#/components/schemas/Duration_str_"}, {"$ref": "#/components/schemas/Static_str_"}, {"$ref": "#/components/schemas/Spiral_str_"}], "discriminator": {"propertyName": "type", "mapping": {"Concat": "#/components/schemas/Concat_str_-Output", "Duration": "#/components/schemas/Duration_str_", "Line": "#/components/schemas/Line_str_", "Mask": "#/components/schemas/Mask_str_-Output", "Product": "#/components/schemas/Product_str_-Output", "Repeat": "#/components/schemas/Repeat_str_", "Snake": "#/components/schemas/Snake_str_-Output", "Spiral": "#/components/schemas/Spiral_str_", "Squash": "#/components/schemas/Squash_str_-Output", "Static": "#/components/schemas/Static_str_", "Zip": "#/components/schemas/Zip_str_-Output"}}}, "Spiral_str_": {"properties": {"x_axis": {"type": "string", "title": "X Axis", "description": "An identifier for what to move for x"}, "y_axis": {"type": "string", "title": "Y Axis", "description": "An identifier for what to move for y"}, "x_start": {"type": "number", "title": "X Start", "description": "x centre of the spiral"}, "y_start": {"type": "number", "title": "Y Start", "description": "y centre of the spiral"}, "x_range": {"type": "number", "title": "X Range", "description": "x width of the spiral"}, "y_range": {"type": "number", "title": "Y Range", "description": "y width of the spiral"}, "num": {"type": "integer", "minimum": 1.0, "title": "Num", "description": "Number of frames to produce"}, "rotate": {"type": "number", "title": "Rotate", "description": "How much to rotate the angle of the spiral", "default": 0.0}, "type": {"type": "string", "const": "Spiral", "title": "Type", "default": "Spiral"}}, "additionalProperties": false, "type": "object", "required": ["x_axis", "y_axis", "x_start", "y_start", "x_range", "y_range", "num"], "title": "Spiral", "description": "Archimedean spiral of \"x_axis\" and \"y_axis\".\n\nStarts at centre point (\"x_start\", \"y_start\") with angle \"rotate\". Produces\n\"num\" points in a spiral spanning width of \"x_range\" and height of \"y_range\"\n\n.. example_spec::\n\n from scanspec.specs import Spiral\n\n spec = Spiral(\"x\", \"y\", 1, 5, 10, 50, 30)"}, "Squash_str_-Input": {"properties": {"spec": {"$ref": "#/components/schemas/Spec-Input", "description": "The Spec to squash the dimensions of"}, "check_path_changes": {"type": "boolean", "title": "Check Path Changes", "description": "If True path through scan will not be modified by squash", "default": true}, "type": {"type": "string", "const": "Squash", "title": "Type", "default": "Squash"}}, "additionalProperties": false, "type": "object", "required": ["spec"], "title": "Squash", "description": "Squash a stack of Dimension together into a single expanded Dimension object.\n\nSee Also:\n `why-squash-can-change-path`\n\n.. example_spec::\n\n from scanspec.specs import Line, Squash\n\n spec = Squash(Line(\"y\", 1, 2, 3) * Line(\"x\", 0, 1, 4))"}, "Squash_str_-Output": {"properties": {"spec": {"$ref": "#/components/schemas/Spec-Output", "description": "The Spec to squash the dimensions of"}, "check_path_changes": {"type": "boolean", "title": "Check Path Changes", "description": "If True path through scan will not be modified by squash", "default": true}, "type": {"type": "string", "const": "Squash", "title": "Type", "default": "Squash"}}, "additionalProperties": false, "type": "object", "required": ["spec"], "title": "Squash", "description": "Squash a stack of Dimension together into a single expanded Dimension object.\n\nSee Also:\n `why-squash-can-change-path`\n\n.. example_spec::\n\n from scanspec.specs import Line, Squash\n\n spec = Squash(Line(\"y\", 1, 2, 3) * Line(\"x\", 0, 1, 4))"}, "Static_str_": {"properties": {"axis": {"type": "string", "title": "Axis", "description": "An identifier for what to move"}, "value": {"type": "number", "title": "Value", "description": "The value at each point"}, "num": {"type": "integer", "minimum": 1.0, "title": "Num", "description": "Number of frames to produce", "default": 1}, "type": {"type": "string", "const": "Static", "title": "Type", "default": "Static"}}, "additionalProperties": false, "type": "object", "required": ["axis", "value"], "title": "Static", "description": "A static frame, repeated num times, with axis at value.\n\nCan be used to set axis=value at every point in a scan.\n\n.. example_spec::\n\n from scanspec.specs import Line, Static\n\n spec = Line(\"y\", 1, 2, 3).zip(Static(\"x\", 3))"}, "SymmetricDifferenceOf_str_-Input": {"properties": {"left": {"$ref": "#/components/schemas/Region-Input", "description": "The left-hand Region to combine"}, "right": {"$ref": "#/components/schemas/Region-Input", "description": "The right-hand Region to combine"}, "type": {"type": "string", "const": "SymmetricDifferenceOf", "title": "Type", "default": "SymmetricDifferenceOf"}}, "additionalProperties": false, "type": "object", "required": ["left", "right"], "title": "SymmetricDifferenceOf", "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])"}, "SymmetricDifferenceOf_str_-Output": {"properties": {"left": {"$ref": "#/components/schemas/Region-Output", "description": "The left-hand Region to combine"}, "right": {"$ref": "#/components/schemas/Region-Output", "description": "The right-hand Region to combine"}, "type": {"type": "string", "const": "SymmetricDifferenceOf", "title": "Type", "default": "SymmetricDifferenceOf"}}, "additionalProperties": false, "type": "object", "required": ["left", "right"], "title": "SymmetricDifferenceOf", "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])"}, "UnionOf_str_-Input": {"properties": {"left": {"$ref": "#/components/schemas/Region-Input", "description": "The left-hand Region to combine"}, "right": {"$ref": "#/components/schemas/Region-Input", "description": "The right-hand Region to combine"}, "type": {"type": "string", "const": "UnionOf", "title": "Type", "default": "UnionOf"}}, "additionalProperties": false, "type": "object", "required": ["left", "right"], "title": "UnionOf", "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])"}, "UnionOf_str_-Output": {"properties": {"left": {"$ref": "#/components/schemas/Region-Output", "description": "The left-hand Region to combine"}, "right": {"$ref": "#/components/schemas/Region-Output", "description": "The right-hand Region to combine"}, "type": {"type": "string", "const": "UnionOf", "title": "Type", "default": "UnionOf"}}, "additionalProperties": false, "type": "object", "required": ["left", "right"], "title": "UnionOf", "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])"}, "ValidResponse": {"properties": {"input_spec": {"$ref": "#/components/schemas/Spec-Output", "description": "The input scanspec"}, "valid_spec": {"$ref": "#/components/schemas/Spec-Output", "description": "The validated version of the spec"}}, "type": "object", "required": ["input_spec", "valid_spec"], "title": "ValidResponse", "description": "Response model for spec validation."}, "ValidationError": {"properties": {"loc": {"items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, "type": "array", "title": "Location"}, "msg": {"type": "string", "title": "Message"}, "type": {"type": "string", "title": "Error Type"}}, "type": "object", "required": ["loc", "msg", "type"], "title": "ValidationError"}, "Zip_str_-Input": {"properties": {"left": {"$ref": "#/components/schemas/Spec-Input", "description": "The left-hand Spec to Zip, will appear earlier in axes"}, "right": {"$ref": "#/components/schemas/Spec-Input", "description": "The right-hand Spec to Zip, will appear later in axes"}, "type": {"type": "string", "const": "Zip", "title": "Type", "default": "Zip"}}, "additionalProperties": false, "type": "object", "required": ["left", "right"], "title": "Zip", "description": "Run two Specs in parallel, merging their midpoints together.\n\nTypically formed using `Spec.zip`.\n\nStacks of Dimension are merged by:\n\n- If right creates a stack of a single Dimension object of size 1, expand it to\n the size of the fastest Dimension object created by left\n- Merge individual Dimension objects together from fastest to slowest\n\nThis means that Zipping a Spec producing stack [l2, l1] with a Spec\nproducing stack [r1] will assert len(l1)==len(r1), and produce\nstack [l2, l1.zip(r1)].\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"z\", 1, 2, 3) * Line(\"y\", 3, 4, 5).zip(Line(\"x\", 4, 5, 5))"}, "Zip_str_-Output": {"properties": {"left": {"$ref": "#/components/schemas/Spec-Output", "description": "The left-hand Spec to Zip, will appear earlier in axes"}, "right": {"$ref": "#/components/schemas/Spec-Output", "description": "The right-hand Spec to Zip, will appear later in axes"}, "type": {"type": "string", "const": "Zip", "title": "Type", "default": "Zip"}}, "additionalProperties": false, "type": "object", "required": ["left", "right"], "title": "Zip", "description": "Run two Specs in parallel, merging their midpoints together.\n\nTypically formed using `Spec.zip`.\n\nStacks of Dimension are merged by:\n\n- If right creates a stack of a single Dimension object of size 1, expand it to\n the size of the fastest Dimension object created by left\n- Merge individual Dimension objects together from fastest to slowest\n\nThis means that Zipping a Spec producing stack [l2, l1] with a Spec\nproducing stack [r1] will assert len(l1)==len(r1), and produce\nstack [l2, l1.zip(r1)].\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"z\", 1, 2, 3) * Line(\"y\", 3, 4, 5).zip(Line(\"x\", 4, 5, 5))"}}}} +{ + "openapi": "3.1.0", + "info": { + "title": "FastAPI", + "version": "0.1.1" + }, + "paths": { + "/valid": { + "post": { + "summary": "Valid", + "description": "Validate wether a ScanSpec[str] can produce a viable scan.\n\nArgs:\n spec: The scanspec to validate\n\nReturns:\n ValidResponse: A canonical version of the spec if it is valid.\n An error otherwise.", + "operationId": "valid_valid_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Spec-Input", + "examples": [ + { + "outer": { + "axis": "y", + "start": 0.0, + "stop": 10.0, + "num": 3, + "type": "Line" + }, + "inner": { + "axis": "x", + "start": 0.0, + "stop": 10.0, + "num": 4, + "type": "Line" + }, + "type": "Product" + } + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/midpoints": { + "post": { + "summary": "Midpoints", + "description": "Generate midpoints from a scanspec.\n\nA scanspec can produce bounded points (i.e. a point is valid if an\naxis is between a minimum and and a maximum, see /bounds). The midpoints\nare the middle of each set of bounds.\n\nArgs:\n request: Scanspec and formatting info.\n\nReturns:\n MidpointsResponse: Midpoints of the scan", + "operationId": "midpoints_midpoints_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PointsRequest", + "examples": [ + { + "spec": { + "outer": { + "axis": "y", + "start": 0.0, + "stop": 10.0, + "num": 3, + "type": "Line" + }, + "inner": { + "axis": "x", + "start": 0.0, + "stop": 10.0, + "num": 4, + "type": "Line" + }, + "type": "Product" + }, + "max_frames": 1024, + "format": "FLOAT_LIST" + } + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MidpointsResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/bounds": { + "post": { + "summary": "Bounds", + "description": "Generate bounds from a scanspec.\n\nA scanspec can produce points with lower and upper bounds.\n\nArgs:\n request: Scanspec and formatting info.\n\nReturns:\n BoundsResponse: Bounds of the scan", + "operationId": "bounds_bounds_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PointsRequest", + "examples": [ + { + "spec": { + "outer": { + "axis": "y", + "start": 0.0, + "stop": 10.0, + "num": 3, + "type": "Line" + }, + "inner": { + "axis": "x", + "start": 0.0, + "stop": 10.0, + "num": 4, + "type": "Line" + }, + "type": "Product" + }, + "max_frames": 1024, + "format": "FLOAT_LIST" + } + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BoundsResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/gap": { + "post": { + "summary": "Gap", + "description": "Generate gaps from a scanspec.\n\nA scanspec may indicate if there is a gap between two frames.\nThe array returned corresponds to whether or not there is a gap\nafter each frame.\n\nArgs:\n spec: Scanspec and formatting info.\n\nReturns:\n GapResponse: Bounds of the scan", + "operationId": "gap_gap_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Spec-Input", + "examples": [ + { + "outer": { + "axis": "y", + "start": 0.0, + "stop": 10.0, + "num": 3, + "type": "Line" + }, + "inner": { + "axis": "x", + "start": 0.0, + "stop": 10.0, + "num": 4, + "type": "Line" + }, + "type": "Product" + } + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GapResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/smalleststep": { + "post": { + "summary": "Smallest Step", + "description": "Calculate the smallest step in a scan, both absolutely and per-axis.\n\nIgnore any steps of size 0.\n\nArgs:\n spec: The spec of the scan\n\nReturns:\n SmallestStepResponse: A description of the smallest steps in the spec", + "operationId": "smallest_step_smalleststep_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Spec-Input", + "examples": [ + { + "outer": { + "axis": "y", + "start": 0.0, + "stop": 10.0, + "num": 3, + "type": "Line" + }, + "inner": { + "axis": "x", + "start": 0.0, + "stop": 10.0, + "num": 4, + "type": "Line" + }, + "type": "Product" + } + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SmallestStepResponse" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "BoundsResponse": { + "properties": { + "total_frames": { + "type": "integer", + "title": "Total Frames", + "description": "Total number of frames in spec" + }, + "returned_frames": { + "type": "integer", + "title": "Returned Frames", + "description": "Total of number of frames in this response, may be less than total_frames due to downsampling etc." + }, + "format": { + "$ref": "#/components/schemas/PointsFormat", + "description": "Format of returned point data" + }, + "lower": { + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "number" + }, + "type": "array" + } + ] + }, + "type": "object", + "title": "Lower", + "description": "Lower bounds of scan frames if different from midpoints" + }, + "upper": { + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "number" + }, + "type": "array" + } + ] + }, + "type": "object", + "title": "Upper", + "description": "Upper bounds of scan frames if different from midpoints" + } + }, + "type": "object", + "required": [ + "total_frames", + "returned_frames", + "format", + "lower", + "upper" + ], + "title": "BoundsResponse", + "description": "Bounds of a generated scan." + }, + "Circle_str_": { + "properties": { + "x_axis": { + "type": "string", + "title": "X Axis", + "description": "The name matching the x axis of the spec" + }, + "y_axis": { + "type": "string", + "title": "Y Axis", + "description": "The name matching the y axis of the spec" + }, + "x_middle": { + "type": "number", + "title": "X Middle", + "description": "The central x point of the circle" + }, + "y_middle": { + "type": "number", + "title": "Y Middle", + "description": "The central y point of the circle" + }, + "radius": { + "type": "number", + "exclusiveMinimum": 0.0, + "title": "Radius", + "description": "Radius of the circle" + }, + "type": { + "type": "string", + "const": "Circle", + "title": "Type", + "default": "Circle" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "x_axis", + "y_axis", + "x_middle", + "y_middle", + "radius" + ], + "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)" + }, + "CombinationOf_str_-Input": { + "properties": { + "left": { + "$ref": "#/components/schemas/Region-Input", + "description": "The left-hand Region to combine" + }, + "right": { + "$ref": "#/components/schemas/Region-Input", + "description": "The right-hand Region to combine" + }, + "type": { + "type": "string", + "const": "CombinationOf", + "title": "Type", + "default": "CombinationOf" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "left", + "right" + ], + "title": "CombinationOf", + "description": "Abstract baseclass for a combination of two regions, left and right." + }, + "CombinationOf_str_-Output": { + "properties": { + "left": { + "$ref": "#/components/schemas/Region-Output", + "description": "The left-hand Region to combine" + }, + "right": { + "$ref": "#/components/schemas/Region-Output", + "description": "The right-hand Region to combine" + }, + "type": { + "type": "string", + "const": "CombinationOf", + "title": "Type", + "default": "CombinationOf" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "left", + "right" + ], + "title": "CombinationOf", + "description": "Abstract baseclass for a combination of two regions, left and right." + }, + "Concat_str_-Input": { + "properties": { + "left": { + "$ref": "#/components/schemas/Spec-Input", + "description": "The left-hand Spec to Concat, midpoints will appear earlier" + }, + "right": { + "$ref": "#/components/schemas/Spec-Input", + "description": "The right-hand Spec to Concat, midpoints will appear later" + }, + "gap": { + "type": "boolean", + "title": "Gap", + "description": "If True, force a gap in the output at the join", + "default": false + }, + "check_path_changes": { + "type": "boolean", + "title": "Check Path Changes", + "description": "If True path through scan will not be modified by squash", + "default": true + }, + "type": { + "type": "string", + "const": "Concat", + "title": "Type", + "default": "Concat" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "left", + "right" + ], + "title": "Concat", + "description": "Concatenate two Specs together, running one after the other.\n\nEach Dimension of left and right must contain the same axes. Typically\nformed using `Spec.concat`.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"x\", 1, 3, 3).concat(Line(\"x\", 4, 5, 5))" + }, + "Concat_str_-Output": { + "properties": { + "left": { + "$ref": "#/components/schemas/Spec-Output", + "description": "The left-hand Spec to Concat, midpoints will appear earlier" + }, + "right": { + "$ref": "#/components/schemas/Spec-Output", + "description": "The right-hand Spec to Concat, midpoints will appear later" + }, + "gap": { + "type": "boolean", + "title": "Gap", + "description": "If True, force a gap in the output at the join", + "default": false + }, + "check_path_changes": { + "type": "boolean", + "title": "Check Path Changes", + "description": "If True path through scan will not be modified by squash", + "default": true + }, + "type": { + "type": "string", + "const": "Concat", + "title": "Type", + "default": "Concat" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "left", + "right" + ], + "title": "Concat", + "description": "Concatenate two Specs together, running one after the other.\n\nEach Dimension of left and right must contain the same axes. Typically\nformed using `Spec.concat`.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"x\", 1, 3, 3).concat(Line(\"x\", 4, 5, 5))" + }, + "ConstantDuration_str_-Input": { + "properties": { + "spec": { + "$ref": "#/components/schemas/Spec-Input", + "description": "Spec contaning the path to be followed" + }, + "constant_duration": { + "type": "number", + "title": "Constant Duration", + "description": "The value at each point" + }, + "fly": { + "type": "boolean", + "title": "Fly", + "description": "Define if the Spec is a fly or step scan", + "default": false + }, + "type": { + "type": "string", + "const": "ConstantDuration", + "title": "Type", + "default": "ConstantDuration" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "spec", + "constant_duration" + ], + "title": "ConstantDuration", + "description": "A special spec used to hold information about the duration of each frame." + }, + "ConstantDuration_str_-Output": { + "properties": { + "spec": { + "$ref": "#/components/schemas/Spec-Output", + "description": "Spec contaning the path to be followed" + }, + "constant_duration": { + "type": "number", + "title": "Constant Duration", + "description": "The value at each point" + }, + "fly": { + "type": "boolean", + "title": "Fly", + "description": "Define if the Spec is a fly or step scan", + "default": false + }, + "type": { + "type": "string", + "const": "ConstantDuration", + "title": "Type", + "default": "ConstantDuration" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "spec", + "constant_duration" + ], + "title": "ConstantDuration", + "description": "A special spec used to hold information about the duration of each frame." + }, + "DifferenceOf_str_-Input": { + "properties": { + "left": { + "$ref": "#/components/schemas/Region-Input", + "description": "The left-hand Region to combine" + }, + "right": { + "$ref": "#/components/schemas/Region-Input", + "description": "The right-hand Region to combine" + }, + "type": { + "type": "string", + "const": "DifferenceOf", + "title": "Type", + "default": "DifferenceOf" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "left", + "right" + ], + "title": "DifferenceOf", + "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])" + }, + "DifferenceOf_str_-Output": { + "properties": { + "left": { + "$ref": "#/components/schemas/Region-Output", + "description": "The left-hand Region to combine" + }, + "right": { + "$ref": "#/components/schemas/Region-Output", + "description": "The right-hand Region to combine" + }, + "type": { + "type": "string", + "const": "DifferenceOf", + "title": "Type", + "default": "DifferenceOf" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "left", + "right" + ], + "title": "DifferenceOf", + "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])" + }, + "Ellipse_str_": { + "properties": { + "x_axis": { + "type": "string", + "title": "X Axis", + "description": "The name matching the x axis of the spec" + }, + "y_axis": { + "type": "string", + "title": "Y Axis", + "description": "The name matching the y axis of the spec" + }, + "x_middle": { + "type": "number", + "title": "X Middle", + "description": "The central x point of the ellipse" + }, + "y_middle": { + "type": "number", + "title": "Y Middle", + "description": "The central y point of the ellipse" + }, + "x_radius": { + "type": "number", + "exclusiveMinimum": 0.0, + "title": "X Radius", + "description": "The radius along the x axis of the ellipse" + }, + "y_radius": { + "type": "number", + "exclusiveMinimum": 0.0, + "title": "Y Radius", + "description": "The radius along the y axis of the ellipse" + }, + "angle": { + "type": "number", + "title": "Angle", + "description": "The angle of the ellipse (degrees)", + "default": 0.0 + }, + "type": { + "type": "string", + "const": "Ellipse", + "title": "Type", + "default": "Ellipse" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "x_axis", + "y_axis", + "x_middle", + "y_middle", + "x_radius", + "y_radius" + ], + "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)" + }, + "GapResponse": { + "properties": { + "gap": { + "items": { + "type": "boolean" + }, + "type": "array", + "title": "Gap", + "description": "Boolean array indicating if there is a gap between each frame" + } + }, + "type": "object", + "required": [ + "gap" + ], + "title": "GapResponse", + "description": "Presence of gaps in a generated scan." + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail" + } + }, + "type": "object", + "title": "HTTPValidationError" + }, + "IntersectionOf_str_-Input": { + "properties": { + "left": { + "$ref": "#/components/schemas/Region-Input", + "description": "The left-hand Region to combine" + }, + "right": { + "$ref": "#/components/schemas/Region-Input", + "description": "The right-hand Region to combine" + }, + "type": { + "type": "string", + "const": "IntersectionOf", + "title": "Type", + "default": "IntersectionOf" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "left", + "right" + ], + "title": "IntersectionOf", + "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])" + }, + "IntersectionOf_str_-Output": { + "properties": { + "left": { + "$ref": "#/components/schemas/Region-Output", + "description": "The left-hand Region to combine" + }, + "right": { + "$ref": "#/components/schemas/Region-Output", + "description": "The right-hand Region to combine" + }, + "type": { + "type": "string", + "const": "IntersectionOf", + "title": "Type", + "default": "IntersectionOf" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "left", + "right" + ], + "title": "IntersectionOf", + "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])" + }, + "Line_str_": { + "properties": { + "axis": { + "type": "string", + "title": "Axis", + "description": "An identifier for what to move" + }, + "start": { + "type": "number", + "title": "Start", + "description": "Midpoint of the first point of the line" + }, + "stop": { + "type": "number", + "title": "Stop", + "description": "Midpoint of the last point of the line" + }, + "num": { + "type": "integer", + "minimum": 1.0, + "title": "Num", + "description": "Number of frames to produce" + }, + "type": { + "type": "string", + "const": "Line", + "title": "Type", + "default": "Line" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "axis", + "start", + "stop", + "num" + ], + "title": "Line", + "description": "Linearly spaced frames with start and stop as first and last midpoints.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"x\", 1, 2, 5)" + }, + "Mask_str_-Input": { + "properties": { + "spec": { + "$ref": "#/components/schemas/Spec-Input", + "description": "The Spec containing the source midpoints" + }, + "region": { + "$ref": "#/components/schemas/Region-Input", + "description": "The Region that midpoints will be inside" + }, + "check_path_changes": { + "type": "boolean", + "title": "Check Path Changes", + "description": "If True path through scan will not be modified by squash", + "default": true + }, + "type": { + "type": "string", + "const": "Mask", + "title": "Type", + "default": "Mask" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "spec", + "region" + ], + "title": "Mask", + "description": "Restrict Spec to only midpoints that fall inside the given Region.\n\nTypically created with the ``&`` operator. It also pushes down the\n``& | ^ -`` operators to its `Region` to avoid the need for brackets on\ncombinations of Regions.\n\nIf a Region spans multiple Dimension objects, they will be squashed together.\n\n.. example_spec::\n\n from scanspec.regions import Circle\n from scanspec.specs import Line\n\n spec = Line(\"y\", 1, 3, 3) * Line(\"x\", 3, 5, 5) & Circle(\"x\", \"y\", 4, 2, 1.2)\n\nSee Also: `why-squash-can-change-path`" + }, + "Mask_str_-Output": { + "properties": { + "spec": { + "$ref": "#/components/schemas/Spec-Output", + "description": "The Spec containing the source midpoints" + }, + "region": { + "$ref": "#/components/schemas/Region-Output", + "description": "The Region that midpoints will be inside" + }, + "check_path_changes": { + "type": "boolean", + "title": "Check Path Changes", + "description": "If True path through scan will not be modified by squash", + "default": true + }, + "type": { + "type": "string", + "const": "Mask", + "title": "Type", + "default": "Mask" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "spec", + "region" + ], + "title": "Mask", + "description": "Restrict Spec to only midpoints that fall inside the given Region.\n\nTypically created with the ``&`` operator. It also pushes down the\n``& | ^ -`` operators to its `Region` to avoid the need for brackets on\ncombinations of Regions.\n\nIf a Region spans multiple Dimension objects, they will be squashed together.\n\n.. example_spec::\n\n from scanspec.regions import Circle\n from scanspec.specs import Line\n\n spec = Line(\"y\", 1, 3, 3) * Line(\"x\", 3, 5, 5) & Circle(\"x\", \"y\", 4, 2, 1.2)\n\nSee Also: `why-squash-can-change-path`" + }, + "MidpointsResponse": { + "properties": { + "total_frames": { + "type": "integer", + "title": "Total Frames", + "description": "Total number of frames in spec" + }, + "returned_frames": { + "type": "integer", + "title": "Returned Frames", + "description": "Total of number of frames in this response, may be less than total_frames due to downsampling etc." + }, + "format": { + "$ref": "#/components/schemas/PointsFormat", + "description": "Format of returned point data" + }, + "midpoints": { + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "items": { + "type": "number" + }, + "type": "array" + } + ] + }, + "type": "object", + "title": "Midpoints", + "description": "The midpoints of scan frames for each axis" + } + }, + "type": "object", + "required": [ + "total_frames", + "returned_frames", + "format", + "midpoints" + ], + "title": "MidpointsResponse", + "description": "Midpoints of a generated scan." + }, + "PointsFormat": { + "type": "string", + "enum": [ + "STRING", + "FLOAT_LIST", + "BASE64_ENCODED" + ], + "title": "PointsFormat", + "description": "Formats in which we can return points." + }, + "PointsRequest": { + "properties": { + "spec": { + "$ref": "#/components/schemas/Spec-Input", + "description": "The spec from which to generate points" + }, + "max_frames": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Max Frames", + "description": "The maximum number of points to return, if None will return as many as calculated", + "default": 100000 + }, + "format": { + "$ref": "#/components/schemas/PointsFormat", + "description": "The format in which to output the points data", + "default": "FLOAT_LIST" + } + }, + "type": "object", + "required": [ + "spec" + ], + "title": "PointsRequest", + "description": "A request for generated scan points." + }, + "Polygon_str_": { + "properties": { + "x_axis": { + "type": "string", + "title": "X Axis", + "description": "The name matching the x axis of the spec" + }, + "y_axis": { + "type": "string", + "title": "Y Axis", + "description": "The name matching the y axis of the spec" + }, + "x_verts": { + "items": { + "type": "number" + }, + "type": "array", + "minItems": 3, + "title": "X Verts", + "description": "The Nx1 x coordinates of the polygons vertices" + }, + "y_verts": { + "items": { + "type": "number" + }, + "type": "array", + "minItems": 3, + "title": "Y Verts", + "description": "The Nx1 y coordinates of the polygons vertices" + }, + "type": { + "type": "string", + "const": "Polygon", + "title": "Type", + "default": "Polygon" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "x_axis", + "y_axis", + "x_verts", + "y_verts" + ], + "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])" + }, + "Product_str_-Input": { + "properties": { + "outer": { + "$ref": "#/components/schemas/Spec-Input", + "description": "Will be executed once" + }, + "inner": { + "$ref": "#/components/schemas/Spec-Input", + "description": "Will be executed len(outer) times" + }, + "type": { + "type": "string", + "const": "Product", + "title": "Type", + "default": "Product" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "outer", + "inner" + ], + "title": "Product", + "description": "Outer product of two Specs, nesting inner within outer.\n\nThis means that inner will run in its entirety at each point in outer.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"y\", 1, 2, 3) * Line(\"x\", 3, 4, 12)" + }, + "Product_str_-Output": { + "properties": { + "outer": { + "$ref": "#/components/schemas/Spec-Output", + "description": "Will be executed once" + }, + "inner": { + "$ref": "#/components/schemas/Spec-Output", + "description": "Will be executed len(outer) times" + }, + "type": { + "type": "string", + "const": "Product", + "title": "Type", + "default": "Product" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "outer", + "inner" + ], + "title": "Product", + "description": "Outer product of two Specs, nesting inner within outer.\n\nThis means that inner will run in its entirety at each point in outer.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"y\", 1, 2, 3) * Line(\"x\", 3, 4, 12)" + }, + "Range_str_": { + "properties": { + "axis": { + "type": "string", + "title": "Axis", + "description": "The name matching the axis to mask in spec" + }, + "min": { + "type": "number", + "title": "Min", + "description": "The minimum inclusive value in the region" + }, + "max": { + "type": "number", + "title": "Max", + "description": "The minimum inclusive value in the region" + }, + "type": { + "type": "string", + "const": "Range", + "title": "Type", + "default": "Range" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "axis", + "min", + "max" + ], + "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])" + }, + "Rectangle_str_": { + "properties": { + "x_axis": { + "type": "string", + "title": "X Axis", + "description": "The name matching the x axis of the spec" + }, + "y_axis": { + "type": "string", + "title": "Y Axis", + "description": "The name matching the y axis of the spec" + }, + "x_min": { + "type": "number", + "title": "X Min", + "description": "Minimum inclusive x value in the region" + }, + "y_min": { + "type": "number", + "title": "Y Min", + "description": "Minimum inclusive y value in the region" + }, + "x_max": { + "type": "number", + "title": "X Max", + "description": "Maximum inclusive x value in the region" + }, + "y_max": { + "type": "number", + "title": "Y Max", + "description": "Maximum inclusive y value in the region" + }, + "angle": { + "type": "number", + "title": "Angle", + "description": "Clockwise rotation angle of the rectangle", + "default": 0.0 + }, + "type": { + "type": "string", + "const": "Rectangle", + "title": "Type", + "default": "Rectangle" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "x_axis", + "y_axis", + "x_min", + "y_min", + "x_max", + "y_max" + ], + "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)" + }, + "Region-Input": { + "oneOf": [ + { + "$ref": "#/components/schemas/CombinationOf_str_-Input" + }, + { + "$ref": "#/components/schemas/UnionOf_str_-Input" + }, + { + "$ref": "#/components/schemas/IntersectionOf_str_-Input" + }, + { + "$ref": "#/components/schemas/DifferenceOf_str_-Input" + }, + { + "$ref": "#/components/schemas/SymmetricDifferenceOf_str_-Input" + }, + { + "$ref": "#/components/schemas/Range_str_" + }, + { + "$ref": "#/components/schemas/Rectangle_str_" + }, + { + "$ref": "#/components/schemas/Polygon_str_" + }, + { + "$ref": "#/components/schemas/Circle_str_" + }, + { + "$ref": "#/components/schemas/Ellipse_str_" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "Circle": "#/components/schemas/Circle_str_", + "CombinationOf": "#/components/schemas/CombinationOf_str_-Input", + "DifferenceOf": "#/components/schemas/DifferenceOf_str_-Input", + "Ellipse": "#/components/schemas/Ellipse_str_", + "IntersectionOf": "#/components/schemas/IntersectionOf_str_-Input", + "Polygon": "#/components/schemas/Polygon_str_", + "Range": "#/components/schemas/Range_str_", + "Rectangle": "#/components/schemas/Rectangle_str_", + "SymmetricDifferenceOf": "#/components/schemas/SymmetricDifferenceOf_str_-Input", + "UnionOf": "#/components/schemas/UnionOf_str_-Input" + } + } + }, + "Region-Output": { + "oneOf": [ + { + "$ref": "#/components/schemas/CombinationOf_str_-Output" + }, + { + "$ref": "#/components/schemas/UnionOf_str_-Output" + }, + { + "$ref": "#/components/schemas/IntersectionOf_str_-Output" + }, + { + "$ref": "#/components/schemas/DifferenceOf_str_-Output" + }, + { + "$ref": "#/components/schemas/SymmetricDifferenceOf_str_-Output" + }, + { + "$ref": "#/components/schemas/Range_str_" + }, + { + "$ref": "#/components/schemas/Rectangle_str_" + }, + { + "$ref": "#/components/schemas/Polygon_str_" + }, + { + "$ref": "#/components/schemas/Circle_str_" + }, + { + "$ref": "#/components/schemas/Ellipse_str_" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "Circle": "#/components/schemas/Circle_str_", + "CombinationOf": "#/components/schemas/CombinationOf_str_-Output", + "DifferenceOf": "#/components/schemas/DifferenceOf_str_-Output", + "Ellipse": "#/components/schemas/Ellipse_str_", + "IntersectionOf": "#/components/schemas/IntersectionOf_str_-Output", + "Polygon": "#/components/schemas/Polygon_str_", + "Range": "#/components/schemas/Range_str_", + "Rectangle": "#/components/schemas/Rectangle_str_", + "SymmetricDifferenceOf": "#/components/schemas/SymmetricDifferenceOf_str_-Output", + "UnionOf": "#/components/schemas/UnionOf_str_-Output" + } + } + }, + "Repeat_str_": { + "properties": { + "num": { + "type": "integer", + "minimum": 1.0, + "title": "Num", + "description": "Number of frames to produce" + }, + "gap": { + "type": "boolean", + "title": "Gap", + "description": "If False and the slowest of the stack of Dimension is snaked then the end and start of consecutive iterations of Spec will have no gap", + "default": true + }, + "type": { + "type": "string", + "const": "Repeat", + "title": "Type", + "default": "Repeat" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "num" + ], + "title": "Repeat", + "description": "Repeat an empty frame num times.\n\nCan be used on the outside of a scan to repeat the same scan many times.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = 2 * ~Line.bounded(\"x\", 3, 4, 1)\n\nIf you want snaked axes to have no gap between iterations you can do:\n\n.. example_spec::\n\n from scanspec.specs import Line, Repeat\n\n spec = Repeat(2, gap=False) * ~Line.bounded(\"x\", 3, 4, 1)\n\n.. note:: There is no turnaround arrow at x=4" + }, + "SmallestStepResponse": { + "properties": { + "absolute": { + "type": "number", + "title": "Absolute", + "description": "Absolute smallest distance between two points on a single axis" + }, + "per_axis": { + "additionalProperties": { + "type": "number" + }, + "type": "object", + "title": "Per Axis", + "description": "Smallest distance between two points on each axis" + } + }, + "type": "object", + "required": [ + "absolute", + "per_axis" + ], + "title": "SmallestStepResponse", + "description": "Information about the smallest steps between points in a spec." + }, + "Snake_str_-Input": { + "properties": { + "spec": { + "$ref": "#/components/schemas/Spec-Input", + "description": "The Spec to run in reverse every other iteration" + }, + "type": { + "type": "string", + "const": "Snake", + "title": "Type", + "default": "Snake" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "spec" + ], + "title": "Snake", + "description": "Run the Spec in reverse on every other iteration when nested.\n\nTypically created with the ``~`` operator.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"y\", 1, 3, 3) * ~Line(\"x\", 3, 5, 5)" + }, + "Snake_str_-Output": { + "properties": { + "spec": { + "$ref": "#/components/schemas/Spec-Output", + "description": "The Spec to run in reverse every other iteration" + }, + "type": { + "type": "string", + "const": "Snake", + "title": "Type", + "default": "Snake" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "spec" + ], + "title": "Snake", + "description": "Run the Spec in reverse on every other iteration when nested.\n\nTypically created with the ``~`` operator.\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"y\", 1, 3, 3) * ~Line(\"x\", 3, 5, 5)" + }, + "Spec-Input": { + "oneOf": [ + { + "$ref": "#/components/schemas/Product_str_-Input" + }, + { + "$ref": "#/components/schemas/Repeat_str_" + }, + { + "$ref": "#/components/schemas/Zip_str_-Input" + }, + { + "$ref": "#/components/schemas/Mask_str_-Input" + }, + { + "$ref": "#/components/schemas/Snake_str_-Input" + }, + { + "$ref": "#/components/schemas/Concat_str_-Input" + }, + { + "$ref": "#/components/schemas/Squash_str_-Input" + }, + { + "$ref": "#/components/schemas/Line_str_" + }, + { + "$ref": "#/components/schemas/ConstantDuration_str_-Input" + }, + { + "$ref": "#/components/schemas/Static_str_" + }, + { + "$ref": "#/components/schemas/Spiral_str_" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "Concat": "#/components/schemas/Concat_str_-Input", + "ConstantDuration": "#/components/schemas/ConstantDuration_str_-Input", + "Line": "#/components/schemas/Line_str_", + "Mask": "#/components/schemas/Mask_str_-Input", + "Product": "#/components/schemas/Product_str_-Input", + "Repeat": "#/components/schemas/Repeat_str_", + "Snake": "#/components/schemas/Snake_str_-Input", + "Spiral": "#/components/schemas/Spiral_str_", + "Squash": "#/components/schemas/Squash_str_-Input", + "Static": "#/components/schemas/Static_str_", + "Zip": "#/components/schemas/Zip_str_-Input" + } + } + }, + "Spec-Output": { + "oneOf": [ + { + "$ref": "#/components/schemas/Product_str_-Output" + }, + { + "$ref": "#/components/schemas/Repeat_str_" + }, + { + "$ref": "#/components/schemas/Zip_str_-Output" + }, + { + "$ref": "#/components/schemas/Mask_str_-Output" + }, + { + "$ref": "#/components/schemas/Snake_str_-Output" + }, + { + "$ref": "#/components/schemas/Concat_str_-Output" + }, + { + "$ref": "#/components/schemas/Squash_str_-Output" + }, + { + "$ref": "#/components/schemas/Line_str_" + }, + { + "$ref": "#/components/schemas/ConstantDuration_str_-Output" + }, + { + "$ref": "#/components/schemas/Static_str_" + }, + { + "$ref": "#/components/schemas/Spiral_str_" + } + ], + "discriminator": { + "propertyName": "type", + "mapping": { + "Concat": "#/components/schemas/Concat_str_-Output", + "ConstantDuration": "#/components/schemas/ConstantDuration_str_-Output", + "Line": "#/components/schemas/Line_str_", + "Mask": "#/components/schemas/Mask_str_-Output", + "Product": "#/components/schemas/Product_str_-Output", + "Repeat": "#/components/schemas/Repeat_str_", + "Snake": "#/components/schemas/Snake_str_-Output", + "Spiral": "#/components/schemas/Spiral_str_", + "Squash": "#/components/schemas/Squash_str_-Output", + "Static": "#/components/schemas/Static_str_", + "Zip": "#/components/schemas/Zip_str_-Output" + } + } + }, + "Spiral_str_": { + "properties": { + "x_axis": { + "type": "string", + "title": "X Axis", + "description": "An identifier for what to move for x" + }, + "y_axis": { + "type": "string", + "title": "Y Axis", + "description": "An identifier for what to move for y" + }, + "x_start": { + "type": "number", + "title": "X Start", + "description": "x centre of the spiral" + }, + "y_start": { + "type": "number", + "title": "Y Start", + "description": "y centre of the spiral" + }, + "x_range": { + "type": "number", + "title": "X Range", + "description": "x width of the spiral" + }, + "y_range": { + "type": "number", + "title": "Y Range", + "description": "y width of the spiral" + }, + "num": { + "type": "integer", + "minimum": 1.0, + "title": "Num", + "description": "Number of frames to produce" + }, + "rotate": { + "type": "number", + "title": "Rotate", + "description": "How much to rotate the angle of the spiral", + "default": 0.0 + }, + "type": { + "type": "string", + "const": "Spiral", + "title": "Type", + "default": "Spiral" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "x_axis", + "y_axis", + "x_start", + "y_start", + "x_range", + "y_range", + "num" + ], + "title": "Spiral", + "description": "Archimedean spiral of \"x_axis\" and \"y_axis\".\n\nStarts at centre point (\"x_start\", \"y_start\") with angle \"rotate\". Produces\n\"num\" points in a spiral spanning width of \"x_range\" and height of \"y_range\"\n\n.. example_spec::\n\n from scanspec.specs import Spiral\n\n spec = Spiral(\"x\", \"y\", 1, 5, 10, 50, 30)" + }, + "Squash_str_-Input": { + "properties": { + "spec": { + "$ref": "#/components/schemas/Spec-Input", + "description": "The Spec to squash the dimensions of" + }, + "check_path_changes": { + "type": "boolean", + "title": "Check Path Changes", + "description": "If True path through scan will not be modified by squash", + "default": true + }, + "type": { + "type": "string", + "const": "Squash", + "title": "Type", + "default": "Squash" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "spec" + ], + "title": "Squash", + "description": "Squash a stack of Dimension together into a single expanded Dimension object.\n\nSee Also:\n `why-squash-can-change-path`\n\n.. example_spec::\n\n from scanspec.specs import Line, Squash\n\n spec = Squash(Line(\"y\", 1, 2, 3) * Line(\"x\", 0, 1, 4))" + }, + "Squash_str_-Output": { + "properties": { + "spec": { + "$ref": "#/components/schemas/Spec-Output", + "description": "The Spec to squash the dimensions of" + }, + "check_path_changes": { + "type": "boolean", + "title": "Check Path Changes", + "description": "If True path through scan will not be modified by squash", + "default": true + }, + "type": { + "type": "string", + "const": "Squash", + "title": "Type", + "default": "Squash" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "spec" + ], + "title": "Squash", + "description": "Squash a stack of Dimension together into a single expanded Dimension object.\n\nSee Also:\n `why-squash-can-change-path`\n\n.. example_spec::\n\n from scanspec.specs import Line, Squash\n\n spec = Squash(Line(\"y\", 1, 2, 3) * Line(\"x\", 0, 1, 4))" + }, + "Static_str_": { + "properties": { + "axis": { + "type": "string", + "title": "Axis", + "description": "An identifier for what to move" + }, + "value": { + "type": "number", + "title": "Value", + "description": "The value at each point" + }, + "num": { + "type": "integer", + "minimum": 1.0, + "title": "Num", + "description": "Number of frames to produce", + "default": 1 + }, + "type": { + "type": "string", + "const": "Static", + "title": "Type", + "default": "Static" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "axis", + "value" + ], + "title": "Static", + "description": "A static frame, repeated num times, with axis at value.\n\nCan be used to set axis=value at every point in a scan.\n\n.. example_spec::\n\n from scanspec.specs import Line, Static\n\n spec = Line(\"y\", 1, 2, 3).zip(Static(\"x\", 3))" + }, + "SymmetricDifferenceOf_str_-Input": { + "properties": { + "left": { + "$ref": "#/components/schemas/Region-Input", + "description": "The left-hand Region to combine" + }, + "right": { + "$ref": "#/components/schemas/Region-Input", + "description": "The right-hand Region to combine" + }, + "type": { + "type": "string", + "const": "SymmetricDifferenceOf", + "title": "Type", + "default": "SymmetricDifferenceOf" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "left", + "right" + ], + "title": "SymmetricDifferenceOf", + "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])" + }, + "SymmetricDifferenceOf_str_-Output": { + "properties": { + "left": { + "$ref": "#/components/schemas/Region-Output", + "description": "The left-hand Region to combine" + }, + "right": { + "$ref": "#/components/schemas/Region-Output", + "description": "The right-hand Region to combine" + }, + "type": { + "type": "string", + "const": "SymmetricDifferenceOf", + "title": "Type", + "default": "SymmetricDifferenceOf" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "left", + "right" + ], + "title": "SymmetricDifferenceOf", + "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])" + }, + "UnionOf_str_-Input": { + "properties": { + "left": { + "$ref": "#/components/schemas/Region-Input", + "description": "The left-hand Region to combine" + }, + "right": { + "$ref": "#/components/schemas/Region-Input", + "description": "The right-hand Region to combine" + }, + "type": { + "type": "string", + "const": "UnionOf", + "title": "Type", + "default": "UnionOf" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "left", + "right" + ], + "title": "UnionOf", + "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])" + }, + "UnionOf_str_-Output": { + "properties": { + "left": { + "$ref": "#/components/schemas/Region-Output", + "description": "The left-hand Region to combine" + }, + "right": { + "$ref": "#/components/schemas/Region-Output", + "description": "The right-hand Region to combine" + }, + "type": { + "type": "string", + "const": "UnionOf", + "title": "Type", + "default": "UnionOf" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "left", + "right" + ], + "title": "UnionOf", + "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])" + }, + "ValidResponse": { + "properties": { + "input_spec": { + "$ref": "#/components/schemas/Spec-Output", + "description": "The input scanspec" + }, + "valid_spec": { + "$ref": "#/components/schemas/Spec-Output", + "description": "The validated version of the spec" + } + }, + "type": "object", + "required": [ + "input_spec", + "valid_spec" + ], + "title": "ValidResponse", + "description": "Response model for spec validation." + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "type": "array", + "title": "Location" + }, + "msg": { + "type": "string", + "title": "Message" + }, + "type": { + "type": "string", + "title": "Error Type" + } + }, + "type": "object", + "required": [ + "loc", + "msg", + "type" + ], + "title": "ValidationError" + }, + "Zip_str_-Input": { + "properties": { + "left": { + "$ref": "#/components/schemas/Spec-Input", + "description": "The left-hand Spec to Zip, will appear earlier in axes" + }, + "right": { + "$ref": "#/components/schemas/Spec-Input", + "description": "The right-hand Spec to Zip, will appear later in axes" + }, + "type": { + "type": "string", + "const": "Zip", + "title": "Type", + "default": "Zip" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "left", + "right" + ], + "title": "Zip", + "description": "Run two Specs in parallel, merging their midpoints together.\n\nTypically formed using `Spec.zip`.\n\nStacks of Dimension are merged by:\n\n- If right creates a stack of a single Dimension object of size 1, expand it to\n the size of the fastest Dimension object created by left\n- Merge individual Dimension objects together from fastest to slowest\n\nThis means that Zipping a Spec producing stack [l2, l1] with a Spec\nproducing stack [r1] will assert len(l1)==len(r1), and produce\nstack [l2, l1.zip(r1)].\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"z\", 1, 2, 3) * Line(\"y\", 3, 4, 5).zip(Line(\"x\", 4, 5, 5))" + }, + "Zip_str_-Output": { + "properties": { + "left": { + "$ref": "#/components/schemas/Spec-Output", + "description": "The left-hand Spec to Zip, will appear earlier in axes" + }, + "right": { + "$ref": "#/components/schemas/Spec-Output", + "description": "The right-hand Spec to Zip, will appear later in axes" + }, + "type": { + "type": "string", + "const": "Zip", + "title": "Type", + "default": "Zip" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "left", + "right" + ], + "title": "Zip", + "description": "Run two Specs in parallel, merging their midpoints together.\n\nTypically formed using `Spec.zip`.\n\nStacks of Dimension are merged by:\n\n- If right creates a stack of a single Dimension object of size 1, expand it to\n the size of the fastest Dimension object created by left\n- Merge individual Dimension objects together from fastest to slowest\n\nThis means that Zipping a Spec producing stack [l2, l1] with a Spec\nproducing stack [r1] will assert len(l1)==len(r1), and produce\nstack [l2, l1.zip(r1)].\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"z\", 1, 2, 3) * Line(\"y\", 3, 4, 5).zip(Line(\"x\", 4, 5, 5))" + } + } + } +} From cf2a2a554eb869f0b11a052edb02ca3e9741cf43 Mon Sep 17 00:00:00 2001 From: Luis Segalla Date: Fri, 18 Jul 2025 13:49:37 +0100 Subject: [PATCH 07/38] Updated Specs - Changed Duration class to now be `ConstantDuration`. The new class wraps a `Spec` and upon a call to `calculate` will update the `.duration` attribute accordingly. - Made `duration` into a class method that errors if not implemented for each new `Spec`. - It's not possible anymore to have `Duration` only Spec being defined, you'll need another `Spec` to wrap it. --- src/scanspec/specs.py | 173 +++++++++++++++++++++++++++++------------- 1 file changed, 119 insertions(+), 54 deletions(-) diff --git a/src/scanspec/specs.py b/src/scanspec/specs.py index 51c1fb27..994a4eb7 100644 --- a/src/scanspec/specs.py +++ b/src/scanspec/specs.py @@ -8,7 +8,7 @@ from __future__ import annotations from collections.abc import Callable, Mapping -from typing import Any, Generic, overload +from typing import Any, Generic, Literal, overload import numpy as np import numpy.typing as npt @@ -31,7 +31,7 @@ from .regions import Region, get_mask __all__ = [ - "DURATION", + "ConstantDuration", "Spec", "Product", "Repeat", @@ -47,9 +47,7 @@ "step", ] - -#: Can be used as a special key to indicate how long each point should be -DURATION = "DURATION" +VARIABLE_DURATION = "VARIABLE_DURATION" @discriminated_union_of_subclasses @@ -64,6 +62,11 @@ class Spec(Generic[Axis]): - ``~``: `Snake` the Spec, reversing every other iteration of it """ + def __post_init__(self): + # Call axes and duration as they do error checking for zip, product etc. + self.axes() + self.duration() + def axes(self) -> list[Axis]: # noqa: D102 """Return the list of axes that are present in the scan. @@ -71,6 +74,16 @@ def axes(self) -> list[Axis]: # noqa: D102 """ raise NotImplementedError(self) + def duration(self) -> float | None | Literal["VARIABLE_DURATION"]: + """Returns the duration of each scan point. + + Return value will be one of: + - ``None``: No duration defined + - ``float``: A constant duration for each point + - `VARIABLE_DURATION`: A different duration for each point + """ + raise NotImplementedError(self) + def calculate( self, bounds: bool = True, nested: bool = False ) -> list[Dimension[Axis]]: # noqa: D102 @@ -149,6 +162,19 @@ class Product(Spec[Axis]): def axes(self) -> list[Axis]: # noqa: D102 return self.outer.axes() + self.inner.axes() + def duration(self): # noqa: D102 + if self.outer.duration() is None and self.inner.duration is None: + return None + else: + if self.outer.duration() == self.inner.duration: + return self.inner.duration() + elif self.outer.duration() is None: + return self.inner.duration() + elif self.inner.duration(): + return self.outer.duration() + else: + return VARIABLE_DURATION + def calculate( # noqa: D102 self, bounds: bool = True, nested: bool = False ) -> list[Dimension[Axis]]: @@ -190,6 +216,9 @@ class Repeat(Spec[Axis]): def axes(self) -> list[Axis]: # noqa: D102 return [] + def duration(self): # noqa: D102 + return None + def calculate( # noqa: D102 self, bounds: bool = True, nested: bool = False ) -> list[Dimension[Axis]]: @@ -229,6 +258,19 @@ class Zip(Spec[Axis]): def axes(self) -> list[Axis]: # noqa: D102 return self.left.axes() + self.right.axes() + def duration(self): # noqa: D102 + if self.left.duration() is None and self.right.duration is None: + return None + else: + if self.left.duration() == self.right.duration: + return self.right.duration() + elif self.left.duration() is None: + return self.right.duration() + elif self.right.duration(): + return self.left.duration() + else: + return VARIABLE_DURATION + def calculate( # noqa: D102 self, bounds: bool = True, nested: bool = False ) -> list[Dimension[Axis]]: @@ -300,6 +342,12 @@ class Mask(Spec[Axis]): def axes(self) -> list[Axis]: # noqa: D102 return self.spec.axes() + def duration(self): # noqa: D102 + if self.spec.duration() is None: + return None + else: + return self.spec.duration() + def calculate( # noqa: D102 self, bounds: bool = True, nested: bool = False ) -> list[Dimension[Axis]]: @@ -360,6 +408,9 @@ class Snake(Spec[Axis]): def axes(self) -> list[Axis]: # noqa: D102 return self.spec.axes() + def duration(self): # noqa: D102 + return self.spec.duration() + def calculate( # noqa: D102 self, bounds: bool = True, nested: bool = False ) -> list[Dimension[Axis]]: @@ -405,6 +456,19 @@ def axes(self) -> list[Axis]: # noqa: D102 assert set(left_axes) == set(right_axes), f"axes {left_axes} != {right_axes}" return left_axes + def duration(self): # noqa: D102 + if self.left.duration() is None and self.right.duration is None: + return None + else: + if self.left.duration() == self.right.duration: + return self.right.duration() + elif self.left.duration() is None: + return self.right.duration() + elif self.right.duration(): + return self.left.duration() + else: + return VARIABLE_DURATION + def calculate( # noqa: D102 self, bounds: bool = True, nested: bool = False ) -> list[Dimension[Axis]]: @@ -442,6 +506,9 @@ class Squash(Spec[Axis]): def axes(self) -> list[Axis]: # noqa: D102 return self.spec.axes() + def duration(self): # noqa: D102 + return self.spec.duration() + def calculate( # noqa: D102 self, bounds: bool = True, nested: bool = False ) -> list[Dimension[Axis]]: @@ -496,6 +563,9 @@ class Line(Spec[Axis]): def axes(self) -> list[Axis]: # noqa: D102 return [self.axis] + def duration(self): # noqa: D102 + return None + def _line_from_indexes( self, indexes: npt.NDArray[np.float64] ) -> dict[Axis, npt.NDArray[np.float64]]: @@ -551,27 +621,38 @@ def bounded( @dataclass(config=StrictConfig) -class Duration(Spec[Axis]): +class ConstantDuration(Spec[Axis]): """A special spec used to hold information about the duration of each frame.""" - duration: float = Field(description="The value at each point") - num: int = Field(ge=1, description="Number of frames to produce", default=1) + spec: Spec[Axis] = Field(description="Spec contaning the path to be followed") + constant_duration: float = Field(description="The value at each point") + fly: bool = Field( + description="Define if the Spec is a fly or step scan", default=False + ) def axes(self) -> list[Axis]: # noqa: D102 - return [] + return self.spec.axes() - def calculate( + def duration(self) -> float: # noqa: D102 + if ( + self.spec.duration() is not None + and self.spec.duration() != self.constant_duration + ): + raise ValueError(self) # Maybe another error? + return self.constant_duration + + def calculate( # noqa: D102 self, bounds: bool = True, nested: bool = False ) -> list[Dimension[Axis]]: - return [ - Dimension( - midpoints={}, - lower=None, - upper=None, - gap=None, - duration=np.full(self.num, self.duration), - ) - ] + dimensions = self.spec.calculate() + for d in dimensions: + if d.duration is not None and d.duration != self.duration: + raise ValueError(self) + + dimensions[-1].duration = np.full( + len(dimensions[-1].midpoints[self.axes()[-1]]), self.constant_duration + ) + return dimensions @dataclass(config=StrictConfig) @@ -591,25 +672,12 @@ class Static(Spec[Axis]): value: float = Field(description="The value at each point") num: int = Field(ge=1, description="Number of frames to produce", default=1) - @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 axes(self) -> list[Axis]: # noqa: D102 return [self.axis] + def duration(self): # noqa: D102 + return None + def _repeats_from_indexes( self, indexes: npt.NDArray[np.float64] ) -> dict[Axis, npt.NDArray[np.float64]]: @@ -623,9 +691,6 @@ def calculate( # noqa: D102 ) -Static.duration = validate_call(Static.duration) # type:ignore - - @dataclass(config=StrictConfig) class Spiral(Spec[Axis]): """Archimedean spiral of "x_axis" and "y_axis". @@ -657,6 +722,9 @@ def axes(self) -> list[Axis]: # noqa: D102 # TODO: reversed from __init__ args, a good idea? return [self.y_axis, self.x_axis] + def duration(self): # noqa: D102 + return None + def _spiral_from_indexes( self, indexes: npt.NDArray[np.float64] ) -> dict[Axis, npt.NDArray[np.float64]]: @@ -725,7 +793,7 @@ def spaced( Spiral.spaced = validate_call(Spiral.spaced) # type:ignore -def fly(spec: Spec[Axis], duration: float) -> Spec[Axis | str]: +def fly(spec: Spec[Axis], duration: float) -> Spec[Axis]: """Flyscan, zipping with fixed duration for every frame. Args: @@ -739,10 +807,10 @@ def fly(spec: Spec[Axis], duration: float) -> Spec[Axis | str]: spec = fly(Line("x", 1, 2, 3), 0.1) """ - return spec.zip(Duration(duration)) + return ConstantDuration(spec=spec, constant_duration=duration, fly=True) -def step(spec: Spec[Axis], duration: float, num: int = 1) -> Spec[Axis | str]: +def step(spec: Spec[Axis], duration: float, num: int = 1) -> Spec[Axis]: """Step scan, with num frames of given duration at each frame in the spec. Args: @@ -758,7 +826,7 @@ def step(spec: Spec[Axis], duration: float, num: int = 1) -> Spec[Axis | str]: spec = step(Line("x", 1, 2, 3), 0.1) """ - return spec * Duration(duration) + return ConstantDuration(spec, duration, fly=False) def get_constant_duration(frames: list[Dimension[Any]]) -> float | None: @@ -772,17 +840,14 @@ def get_constant_duration(frames: list[Dimension[Any]]) -> float | None: 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 + duration_frames = [f.duration for f in frames if f.duration is not None] + if len(duration_frames) == 0: + # List of frames has no frame with duration in it return None + # First element of the first duration array + first_duration = duration_frames[0][0] + for frame in duration_frames: + if np.any(frame != first_duration): + # Not all durations are the same + return None return first_duration From 6eeccd7f9794464d248b320747b737cc0ce3699e Mon Sep 17 00:00:00 2001 From: Luis Segalla Date: Fri, 18 Jul 2025 13:51:06 +0100 Subject: [PATCH 08/38] Updated tests --- tests/test_specs.py | 127 ++++++++++++-------------------------------- 1 file changed, 35 insertions(+), 92 deletions(-) diff --git a/tests/test_specs.py b/tests/test_specs.py index e35c3b36..faff75cc 100644 --- a/tests/test_specs.py +++ b/tests/test_specs.py @@ -1,4 +1,3 @@ -import re from typing import Any import pytest @@ -6,8 +5,8 @@ from scanspec.core import Path, SnakedDimension from scanspec.regions import Circle, Ellipse, Polygon, Rectangle from scanspec.specs import ( - DURATION, Concat, + ConstantDuration, Line, Mask, Repeat, @@ -17,7 +16,7 @@ Static, Zip, fly, - get_constant_duration, + # get_constant_duration, step, ) @@ -30,16 +29,6 @@ def ints(s: str) -> Any: return approx([int(t) for t in s]) -def test_one_point_duration() -> None: - duration = Static.duration(1.0) - (dim,) = duration.calculate() - assert dim.midpoints == {DURATION: approx([1.0])} - assert dim.lower == {DURATION: approx([1.0])} - assert dim.upper == {DURATION: approx([1.0])} - assert not isinstance(dim, SnakedDimension) - assert dim.gap == ints("0") - - def test_one_point_line() -> None: inst = Line(x, 0, 1, 1) (dim,) = inst.calculate() @@ -48,6 +37,7 @@ def test_one_point_line() -> None: assert dim.upper == {x: approx([0.5])} assert not isinstance(dim, SnakedDimension) assert dim.gap == ints("1") + assert dim.duration is None def test_two_point_line() -> None: @@ -61,11 +51,10 @@ def test_two_point_line() -> None: def test_two_point_stepped_line() -> None: inst = step(Line(x, 0, 1, 2), 0.1) - dimx, dimt = inst.calculate() - assert dimx.midpoints == dimx.lower == dimx.upper == {x: approx([0, 1])} - assert dimt.midpoints == dimt.lower == dimt.upper == {} - assert dimt.duration == approx([0.1]) - assert inst.frames().gap == ints("11") + (dim,) = inst.calculate() + assert dim.midpoints == {x: approx([0, 1])} + assert inst.frames().gap == ints("10") + assert dim.duration == approx([0.1, 0.1]) def test_two_point_fly_line() -> None: @@ -193,19 +182,27 @@ def test_squashed_product() -> None: def test_squashed_multiplied_snake_scan() -> None: - inst = Line(z, 1, 2, 2) * Squash( - Line(y, 1, 2, 2) * ~Line.bounded(x, 3, 7, 2) * Static.duration(9, 2) + inst = ConstantDuration( + Line(z, 1, 2, 2) * Squash(Line(y, 1, 2, 2) * ~Line.bounded(x, 3, 7, 2)), + 9, ) - assert inst.axes() == [z, y, x, DURATION] + assert inst.axes() == [z, y, x] dimz, dimxyt = inst.calculate() - for d in dimxyt.midpoints, dimxyt.lower, dimxyt.upper: - assert d == { - x: approx([4, 4, 6, 6, 6, 6, 4, 4]), - y: approx([1, 1, 1, 1, 2, 2, 2, 2]), - DURATION: approx([9, 9, 9, 9, 9, 9, 9, 9]), - } - assert dimz.midpoints == dimz.lower == dimz.upper == {z: approx([1, 2])} - assert inst.frames().gap == ints("1010101010101010") + assert dimxyt.midpoints == { + x: approx([4, 6, 6, 4]), + y: approx([1, 1, 2, 2]), + } + assert dimxyt.lower == { + x: approx([3, 5, 7, 5]), + y: approx([1, 1, 2, 2]), + } + assert dimxyt.upper == { + x: approx([5, 7, 5, 3]), + y: approx([1, 1, 2, 2]), + } + assert dimxyt.duration == approx([9, 9, 9, 9]) + assert dimz.midpoints == {z: approx([1, 2])} + assert inst.frames().gap == ints("10101010") def test_product_snaking_lines() -> None: @@ -312,17 +309,21 @@ def test_rect_region_intersection() -> None: def test_rect_region_difference() -> None: # Bracket to force testing Mask.__sub__ rather than Region.__sub__ - inst = ( - Line(y, 1, 3, 5) * Line(x, 0, 2, 3).zip(Static(DURATION, 0.1)) - & Rectangle(x, y, 0, 1, 1.5, 2.2) + spec = ( + Line(y, 1, 3, 5) * Line(x, 0, 2, 3) & Rectangle(x, y, 0, 1, 1.5, 2.2) ) - Rectangle(x, y, 0.5, 1.5, 2, 2.5) - assert inst.axes() == [y, x, DURATION] + + inst = ConstantDuration( + spec, + 0.1, + ) + assert inst.axes() == [y, x] (dim,) = inst.calculate() assert dim.midpoints == { x: approx([0, 1, 0, 0]), y: approx([1, 1, 1.5, 2]), - DURATION: approx([0.1, 0.1, 0.1, 0.1]), } + assert dim.duration == approx([0.1, 0.1, 0.1, 0.1]) assert dim.gap == ints("1011") @@ -547,61 +548,3 @@ def test_multiple_statics_with_grid(): ) def test_shape(spec: Spec[Any], expected_shape: tuple[int, ...]): assert expected_shape == spec.shape() - - -def test_single_frame_single_point(): - spec = Static.duration(0.1) - assert get_constant_duration(spec.calculate()) == 0.1 - - -def test_consistent_points(): - spec: Spec[str] = Static.duration(0.1).concat(Static.duration(0.1)) - assert get_constant_duration(spec.calculate()) == 0.1 - - -def test_inconsistent_points(): - spec = Static.duration(0.1).concat(Static.duration(0.2)) - assert get_constant_duration(spec.calculate()) is None - - -def test_frame_with_multiple_axes(): - spec = Static.duration(0.1).zip(Line.bounded("x", 0, 0, 1)) - frames = spec.calculate() - assert len(frames) == 1 - assert get_constant_duration(frames) == 0.1 - - -def test_inconsistent_frame_with_multiple_axes(): - spec = ( - Static.duration(0.1) - .concat(Static.duration(0.2)) - .zip(Line.bounded("x", 0, 0, 2)) - ) - frames = spec.calculate() - assert len(frames) == 1 - assert get_constant_duration(frames) is None - - -def test_non_static_spec_duration(): - spec = Line.bounded(DURATION, 0, 0, 3) - frames = spec.calculate() - assert len(frames) == 1 - assert get_constant_duration(frames) == 0 - - -def test_multiple_duration_frames(): - spec = ( - Static.duration(0.1) - .concat(Static.duration(0.2)) - .zip(Line.bounded(DURATION, 0, 0, 2)) - ) - with pytest.raises( - AssertionError, match=re.escape("Zipping would overwrite axes ['DURATION']") - ): - spec.calculate() - spec = ( # TODO: refactor when https://github.com/bluesky/scanspec/issues/90 - Static.duration(0.1) * Line.bounded(DURATION, 0, 0, 2) - ) - frames = spec.calculate() - assert len(frames) == 2 - assert get_constant_duration(frames) is None From 6e27691112a6cf4bb7afdec12c04fc1b4727bae1 Mon Sep 17 00:00:00 2001 From: Luis Segalla Date: Wed, 23 Jul 2025 14:13:01 +0100 Subject: [PATCH 09/38] PR changes Updated code based on PR comments. --- src/scanspec/core.py | 20 ++++---- src/scanspec/specs.py | 107 ++++++++++++++++++++---------------------- tests/test_specs.py | 11 +++-- 3 files changed, 69 insertions(+), 69 deletions(-) diff --git a/src/scanspec/core.py b/src/scanspec/core.py index 1f6cf9cb..02aaf0e6 100644 --- a/src/scanspec/core.py +++ b/src/scanspec/core.py @@ -381,6 +381,8 @@ def __init__( # as the duration provided for the __len__ method elif gap is None and self.duration is not None and len(self.midpoints) == 0: self.gap = np.full(len(self.duration), False) + else: + raise ValueError("self.gap is undefined") # Check all axes and ordering are the same assert list(self.midpoints) == list(self.lower) == list(self.upper), ( f"Mismatching axes " @@ -442,7 +444,7 @@ def extract_duration( for d in durations: if d is not None: return d[dim_indices] - return None + return None return _merge_frames( self, @@ -488,9 +490,11 @@ def concat_duration( durations: Sequence[DurationArray | None], ) -> DurationArray | None: # Check if there are more than one duration being zipped - specified_durations = [d for d in durations if d is not None] - d = np.concatenate(specified_durations) - return d + if any(d is None for d in durations): + raise ValueError( + "Can't concatenate dimensions unless all or none provide durations" + ) + return np.concatenate(durations) # type: ignore return _merge_frames( self, @@ -526,7 +530,8 @@ def zip_gap(gaps: Sequence[GapArray]) -> GapArray: def zip_duration( durations: Sequence[DurationArray | None], ) -> DurationArray | None: - # Check if there are more than one duration being zipped + # We will be passed a sequence of durations where at least one + # is not None. We require that there is precisely one specified_durations = [d for d in durations if d is not None] if len(specified_durations) != 1: raise ValueError("Can't have more than one durations array") @@ -545,8 +550,7 @@ def _merge_frames( *stack: Dimension[Axis], dict_merge: Callable[[Sequence[AxesPoints[Axis]]], AxesPoints[Axis]], # type: ignore gap_merge: Callable[[Sequence[GapArray]], GapArray | None], - duration_merge: Callable[[Sequence[DurationArray]], DurationArray | None] - | None = None, + duration_merge: Callable[[Sequence[DurationArray | None]], DurationArray | None], ) -> Dimension[Axis]: types = {type(fs) for fs in stack} assert len(types) == 1, f"Mismatching types for {stack}" @@ -563,7 +567,7 @@ def _merge_frames( upper=dict_merge([fs.upper for fs in stack]) if any(fs.midpoints is not fs.upper for fs in stack) else None, - duration=duration_merge([fs.duration for fs in stack]) # type: ignore | not sure + duration=duration_merge([fs.duration for fs in stack]) if any(fs.duration is not None for fs in stack) else None, ) diff --git a/src/scanspec/specs.py b/src/scanspec/specs.py index 994a4eb7..a771cf47 100644 --- a/src/scanspec/specs.py +++ b/src/scanspec/specs.py @@ -7,6 +7,7 @@ from __future__ import annotations +import warnings from collections.abc import Callable, Mapping from typing import Any, Generic, Literal, overload @@ -162,18 +163,11 @@ class Product(Spec[Axis]): def axes(self) -> list[Axis]: # noqa: D102 return self.outer.axes() + self.inner.axes() - def duration(self): # noqa: D102 - if self.outer.duration() is None and self.inner.duration is None: - return None - else: - if self.outer.duration() == self.inner.duration: - return self.inner.duration() - elif self.outer.duration() is None: - return self.inner.duration() - elif self.inner.duration(): - return self.outer.duration() - else: - return VARIABLE_DURATION + def duration(self) -> float | None | Literal["VARIABLE_DURATION"]: # noqa: D102 + outer, inner = self.outer.duration(), self.inner.duration() + if outer is not None and inner is not None: + raise ValueError("Both outer and inner define a duration") + return inner if inner is not None else outer def calculate( # noqa: D102 self, bounds: bool = True, nested: bool = False @@ -216,7 +210,7 @@ class Repeat(Spec[Axis]): def axes(self) -> list[Axis]: # noqa: D102 return [] - def duration(self): # noqa: D102 + def duration(self) -> float | None | Literal["VARIABLE_DURATION"]: # noqa: D102 return None def calculate( # noqa: D102 @@ -258,18 +252,11 @@ class Zip(Spec[Axis]): def axes(self) -> list[Axis]: # noqa: D102 return self.left.axes() + self.right.axes() - def duration(self): # noqa: D102 - if self.left.duration() is None and self.right.duration is None: - return None - else: - if self.left.duration() == self.right.duration: - return self.right.duration() - elif self.left.duration() is None: - return self.right.duration() - elif self.right.duration(): - return self.left.duration() - else: - return VARIABLE_DURATION + def duration(self) -> float | None | Literal["VARIABLE_DURATION"]: # noqa: D102 + left, right = self.left.duration(), self.right.duration() + if left is not None and right is not None: + raise ValueError("Both left and right define a duration") + return left if left is not None else right def calculate( # noqa: D102 self, bounds: bool = True, nested: bool = False @@ -342,11 +329,8 @@ class Mask(Spec[Axis]): def axes(self) -> list[Axis]: # noqa: D102 return self.spec.axes() - def duration(self): # noqa: D102 - if self.spec.duration() is None: - return None - else: - return self.spec.duration() + def duration(self) -> float | None | Literal["VARIABLE_DURATION"]: # noqa: D102 + return self.spec.duration() def calculate( # noqa: D102 self, bounds: bool = True, nested: bool = False @@ -408,7 +392,7 @@ class Snake(Spec[Axis]): def axes(self) -> list[Axis]: # noqa: D102 return self.spec.axes() - def duration(self): # noqa: D102 + def duration(self) -> float | None | Literal["VARIABLE_DURATION"]: # noqa: D102 return self.spec.duration() def calculate( # noqa: D102 @@ -456,18 +440,17 @@ def axes(self) -> list[Axis]: # noqa: D102 assert set(left_axes) == set(right_axes), f"axes {left_axes} != {right_axes}" return left_axes - def duration(self): # noqa: D102 - if self.left.duration() is None and self.right.duration is None: - return None + def duration(self) -> float | None | Literal["VARIABLE_DURATION"]: # noqa: D102 + left, right = self.left.duration(), self.right.duration() + if left == right: + # They are producing the same duration + return left + elif left is None or right is None: + # They aren't both None, but if one is then raise + raise ValueError("Only one of left and right defines a duration") else: - if self.left.duration() == self.right.duration: - return self.right.duration() - elif self.left.duration() is None: - return self.right.duration() - elif self.right.duration(): - return self.left.duration() - else: - return VARIABLE_DURATION + # They both exist, but are different, so are variable + return VARIABLE_DURATION def calculate( # noqa: D102 self, bounds: bool = True, nested: bool = False @@ -506,7 +489,7 @@ class Squash(Spec[Axis]): def axes(self) -> list[Axis]: # noqa: D102 return self.spec.axes() - def duration(self): # noqa: D102 + def duration(self) -> float | None | Literal["VARIABLE_DURATION"]: # noqa: D102 return self.spec.duration() def calculate( # noqa: D102 @@ -563,7 +546,7 @@ class Line(Spec[Axis]): def axes(self) -> list[Axis]: # noqa: D102 return [self.axis] - def duration(self): # noqa: D102 + def duration(self) -> float | None | Literal["VARIABLE_DURATION"]: # noqa: D102 return None def _line_from_indexes( @@ -633,12 +616,9 @@ class ConstantDuration(Spec[Axis]): def axes(self) -> list[Axis]: # noqa: D102 return self.spec.axes() - def duration(self) -> float: # noqa: D102 - if ( - self.spec.duration() is not None - and self.spec.duration() != self.constant_duration - ): - raise ValueError(self) # Maybe another error? + def duration(self) -> float | None | Literal["VARIABLE_DURATION"]: # noqa: D102 + if self.spec.duration() is not None: + raise ValueError(f"{self.spec} already defines a duration") return self.constant_duration def calculate( # noqa: D102 @@ -648,10 +628,20 @@ def calculate( # noqa: D102 for d in dimensions: if d.duration is not None and d.duration != self.duration: raise ValueError(self) - - dimensions[-1].duration = np.full( - len(dimensions[-1].midpoints[self.axes()[-1]]), self.constant_duration - ) + if self.fly: + dimensions[-1].duration = np.full( + len(dimensions[-1].midpoints[self.axes()[-1]]), self.constant_duration + ) + else: + step_duration: Dimension[Axis] = Dimension( + midpoints={}, + gap=None, + duration=np.full( + len(dimensions[-1].midpoints[self.axes()[-1]]), + self.constant_duration, + ), + ) + dimensions = dimensions + [step_duration] return dimensions @@ -675,7 +665,7 @@ class Static(Spec[Axis]): def axes(self) -> list[Axis]: # noqa: D102 return [self.axis] - def duration(self): # noqa: D102 + def duration(self) -> float | None | Literal["VARIABLE_DURATION"]: # noqa: D102 return None def _repeats_from_indexes( @@ -722,7 +712,7 @@ def axes(self) -> list[Axis]: # noqa: D102 # TODO: reversed from __init__ args, a good idea? return [self.y_axis, self.x_axis] - def duration(self): # noqa: D102 + def duration(self) -> float | None | Literal["VARIABLE_DURATION"]: # noqa: D102 return None def _spiral_from_indexes( @@ -840,6 +830,11 @@ def get_constant_duration(frames: list[Dimension[Any]]) -> float | None: None: otherwise """ + warnings.warn( + "get_constant_duration method is deprecated! Use spec.duration() instead", + DeprecationWarning, + stacklevel=2, + ) duration_frames = [f.duration for f in frames if f.duration is not None] if len(duration_frames) == 0: # List of frames has no frame with duration in it diff --git a/tests/test_specs.py b/tests/test_specs.py index faff75cc..6323ba85 100644 --- a/tests/test_specs.py +++ b/tests/test_specs.py @@ -51,10 +51,10 @@ def test_two_point_line() -> None: def test_two_point_stepped_line() -> None: inst = step(Line(x, 0, 1, 2), 0.1) - (dim,) = inst.calculate() + (dim, dimt) = inst.calculate() assert dim.midpoints == {x: approx([0, 1])} - assert inst.frames().gap == ints("10") - assert dim.duration == approx([0.1, 0.1]) + assert dim.gap == ints("10") + assert dimt.duration == approx([0.1, 0.1]) def test_two_point_fly_line() -> None: @@ -185,6 +185,7 @@ def test_squashed_multiplied_snake_scan() -> None: inst = ConstantDuration( Line(z, 1, 2, 2) * Squash(Line(y, 1, 2, 2) * ~Line.bounded(x, 3, 7, 2)), 9, + fly=True, ) assert inst.axes() == [z, y, x] dimz, dimxyt = inst.calculate() @@ -318,12 +319,12 @@ def test_rect_region_difference() -> None: 0.1, ) assert inst.axes() == [y, x] - (dim,) = inst.calculate() + (dim, dimt) = inst.calculate() assert dim.midpoints == { x: approx([0, 1, 0, 0]), y: approx([1, 1, 1.5, 2]), } - assert dim.duration == approx([0.1, 0.1, 0.1, 0.1]) + assert dimt.duration == approx([0.1, 0.1, 0.1, 0.1]) assert dim.gap == ints("1011") From 7968f78d2c07f2df27028b6c84d049c7626ad76e Mon Sep 17 00:00:00 2001 From: Luis Segalla Date: Thu, 24 Jul 2025 09:11:03 +0100 Subject: [PATCH 10/38] Updated calculate method on ConstantDuration class Made it so that for step scans the new dimension has a single duration point. --- src/scanspec/specs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scanspec/specs.py b/src/scanspec/specs.py index a771cf47..6576eff7 100644 --- a/src/scanspec/specs.py +++ b/src/scanspec/specs.py @@ -637,7 +637,7 @@ def calculate( # noqa: D102 midpoints={}, gap=None, duration=np.full( - len(dimensions[-1].midpoints[self.axes()[-1]]), + 1, self.constant_duration, ), ) From c6e036fa64fa00c2d394e2fda6bc08be86655bf7 Mon Sep 17 00:00:00 2001 From: Luis Segalla Date: Thu, 24 Jul 2025 09:11:19 +0100 Subject: [PATCH 11/38] Updated tests --- tests/test_specs.py | 54 +++++++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/tests/test_specs.py b/tests/test_specs.py index 6323ba85..4016cae4 100644 --- a/tests/test_specs.py +++ b/tests/test_specs.py @@ -54,7 +54,7 @@ def test_two_point_stepped_line() -> None: (dim, dimt) = inst.calculate() assert dim.midpoints == {x: approx([0, 1])} assert dim.gap == ints("10") - assert dimt.duration == approx([0.1, 0.1]) + assert dimt.duration == approx([0.1]) def test_two_point_fly_line() -> None: @@ -182,28 +182,21 @@ def test_squashed_product() -> None: def test_squashed_multiplied_snake_scan() -> None: - inst = ConstantDuration( - Line(z, 1, 2, 2) * Squash(Line(y, 1, 2, 2) * ~Line.bounded(x, 3, 7, 2)), - 9, - fly=True, + inst: Spec[str] = Line(z, 1, 2, 2) * Squash( + Line(y, 1, 2, 2) + * ~Line.bounded(x, 3, 7, 2) + * ConstantDuration(Repeat(2), 9, fly=False) # type: ignore ) assert inst.axes() == [z, y, x] - dimz, dimxyt = inst.calculate() - assert dimxyt.midpoints == { - x: approx([4, 6, 6, 4]), - y: approx([1, 1, 2, 2]), - } - assert dimxyt.lower == { - x: approx([3, 5, 7, 5]), - y: approx([1, 1, 2, 2]), - } - assert dimxyt.upper == { - x: approx([5, 7, 5, 3]), - y: approx([1, 1, 2, 2]), - } - assert dimxyt.duration == approx([9, 9, 9, 9]) - assert dimz.midpoints == {z: approx([1, 2])} - assert inst.frames().gap == ints("10101010") + (dimz, dimxyt) = inst.calculate() + for d in dimxyt.midpoints, dimxyt.lower, dimxyt.upper: + assert d == { + x: approx([4, 4, 6, 6, 6, 6, 4, 4]), + y: approx([1, 1, 1, 1, 2, 2, 2, 2]), + } + assert dimxyt.duration == approx([9, 9, 9, 9, 9, 9, 9, 9]) + assert dimz.midpoints == dimz.lower == dimz.upper == {z: approx([1, 2])} + assert inst.frames().gap == ints("1111111111111111") def test_product_snaking_lines() -> None: @@ -324,7 +317,7 @@ def test_rect_region_difference() -> None: x: approx([0, 1, 0, 0]), y: approx([1, 1, 1.5, 2]), } - assert dimt.duration == approx([0.1, 0.1, 0.1, 0.1]) + assert dimt.duration == approx([0.1]) assert dim.gap == ints("1011") @@ -549,3 +542,20 @@ def test_multiple_statics_with_grid(): ) def test_shape(spec: Spec[Any], expected_shape: tuple[int, ...]): assert expected_shape == spec.shape() + + +def test_constant_duration(): + spec1 = fly(Line("x", 0, 1, 2), 1) + spec2 = step(Line("y", 0, 1, 2), 2) + + with pytest.raises(ValueError): + ConstantDuration(spec1, 2) + + with pytest.raises(ValueError): + spec1.zip(spec2) + + with pytest.raises(ValueError): + spec1 = fly(spec1, 2) + + with pytest.raises(ValueError): + spec1.concat(fly(spec2, 1)) From 84323dd5496938d717fbc6eccb0a10bef7e2e180 Mon Sep 17 00:00:00 2001 From: Luis Segalla Date: Thu, 24 Jul 2025 17:22:05 +0100 Subject: [PATCH 12/38] Updated Specs Made Product.calculate only accept inner Spec having a .duration Changed ConstantDuration so that it can receive an empty spec as argument. If that's the case .calculate we'll return a list with a single dimension that contains only the duration field. Updated calculate in ConstantDuration to add duration into the innermost dimension when fly == False --- src/scanspec/specs.py | 54 +++++++++++++++++++++++++------------------ tests/test_specs.py | 18 +++++++-------- 2 files changed, 40 insertions(+), 32 deletions(-) diff --git a/src/scanspec/specs.py b/src/scanspec/specs.py index 6576eff7..240959f0 100644 --- a/src/scanspec/specs.py +++ b/src/scanspec/specs.py @@ -165,9 +165,9 @@ def axes(self) -> list[Axis]: # noqa: D102 def duration(self) -> float | None | Literal["VARIABLE_DURATION"]: # noqa: D102 outer, inner = self.outer.duration(), self.inner.duration() - if outer is not None and inner is not None: - raise ValueError("Both outer and inner define a duration") - return inner if inner is not None else outer + if outer is not None: + raise ValueError("Outer axes defined a duration") + return inner def calculate( # noqa: D102 self, bounds: bool = True, nested: bool = False @@ -607,42 +607,50 @@ def bounded( class ConstantDuration(Spec[Axis]): """A special spec used to hold information about the duration of each frame.""" - spec: Spec[Axis] = Field(description="Spec contaning the path to be followed") constant_duration: float = Field(description="The value at each point") + spec: Spec[Axis] | None = Field( + description="Spec contaning the path to be followed" + ) fly: bool = Field( description="Define if the Spec is a fly or step scan", default=False ) def axes(self) -> list[Axis]: # noqa: D102 - return self.spec.axes() + if self.spec: + return self.spec.axes() + else: + return [] def duration(self) -> float | None | Literal["VARIABLE_DURATION"]: # noqa: D102 - if self.spec.duration() is not None: + if self.spec and self.spec.duration() is not None: raise ValueError(f"{self.spec} already defines a duration") return self.constant_duration def calculate( # noqa: D102 self, bounds: bool = True, nested: bool = False ) -> list[Dimension[Axis]]: - dimensions = self.spec.calculate() - for d in dimensions: - if d.duration is not None and d.duration != self.duration: - raise ValueError(self) - if self.fly: + if self.spec: + dimensions = ( + self.spec.calculate() if self.fly else self.spec.calculate(bounds=False) + ) + for d in dimensions: + if d.duration is not None and d.duration != self.duration: + raise ValueError(self) dimensions[-1].duration = np.full( - len(dimensions[-1].midpoints[self.axes()[-1]]), self.constant_duration + len(dimensions[-1].gap), + self.constant_duration, ) + return dimensions else: - step_duration: Dimension[Axis] = Dimension( - midpoints={}, - gap=None, - duration=np.full( - 1, - self.constant_duration, - ), + # Had to do it like this otherwise it will complain about typing + empty_dim: Dimension[Axis] = Dimension( + {}, + {}, + {}, + None, + duration=np.full(1, self.constant_duration), ) - dimensions = dimensions + [step_duration] - return dimensions + return [empty_dim] @dataclass(config=StrictConfig) @@ -797,7 +805,7 @@ def fly(spec: Spec[Axis], duration: float) -> Spec[Axis]: spec = fly(Line("x", 1, 2, 3), 0.1) """ - return ConstantDuration(spec=spec, constant_duration=duration, fly=True) + return ConstantDuration(constant_duration=duration, spec=spec, fly=True) def step(spec: Spec[Axis], duration: float, num: int = 1) -> Spec[Axis]: @@ -816,7 +824,7 @@ def step(spec: Spec[Axis], duration: float, num: int = 1) -> Spec[Axis]: spec = step(Line("x", 1, 2, 3), 0.1) """ - return ConstantDuration(spec, duration, fly=False) + return ConstantDuration(constant_duration=duration, spec=spec, fly=False) def get_constant_duration(frames: list[Dimension[Any]]) -> float | None: diff --git a/tests/test_specs.py b/tests/test_specs.py index 4016cae4..b37f880c 100644 --- a/tests/test_specs.py +++ b/tests/test_specs.py @@ -51,10 +51,10 @@ def test_two_point_line() -> None: def test_two_point_stepped_line() -> None: inst = step(Line(x, 0, 1, 2), 0.1) - (dim, dimt) = inst.calculate() + (dim,) = inst.calculate() assert dim.midpoints == {x: approx([0, 1])} - assert dim.gap == ints("10") - assert dimt.duration == approx([0.1]) + assert dim.gap == ints("11") + assert dim.duration == approx([0.1, 0.1]) def test_two_point_fly_line() -> None: @@ -185,7 +185,7 @@ def test_squashed_multiplied_snake_scan() -> None: inst: Spec[str] = Line(z, 1, 2, 2) * Squash( Line(y, 1, 2, 2) * ~Line.bounded(x, 3, 7, 2) - * ConstantDuration(Repeat(2), 9, fly=False) # type: ignore + * ConstantDuration(constant_duration=9, spec=Repeat(2), fly=False) # type: ignore ) assert inst.axes() == [z, y, x] (dimz, dimxyt) = inst.calculate() @@ -308,17 +308,17 @@ def test_rect_region_difference() -> None: ) - Rectangle(x, y, 0.5, 1.5, 2, 2.5) inst = ConstantDuration( - spec, 0.1, + spec, ) assert inst.axes() == [y, x] - (dim, dimt) = inst.calculate() + (dim,) = inst.calculate() assert dim.midpoints == { x: approx([0, 1, 0, 0]), y: approx([1, 1, 1.5, 2]), } - assert dimt.duration == approx([0.1]) - assert dim.gap == ints("1011") + assert dim.duration == approx([0.1, 0.1, 0.1, 0.1]) + assert dim.gap == ints("1111") def test_rect_region_symmetricdifference() -> None: @@ -549,7 +549,7 @@ def test_constant_duration(): spec2 = step(Line("y", 0, 1, 2), 2) with pytest.raises(ValueError): - ConstantDuration(spec1, 2) + ConstantDuration(2, spec1) with pytest.raises(ValueError): spec1.zip(spec2) From 9ee85ea7b63436fef606f329864c25f14585306c Mon Sep 17 00:00:00 2001 From: Luis Segalla Date: Thu, 24 Jul 2025 17:24:17 +0100 Subject: [PATCH 13/38] Updated schema.json --- schema.json | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/schema.json b/schema.json index e897e03b..cfe9b4ab 100644 --- a/schema.json +++ b/schema.json @@ -547,15 +547,22 @@ }, "ConstantDuration_str_-Input": { "properties": { - "spec": { - "$ref": "#/components/schemas/Spec-Input", - "description": "Spec contaning the path to be followed" - }, "constant_duration": { "type": "number", "title": "Constant Duration", "description": "The value at each point" }, + "spec": { + "anyOf": [ + { + "$ref": "#/components/schemas/Spec-Input" + }, + { + "type": "null" + } + ], + "description": "Spec contaning the path to be followed" + }, "fly": { "type": "boolean", "title": "Fly", @@ -572,23 +579,30 @@ "additionalProperties": false, "type": "object", "required": [ - "spec", - "constant_duration" + "constant_duration", + "spec" ], "title": "ConstantDuration", "description": "A special spec used to hold information about the duration of each frame." }, "ConstantDuration_str_-Output": { "properties": { - "spec": { - "$ref": "#/components/schemas/Spec-Output", - "description": "Spec contaning the path to be followed" - }, "constant_duration": { "type": "number", "title": "Constant Duration", "description": "The value at each point" }, + "spec": { + "anyOf": [ + { + "$ref": "#/components/schemas/Spec-Output" + }, + { + "type": "null" + } + ], + "description": "Spec contaning the path to be followed" + }, "fly": { "type": "boolean", "title": "Fly", @@ -605,8 +619,8 @@ "additionalProperties": false, "type": "object", "required": [ - "spec", - "constant_duration" + "constant_duration", + "spec" ], "title": "ConstantDuration", "description": "A special spec used to hold information about the duration of each frame." From ffb8eb737f29ef2d852d8c6c17dfc9232dddfca6 Mon Sep 17 00:00:00 2001 From: Luis Segalla Date: Fri, 25 Jul 2025 14:48:32 +0100 Subject: [PATCH 14/38] PR changes Added more tests to cover all the new cases addded. Made new `Fly` spec and changed ConstantDuration accordingly. --- schema.json | 68 +++++++++++++++++++++++++++--------- src/scanspec/core.py | 2 ++ src/scanspec/specs.py | 43 ++++++++++++++++------- tests/test_specs.py | 81 +++++++++++++++++++++++++++++++++++++------ 4 files changed, 155 insertions(+), 39 deletions(-) diff --git a/schema.json b/schema.json index cfe9b4ab..b9a529de 100644 --- a/schema.json +++ b/schema.json @@ -563,12 +563,6 @@ ], "description": "Spec contaning the path to be followed" }, - "fly": { - "type": "boolean", - "title": "Fly", - "description": "Define if the Spec is a fly or step scan", - "default": false - }, "type": { "type": "string", "const": "ConstantDuration", @@ -579,8 +573,7 @@ "additionalProperties": false, "type": "object", "required": [ - "constant_duration", - "spec" + "constant_duration" ], "title": "ConstantDuration", "description": "A special spec used to hold information about the duration of each frame." @@ -603,12 +596,6 @@ ], "description": "Spec contaning the path to be followed" }, - "fly": { - "type": "boolean", - "title": "Fly", - "description": "Define if the Spec is a fly or step scan", - "default": false - }, "type": { "type": "string", "const": "ConstantDuration", @@ -619,8 +606,7 @@ "additionalProperties": false, "type": "object", "required": [ - "constant_duration", - "spec" + "constant_duration" ], "title": "ConstantDuration", "description": "A special spec used to hold information about the duration of each frame." @@ -737,6 +723,48 @@ "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)" }, + "Fly_str_-Input": { + "properties": { + "spec": { + "$ref": "#/components/schemas/Spec-Input", + "description": "Spec contaning the path to be followed" + }, + "type": { + "type": "string", + "const": "Fly", + "title": "Type", + "default": "Fly" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "spec" + ], + "title": "Fly", + "description": "Spec that represents a fly scan." + }, + "Fly_str_-Output": { + "properties": { + "spec": { + "$ref": "#/components/schemas/Spec-Output", + "description": "Spec contaning the path to be followed" + }, + "type": { + "type": "string", + "const": "Fly", + "title": "Type", + "default": "Fly" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "spec" + ], + "title": "Fly", + "description": "Spec that represents a fly scan." + }, "GapResponse": { "properties": { "gap": { @@ -1423,6 +1451,9 @@ { "$ref": "#/components/schemas/Line_str_" }, + { + "$ref": "#/components/schemas/Fly_str_-Input" + }, { "$ref": "#/components/schemas/ConstantDuration_str_-Input" }, @@ -1438,6 +1469,7 @@ "mapping": { "Concat": "#/components/schemas/Concat_str_-Input", "ConstantDuration": "#/components/schemas/ConstantDuration_str_-Input", + "Fly": "#/components/schemas/Fly_str_-Input", "Line": "#/components/schemas/Line_str_", "Mask": "#/components/schemas/Mask_str_-Input", "Product": "#/components/schemas/Product_str_-Input", @@ -1476,6 +1508,9 @@ { "$ref": "#/components/schemas/Line_str_" }, + { + "$ref": "#/components/schemas/Fly_str_-Output" + }, { "$ref": "#/components/schemas/ConstantDuration_str_-Output" }, @@ -1491,6 +1526,7 @@ "mapping": { "Concat": "#/components/schemas/Concat_str_-Output", "ConstantDuration": "#/components/schemas/ConstantDuration_str_-Output", + "Fly": "#/components/schemas/Fly_str_-Output", "Line": "#/components/schemas/Line_str_", "Mask": "#/components/schemas/Mask_str_-Output", "Product": "#/components/schemas/Product_str_-Output", diff --git a/src/scanspec/core.py b/src/scanspec/core.py index 02aaf0e6..0793ea86 100644 --- a/src/scanspec/core.py +++ b/src/scanspec/core.py @@ -494,6 +494,8 @@ def concat_duration( raise ValueError( "Can't concatenate dimensions unless all or none provide durations" ) + # Need a `type ignore`` here otherwise typing assumes durations can be None, + # which is not possible as it would raise an error on the line above return np.concatenate(durations) # type: ignore return _merge_frames( diff --git a/src/scanspec/specs.py b/src/scanspec/specs.py index 240959f0..73da2c0c 100644 --- a/src/scanspec/specs.py +++ b/src/scanspec/specs.py @@ -44,6 +44,7 @@ "Line", "Static", "Spiral", + "Fly", "fly", "step", ] @@ -86,7 +87,7 @@ def duration(self) -> float | None | Literal["VARIABLE_DURATION"]: raise NotImplementedError(self) def calculate( - self, bounds: bool = True, nested: bool = False + self, bounds: bool = False, nested: bool = False ) -> list[Dimension[Axis]]: # noqa: D102 """Produce a stack of nested `Dimension` that form the scan. @@ -603,16 +604,31 @@ def bounded( Line.bounded = validate_call(Line.bounded) # type:ignore +@dataclass(config=StrictConfig) +class Fly(Spec[Axis]): + """Spec that represents a fly scan.""" + + spec: Spec[Axis] = Field(description="Spec contaning the path to be followed") + + def axes(self) -> list[Axis]: # noqa: D102 + return self.spec.axes() + + def duration(self): # noqa: D102 + return self.spec.duration() + + def calculate( # noqa: D102 + self, bounds: bool = True, nested: bool = False + ) -> list[Dimension[Axis]]: + return self.spec.calculate(bounds=True, nested=nested) + + @dataclass(config=StrictConfig) class ConstantDuration(Spec[Axis]): """A special spec used to hold information about the duration of each frame.""" constant_duration: float = Field(description="The value at each point") spec: Spec[Axis] | None = Field( - description="Spec contaning the path to be followed" - ) - fly: bool = Field( - description="Define if the Spec is a fly or step scan", default=False + description="Spec contaning the path to be followed", default=None ) def axes(self) -> list[Axis]: # noqa: D102 @@ -627,15 +643,16 @@ def duration(self) -> float | None | Literal["VARIABLE_DURATION"]: # noqa: D102 return self.constant_duration def calculate( # noqa: D102 - self, bounds: bool = True, nested: bool = False + self, bounds: bool = False, nested: bool = False ) -> list[Dimension[Axis]]: if self.spec: - dimensions = ( - self.spec.calculate() if self.fly else self.spec.calculate(bounds=False) - ) + dimensions = self.spec.calculate(bounds=bounds) for d in dimensions: - if d.duration is not None and d.duration != self.duration: - raise ValueError(self) + if d.duration is not None: + raise ValueError( + f"Cannot add ConstantDuration to a spec that already\ + has a duration: {self.spec}" + ) dimensions[-1].duration = np.full( len(dimensions[-1].gap), self.constant_duration, @@ -805,7 +822,7 @@ def fly(spec: Spec[Axis], duration: float) -> Spec[Axis]: spec = fly(Line("x", 1, 2, 3), 0.1) """ - return ConstantDuration(constant_duration=duration, spec=spec, fly=True) + return ConstantDuration(constant_duration=duration, spec=spec) def step(spec: Spec[Axis], duration: float, num: int = 1) -> Spec[Axis]: @@ -824,7 +841,7 @@ def step(spec: Spec[Axis], duration: float, num: int = 1) -> Spec[Axis]: spec = step(Line("x", 1, 2, 3), 0.1) """ - return ConstantDuration(constant_duration=duration, spec=spec, fly=False) + return ConstantDuration(constant_duration=duration, spec=spec) def get_constant_duration(frames: list[Dimension[Any]]) -> float | None: diff --git a/tests/test_specs.py b/tests/test_specs.py index b37f880c..cbb1c54c 100644 --- a/tests/test_specs.py +++ b/tests/test_specs.py @@ -2,11 +2,13 @@ import pytest -from scanspec.core import Path, SnakedDimension +from scanspec.core import Dimension, Path, SnakedDimension from scanspec.regions import Circle, Ellipse, Polygon, Rectangle from scanspec.specs import ( + VARIABLE_DURATION, Concat, ConstantDuration, + Fly, Line, Mask, Repeat, @@ -15,8 +17,6 @@ Squash, Static, Zip, - fly, - # get_constant_duration, step, ) @@ -52,13 +52,13 @@ def test_two_point_line() -> None: def test_two_point_stepped_line() -> None: inst = step(Line(x, 0, 1, 2), 0.1) (dim,) = inst.calculate() - assert dim.midpoints == {x: approx([0, 1])} + assert dim.midpoints == dim.lower == dim.upper == {x: approx([0, 1])} assert dim.gap == ints("11") assert dim.duration == approx([0.1, 0.1]) def test_two_point_fly_line() -> None: - inst = fly(Line(x, 0, 1, 2), 0.1) + inst = Fly(ConstantDuration(constant_duration=0.1, spec=Line(x, 0, 1, 2))) (dim,) = inst.calculate() assert dim.midpoints == { x: approx([0, 1]), @@ -82,6 +82,50 @@ def test_many_point_line() -> None: assert dim.gap == ints("10000") +def test_empty_dimension() -> None: + with pytest.raises(ValueError): + Dimension(midpoints={}, upper={}, lower={}, gap=None, duration=None) + + +def test_concat() -> None: + (dim1,) = Fly( + spec=ConstantDuration(constant_duration=1, spec=Line("x", 0, 1, 2)) + ).calculate() + (dim2,) = Line("x", 3, 4, 2).calculate() + + # spec2 has no duration which will raise an error in concat_duration + with pytest.raises(ValueError): + dim1.concat(dim2) + + (dim2,) = Fly( + spec=ConstantDuration(constant_duration=1, spec=Line("x", 3, 4, 2)) + ).calculate() + + dim1.concat(dim2) + + assert dim1.duration == approx([1, 1]) + + +def test_zip() -> None: + (dim1,) = Fly( + spec=ConstantDuration(constant_duration=1, spec=Line("x", 0, 1, 2)) + ).calculate() + + (dim2,) = Fly( + spec=ConstantDuration(constant_duration=2, spec=Line("y", 3, 4, 2)) + ).calculate() + + with pytest.raises(ValueError): + dim1.zip(dim2) + + +def test_one_point_duration() -> None: + duration = ConstantDuration(1.0) # type: ignore + (dim,) = duration.calculate() # type: ignore + assert dim.duration == approx([1.0]) + assert duration.axes() == [] + + def test_one_point_bounded_line() -> None: inst = Line.bounded(x, 0, 1, 1) assert inst == Line(x, 0.5, 1.5, 1) @@ -185,7 +229,7 @@ def test_squashed_multiplied_snake_scan() -> None: inst: Spec[str] = Line(z, 1, 2, 2) * Squash( Line(y, 1, 2, 2) * ~Line.bounded(x, 3, 7, 2) - * ConstantDuration(constant_duration=9, spec=Repeat(2), fly=False) # type: ignore + * ConstantDuration(constant_duration=9, spec=Repeat(2, gap=False)) # type: ignore ) assert inst.axes() == [z, y, x] (dimz, dimxyt) = inst.calculate() @@ -196,7 +240,7 @@ def test_squashed_multiplied_snake_scan() -> None: } assert dimxyt.duration == approx([9, 9, 9, 9, 9, 9, 9, 9]) assert dimz.midpoints == dimz.lower == dimz.upper == {z: approx([1, 2])} - assert inst.frames().gap == ints("1111111111111111") + assert inst.frames().gap == ints("1010101010101010") def test_product_snaking_lines() -> None: @@ -220,6 +264,13 @@ def test_product_snaking_lines() -> None: assert dim.gap == ints("101010") +def test_product_duration() -> None: + with pytest.raises(ValueError): + Fly(ConstantDuration(1, Line(y, 1, 2, 3))) * Fly( + ConstantDuration(1, ~Line(x, 0, 1, 2)) + ) # type: ignore + + def test_concat_lines() -> None: inst = Concat(Line(x, 0, 1, 2), Line(x, 1, 2, 3)) assert inst.axes() == [x] @@ -229,6 +280,16 @@ def test_concat_lines() -> None: assert dim.upper == {x: approx([0.5, 1.5, 1.25, 1.75, 2.25])} assert dim.gap == ints("10100") + # Test concating one Spec with duration and another one without + with pytest.raises(ValueError): + Concat(ConstantDuration(1, Line(x, 0, 1, 2)), Line(x, 1, 2, 3)) + + # Variable duration concat + spec = Concat( + ConstantDuration(1, Line(x, 0, 1, 2)), ConstantDuration(2, Line(x, 1, 2, 3)) + ) + assert spec.duration() == VARIABLE_DURATION + def test_rect_region() -> None: inst = Line(y, 1, 3, 5) * Line(x, 0, 2, 3) & Rectangle(x, y, 0, 1, 1.5, 2.2) @@ -545,7 +606,7 @@ def test_shape(spec: Spec[Any], expected_shape: tuple[int, ...]): def test_constant_duration(): - spec1 = fly(Line("x", 0, 1, 2), 1) + spec1 = Fly(ConstantDuration(spec=Line("x", 0, 1, 2), constant_duration=1)) spec2 = step(Line("y", 0, 1, 2), 2) with pytest.raises(ValueError): @@ -555,7 +616,7 @@ def test_constant_duration(): spec1.zip(spec2) with pytest.raises(ValueError): - spec1 = fly(spec1, 2) + spec1 = ConstantDuration(1, spec1) with pytest.raises(ValueError): - spec1.concat(fly(spec2, 1)) + spec1.concat(Fly(ConstantDuration(1, spec2))) From b928267924eea5a4225f86eb04092b10e5bcc975 Mon Sep 17 00:00:00 2001 From: Luis Segalla Date: Fri, 25 Jul 2025 15:00:47 +0100 Subject: [PATCH 15/38] Updated documentation --- docs/how-to/iterate-a-spec.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/how-to/iterate-a-spec.rst b/docs/how-to/iterate-a-spec.rst index 3cfca77c..5b866771 100644 --- a/docs/how-to/iterate-a-spec.rst +++ b/docs/how-to/iterate-a-spec.rst @@ -89,8 +89,9 @@ You may need to know where there is a gap between points, so that you can do something in the turnaround. For example, if we take the x axis of a grid scan, you can see it snakes back and forth: ->>> from scanspec.specs import Line, fly ->>> grid = fly(Line("y", 0, 1, 2) * ~Line("x", 1, 2, 3), 0.1) +>>> from scanspec.specs import Line, Fly, ConstantDuration +>>> spec = Line("y", 0, 1, 2) * ~Line("x", 1, 2, 3) +>>> grid = Fly(ConstantDuration(0.1,spec)) >>> chunk = grid.frames() >>> chunk.midpoints["x"] array([1. , 1.5, 2. , 2. , 1.5, 1. ]) From 51635a6b9e8928c3298ed7b1ffb05e59a1cbabfe Mon Sep 17 00:00:00 2001 From: Luis Segalla Date: Fri, 25 Jul 2025 15:01:04 +0100 Subject: [PATCH 16/38] Enforced version of Pillow to fix broken cli tests --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 85f95c4c..85ddcf9c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ classifiers = [ "Programming Language :: Python :: 3.12", ] description = "Specify step and flyscan paths in a serializable, efficient and Pythonic way" -dependencies = ["numpy", "click>=8.1", "pydantic>=2.0"] +dependencies = ["numpy", "click>=8.1", "pydantic>=2.0", "Pillow==11.0.0"] dynamic = ["version"] license.file = "LICENSE" readme = "README.md" From c04a51771e289ab87361169595637c98e170dd2a Mon Sep 17 00:00:00 2001 From: Luis Segalla Date: Fri, 25 Jul 2025 15:38:54 +0100 Subject: [PATCH 17/38] Removing unreacheable part of the code --- src/scanspec/specs.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/scanspec/specs.py b/src/scanspec/specs.py index 73da2c0c..7530a979 100644 --- a/src/scanspec/specs.py +++ b/src/scanspec/specs.py @@ -647,12 +647,6 @@ def calculate( # noqa: D102 ) -> list[Dimension[Axis]]: if self.spec: dimensions = self.spec.calculate(bounds=bounds) - for d in dimensions: - if d.duration is not None: - raise ValueError( - f"Cannot add ConstantDuration to a spec that already\ - has a duration: {self.spec}" - ) dimensions[-1].duration = np.full( len(dimensions[-1].gap), self.constant_duration, From 5c37b6f861adf611de51bd1a57868c7718a8abb0 Mon Sep 17 00:00:00 2001 From: Luis Segalla Date: Mon, 28 Jul 2025 09:06:20 +0100 Subject: [PATCH 18/38] Removing fly function --- src/scanspec/specs.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/scanspec/specs.py b/src/scanspec/specs.py index 7530a979..626795b2 100644 --- a/src/scanspec/specs.py +++ b/src/scanspec/specs.py @@ -45,7 +45,6 @@ "Static", "Spiral", "Fly", - "fly", "step", ] @@ -802,23 +801,6 @@ def spaced( Spiral.spaced = validate_call(Spiral.spaced) # type:ignore -def fly(spec: Spec[Axis], duration: float) -> Spec[Axis]: - """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 ConstantDuration(constant_duration=duration, spec=spec) - - def step(spec: Spec[Axis], duration: float, num: int = 1) -> Spec[Axis]: """Step scan, with num frames of given duration at each frame in the spec. From b97a9469fc01115bd52422574f7f0aa7fc24fa64 Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Mon, 28 Jul 2025 09:25:37 +0000 Subject: [PATCH 19/38] Remove type ignores --- src/scanspec/service.py | 4 ++-- src/scanspec/specs.py | 2 +- tests/test_specs.py | 36 ++++++++++++++++++------------------ 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/scanspec/service.py b/src/scanspec/service.py index 945d7ee6..8f6115dc 100644 --- a/src/scanspec/service.py +++ b/src/scanspec/service.py @@ -223,7 +223,7 @@ def gap( dims = spec.calculate() # Grab dimensions from spec path = Path(dims) # Convert to a path gap = list(path.consume().gap) - return GapResponse(gap) # type: ignore + return GapResponse(gap) @app.post("/smalleststep", response_model=SmallestStepResponse) @@ -296,7 +296,7 @@ def _format_axes_points( """ if format is PointsFormat.FLOAT_LIST: - return {axis: list(points) for axis, points in axes_points.items()} # type: ignore + return {axis: list(points) for axis, points in axes_points.items()} elif format is PointsFormat.STRING: return {axis: str(points) for axis, points in axes_points.items()} elif format is PointsFormat.BASE64_ENCODED: diff --git a/src/scanspec/specs.py b/src/scanspec/specs.py index 626795b2..8d9824ea 100644 --- a/src/scanspec/specs.py +++ b/src/scanspec/specs.py @@ -283,7 +283,7 @@ def calculate( # noqa: D102 padded_right: list[Dimension[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 + padded_right += frames_right # Work through, zipping them together one by one frames: list[Dimension[Axis]] = [] diff --git a/tests/test_specs.py b/tests/test_specs.py index cbb1c54c..fca8a4c3 100644 --- a/tests/test_specs.py +++ b/tests/test_specs.py @@ -120,8 +120,8 @@ def test_zip() -> None: def test_one_point_duration() -> None: - duration = ConstantDuration(1.0) # type: ignore - (dim,) = duration.calculate() # type: ignore + duration = ConstantDuration[Any](1.0) + (dim,) = duration.calculate() assert dim.duration == approx([1.0]) assert duration.axes() == [] @@ -226,10 +226,10 @@ def test_squashed_product() -> None: def test_squashed_multiplied_snake_scan() -> None: - inst: Spec[str] = Line(z, 1, 2, 2) * Squash( + inst = Line(z, 1, 2, 2) * Squash( Line(y, 1, 2, 2) * ~Line.bounded(x, 3, 7, 2) - * ConstantDuration(constant_duration=9, spec=Repeat(2, gap=False)) # type: ignore + * ConstantDuration(9, Repeat[str](2, gap=False)) # until #177 ) assert inst.axes() == [z, y, x] (dimz, dimxyt) = inst.calculate() @@ -266,9 +266,9 @@ def test_product_snaking_lines() -> None: def test_product_duration() -> None: with pytest.raises(ValueError): - Fly(ConstantDuration(1, Line(y, 1, 2, 3))) * Fly( + _ = Fly(ConstantDuration(1, Line(y, 1, 2, 3))) * Fly( ConstantDuration(1, ~Line(x, 0, 1, 2)) - ) # type: ignore + ) def test_concat_lines() -> None: @@ -524,23 +524,23 @@ def test_beam_selector() -> None: def test_gap_repeat() -> None: # Check that no gap propogates to dim.gap for snaked axis - spec: Spec[str] = Repeat(10, gap=False) * ~Line.bounded(x, 11, 19, 1) # type: ignore - dim = spec.frames() # type: ignore - assert len(dim) == 10 # type: ignore - assert dim.lower == {x: approx([11, 19, 11, 19, 11, 19, 11, 19, 11, 19])} # type: ignore - assert dim.upper == {x: approx([19, 11, 19, 11, 19, 11, 19, 11, 19, 11])} # type: ignore - assert dim.midpoints == {x: approx([15, 15, 15, 15, 15, 15, 15, 15, 15, 15])} # type: ignore + spec = Repeat[str](10, gap=False) * ~Line.bounded(x, 11, 19, 1) + dim = spec.frames() + assert len(dim) == 10 + assert dim.lower == {x: approx([11, 19, 11, 19, 11, 19, 11, 19, 11, 19])} + assert dim.upper == {x: approx([19, 11, 19, 11, 19, 11, 19, 11, 19, 11])} + assert dim.midpoints == {x: approx([15, 15, 15, 15, 15, 15, 15, 15, 15, 15])} assert dim.gap == ints("0000000000") def test_gap_repeat_non_snake() -> None: # Check that no gap doesn't propogate to dim.gap for non-snaked axis - spec: Spec[str] = Repeat(3, gap=False) * Line.bounded(x, 11, 19, 1) # type: ignore - dim = spec.frames() # type: ignore - assert len(dim) == 3 # type: ignore - assert dim.lower == {x: approx([11, 11, 11])} # type: ignore - assert dim.upper == {x: approx([19, 19, 19])} # type: ignore - assert dim.midpoints == {x: approx([15, 15, 15])} # type: ignore + spec = Repeat[str](3, gap=False) * Line.bounded(x, 11, 19, 1) + dim = spec.frames() + assert len(dim) == 3 + assert dim.lower == {x: approx([11, 11, 11])} + assert dim.upper == {x: approx([19, 19, 19])} + assert dim.midpoints == {x: approx([15, 15, 15])} assert dim.gap == ints("111") From 466bd526ee07b004879ca420e7340956a8f35e8f Mon Sep 17 00:00:00 2001 From: Luis Segalla Date: Mon, 28 Jul 2025 11:51:20 +0100 Subject: [PATCH 20/38] Moved Pillow dependency to dev --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 85ddcf9c..194d0449 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ classifiers = [ "Programming Language :: Python :: 3.12", ] description = "Specify step and flyscan paths in a serializable, efficient and Pythonic way" -dependencies = ["numpy", "click>=8.1", "pydantic>=2.0", "Pillow==11.0.0"] +dependencies = ["numpy", "click>=8.1", "pydantic>=2.0"] dynamic = ["version"] license.file = "LICENSE" readme = "README.md" @@ -46,6 +46,7 @@ dev = [ "sphinxcontrib-openapi", "tox-direct", "types-mock", + "Pillow==11.0.0", ] [project.scripts] From 5e09873b71570365dc8926196ba520c3764f34df Mon Sep 17 00:00:00 2001 From: Luis Segalla Date: Mon, 28 Jul 2025 11:51:40 +0100 Subject: [PATCH 21/38] Updated check statements on Dimension class --- src/scanspec/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scanspec/core.py b/src/scanspec/core.py index 0793ea86..e6a9caa4 100644 --- a/src/scanspec/core.py +++ b/src/scanspec/core.py @@ -367,7 +367,7 @@ def __init__( self.gap = gap # If midpoints are provided and we don't have a gap array # calculate it based on the midpoints - elif gap is None and len(self.midpoints) > 0: + elif self.midpoints: # Need to calculate gap as not passed one # We have a gap if upper[i] != lower[i+1] for any axes axes_gap = [ @@ -379,7 +379,7 @@ def __init__( self.gap = np.logical_or.reduce(axes_gap) # If only duratiotn is provided we need to make gap have the same shape # as the duration provided for the __len__ method - elif gap is None and self.duration is not None and len(self.midpoints) == 0: + elif self.duration is not None: self.gap = np.full(len(self.duration), False) else: raise ValueError("self.gap is undefined") From 8127d55d769726f5e5120926c037eb7ef8d840a2 Mon Sep 17 00:00:00 2001 From: Luis Segalla Date: Mon, 28 Jul 2025 13:33:14 +0100 Subject: [PATCH 22/38] Made bounds default to false and changed Duration return Default value of the bounds argument is now `False`. Needed to change the `frames` method in the `Spec` class so that it would call calculate with the correct bounds, otherwise it wouls break some tests. Made it so that the `duration` method in the base class `Spec` returned None by default and removed the implementations from classes that would always return None. --- src/scanspec/specs.py | 40 ++++++++++++++-------------------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/src/scanspec/specs.py b/src/scanspec/specs.py index 8d9824ea..8d45a262 100644 --- a/src/scanspec/specs.py +++ b/src/scanspec/specs.py @@ -83,7 +83,7 @@ def duration(self) -> float | None | Literal["VARIABLE_DURATION"]: - ``float``: A constant duration for each point - `VARIABLE_DURATION`: A different duration for each point """ - raise NotImplementedError(self) + return None def calculate( self, bounds: bool = False, nested: bool = False @@ -94,9 +94,9 @@ def calculate( """ raise NotImplementedError(self) - def frames(self) -> Dimension[Axis]: + def frames(self, bounds: bool = False) -> Dimension[Axis]: """Expand all the scan `Dimension` and return them.""" - return stack2dimension(self.calculate()) + return stack2dimension(self.calculate(bounds=bounds)) def midpoints(self) -> Midpoints[Axis]: """Return `Midpoints` that can be iterated point by point.""" @@ -170,7 +170,7 @@ def duration(self) -> float | None | Literal["VARIABLE_DURATION"]: # noqa: D102 return inner def calculate( # noqa: D102 - self, bounds: bool = True, nested: bool = False + self, bounds: bool = False, nested: bool = False ) -> list[Dimension[Axis]]: frames_outer = self.outer.calculate(bounds=False, nested=nested) frames_inner = self.inner.calculate(bounds, nested=True) @@ -210,11 +210,8 @@ class Repeat(Spec[Axis]): def axes(self) -> list[Axis]: # noqa: D102 return [] - def duration(self) -> float | None | Literal["VARIABLE_DURATION"]: # noqa: D102 - return None - def calculate( # noqa: D102 - self, bounds: bool = True, nested: bool = False + self, bounds: bool = False, nested: bool = False ) -> list[Dimension[Axis]]: return [Dimension({}, gap=np.full(self.num, self.gap))] @@ -259,7 +256,7 @@ def duration(self) -> float | None | Literal["VARIABLE_DURATION"]: # noqa: D102 return left if left is not None else right def calculate( # noqa: D102 - self, bounds: bool = True, nested: bool = False + self, bounds: bool = False, nested: bool = False ) -> list[Dimension[Axis]]: frames_left = self.left.calculate(bounds, nested) frames_right = self.right.calculate(bounds, nested) @@ -333,7 +330,7 @@ def duration(self) -> float | None | Literal["VARIABLE_DURATION"]: # noqa: D102 return self.spec.duration() def calculate( # noqa: D102 - self, bounds: bool = True, nested: bool = False + self, bounds: bool = False, nested: bool = False ) -> list[Dimension[Axis]]: frames = self.spec.calculate(bounds, nested) for axis_set in self.region.axis_sets(): @@ -396,7 +393,7 @@ def duration(self) -> float | None | Literal["VARIABLE_DURATION"]: # noqa: D102 return self.spec.duration() def calculate( # noqa: D102 - self, bounds: bool = True, nested: bool = False + self, bounds: bool = False, nested: bool = False ) -> list[Dimension[Axis]]: return [ SnakedDimension.from_frames(segment) @@ -453,7 +450,7 @@ def duration(self) -> float | None | Literal["VARIABLE_DURATION"]: # noqa: D102 return VARIABLE_DURATION def calculate( # noqa: D102 - self, bounds: bool = True, nested: bool = False + self, bounds: bool = False, nested: bool = False ) -> list[Dimension[Axis]]: dim_left = squash_frames( self.left.calculate(bounds, nested), nested and self.check_path_changes @@ -493,7 +490,7 @@ def duration(self) -> float | None | Literal["VARIABLE_DURATION"]: # noqa: D102 return self.spec.duration() def calculate( # noqa: D102 - self, bounds: bool = True, nested: bool = False + self, bounds: bool = False, nested: bool = False ) -> list[Dimension[Axis]]: dims = self.spec.calculate(bounds, nested) dim = squash_frames(dims, nested and self.check_path_changes) @@ -546,9 +543,6 @@ class Line(Spec[Axis]): def axes(self) -> list[Axis]: # noqa: D102 return [self.axis] - def duration(self) -> float | None | Literal["VARIABLE_DURATION"]: # noqa: D102 - return None - def _line_from_indexes( self, indexes: npt.NDArray[np.float64] ) -> dict[Axis, npt.NDArray[np.float64]]: @@ -564,7 +558,7 @@ def _line_from_indexes( return {self.axis: indexes * step + first} def calculate( # noqa: D102 - self, bounds: bool = True, nested: bool = False + self, bounds: bool = False, nested: bool = False ) -> list[Dimension[Axis]]: return _dimensions_from_indexes( self._line_from_indexes, self.axes(), self.num, bounds @@ -616,7 +610,7 @@ def duration(self): # noqa: D102 return self.spec.duration() def calculate( # noqa: D102 - self, bounds: bool = True, nested: bool = False + self, bounds: bool = False, nested: bool = False ) -> list[Dimension[Axis]]: return self.spec.calculate(bounds=True, nested=nested) @@ -683,16 +677,13 @@ class Static(Spec[Axis]): def axes(self) -> list[Axis]: # noqa: D102 return [self.axis] - def duration(self) -> float | None | Literal["VARIABLE_DURATION"]: # noqa: D102 - return None - 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)} def calculate( # noqa: D102 - self, bounds: bool = True, nested: bool = False + self, bounds: bool = False, nested: bool = False ) -> list[Dimension[Axis]]: return _dimensions_from_indexes( self._repeats_from_indexes, self.axes(), self.num, bounds @@ -730,9 +721,6 @@ def axes(self) -> list[Axis]: # noqa: D102 # TODO: reversed from __init__ args, a good idea? return [self.y_axis, self.x_axis] - def duration(self) -> float | None | Literal["VARIABLE_DURATION"]: # noqa: D102 - return None - def _spiral_from_indexes( self, indexes: npt.NDArray[np.float64] ) -> dict[Axis, npt.NDArray[np.float64]]: @@ -753,7 +741,7 @@ def _spiral_from_indexes( } def calculate( # noqa: D102 - self, bounds: bool = True, nested: bool = False + self, bounds: bool = False, nested: bool = False ) -> list[Dimension[Axis]]: return _dimensions_from_indexes( self._spiral_from_indexes, self.axes(), self.num, bounds From 633b197a42fb7283884820e9a909e7de4cc9783f Mon Sep 17 00:00:00 2001 From: Luis Segalla Date: Mon, 28 Jul 2025 13:33:36 +0100 Subject: [PATCH 23/38] Updated tests based on new `bounds` change --- tests/test_service.py | 15 ++++++------ tests/test_specs.py | 56 +++++++++++++++++++++---------------------- 2 files changed, 36 insertions(+), 35 deletions(-) diff --git a/tests/test_service.py b/tests/test_service.py index bf88fc7f..3f9786de 100644 --- a/tests/test_service.py +++ b/tests/test_service.py @@ -59,18 +59,18 @@ def test_subsampling(client: TestClient) -> None: [ ( PointsFormat.FLOAT_LIST, - [-0.125, 0.125, 0.375, 0.625, 0.875], - [0.125, 0.375, 0.625, 0.875, 1.125], + [0, 0.25, 0.5, 0.75, 1], + [0, 0.25, 0.5, 0.75, 1], ), ( PointsFormat.STRING, - "[-0.125 0.125 0.375 0.625 0.875]", - "[0.125 0.375 0.625 0.875 1.125]", + "[0. 0.25 0.5 0.75 1. ]", + "[0. 0.25 0.5 0.75 1. ]", ), ( PointsFormat.BASE64_ENCODED, - "AAAAAAAAwL8AAAAAAADAPwAAAAAAANg/AAAAAAAA5D8AAAAAAADsPw==", - "AAAAAAAAwD8AAAAAAADYPwAAAAAAAOQ/AAAAAAAA7D8AAAAAAADyPw==", + "AAAAAAAAAAAAAAAAAADQPwAAAAAAAOA/AAAAAAAA6D8AAAAAAADwPw==", + "AAAAAAAAAAAAAAAAAADQPwAAAAAAAOA/AAAAAAAA6D8AAAAAAADwPw==", ), ], ids=["float_list", "string", "base64"], @@ -92,11 +92,12 @@ def test_bounds( # GAP TEST(S) # def test_gap(client: TestClient) -> None: + # If not defined specs will default to a step scan spec = Line("y", 0.0, 10.0, 3) * Line("x", 0.0, 10.0, 3) response = client.post("/gap", json=spec.serialize()) assert response.status_code == 200 assert response.json() == { - "gap": [True, False, False, True, False, False, True, False, False] + "gap": [True, True, True, True, True, True, True, True, True] } diff --git a/tests/test_specs.py b/tests/test_specs.py index fca8a4c3..3ee200c4 100644 --- a/tests/test_specs.py +++ b/tests/test_specs.py @@ -31,7 +31,7 @@ def ints(s: str) -> Any: def test_one_point_line() -> None: inst = Line(x, 0, 1, 1) - (dim,) = inst.calculate() + (dim,) = inst.calculate(bounds=True) assert dim.midpoints == {x: approx([0])} assert dim.lower == {x: approx([-0.5])} assert dim.upper == {x: approx([0.5])} @@ -42,7 +42,7 @@ def test_one_point_line() -> None: def test_two_point_line() -> None: inst = Line(x, 0, 1, 2) - (dim,) = inst.calculate() + (dim,) = inst.calculate(bounds=True) assert dim.midpoints == {x: approx([0, 1])} assert dim.lower == {x: approx([-0.5, 0.5])} assert dim.upper == {x: approx([0.5, 1.5])} @@ -75,7 +75,7 @@ def test_two_point_fly_line() -> None: def test_many_point_line() -> None: inst = Line(x, 0, 1, 5) - (dim,) = inst.calculate() + (dim,) = inst.calculate(bounds=True) assert dim.midpoints == {x: approx([0, 0.25, 0.5, 0.75, 1])} assert dim.lower == {x: approx([-0.125, 0.125, 0.375, 0.625, 0.875])} assert dim.upper == {x: approx([0.125, 0.375, 0.625, 0.875, 1.125])} @@ -138,7 +138,7 @@ def test_many_point_bounded_line() -> None: def test_spiral() -> None: inst = Spiral(x, y, 0, 10, 5, 50, 10) - (dim,) = inst.calculate() + (dim,) = inst.calculate(bounds=True) assert dim.midpoints == { y: approx([5.4, 6.4, 19.7, 23.8, 15.4, 1.7, -8.6, -10.7, -4.1, 8.3], abs=0.1), x: approx([0.3, -0.9, -0.7, 0.5, 1.5, 1.6, 0.7, -0.6, -1.8, -2.4], abs=0.1), @@ -163,7 +163,7 @@ def test_spaced_spiral() -> None: def test_zipped_lines() -> None: inst = Line(x, 0, 1, 5).zip(Line(y, 1, 2, 5)) assert inst.axes() == [x, y] - (dim,) = inst.calculate() + (dim,) = inst.calculate(bounds=True) assert dim.midpoints == { x: approx([0, 0.25, 0.5, 0.75, 1]), y: approx([1, 1.25, 1.5, 1.75, 2]), @@ -174,7 +174,7 @@ def test_zipped_lines() -> None: def test_product_lines() -> None: inst = Line(y, 1, 2, 3) * Line(x, 0, 1, 2) assert inst.axes() == [y, x] - dims = inst.calculate() + dims = inst.calculate(bounds=True) assert len(dims) == 2 dim = Path(dims).consume() assert dim.midpoints == { @@ -195,7 +195,7 @@ def test_product_lines() -> None: def test_zipped_product_lines() -> None: inst = Line(y, 1, 2, 3) * Line(x, 0, 1, 5).zip(Line(z, 2, 3, 5)) assert inst.axes() == [y, x, z] - dimy, dimxz = inst.calculate() + dimy, dimxz = inst.calculate(bounds=True) assert dimxz.midpoints == { x: approx([0, 0.25, 0.5, 0.75, 1]), z: approx([2, 2.25, 2.5, 2.75, 3]), @@ -203,13 +203,13 @@ def test_zipped_product_lines() -> None: assert dimy.midpoints == { y: approx([1, 1.5, 2]), } - assert inst.frames().gap == ints("100001000010000") + assert inst.frames(bounds=True).gap == ints("100001000010000") def test_squashed_product() -> None: inst = Squash(Line(y, 1, 2, 3) * Line(x, 0, 1, 2)) assert inst.axes() == [y, x] - (dim,) = inst.calculate() + (dim,) = inst.calculate(bounds=True) assert dim.midpoints == { x: approx([0, 1, 0, 1, 0, 1]), y: approx([1, 1, 1.5, 1.5, 2, 2]), @@ -240,13 +240,13 @@ def test_squashed_multiplied_snake_scan() -> None: } assert dimxyt.duration == approx([9, 9, 9, 9, 9, 9, 9, 9]) assert dimz.midpoints == dimz.lower == dimz.upper == {z: approx([1, 2])} - assert inst.frames().gap == ints("1010101010101010") + assert inst.frames(bounds=True).gap == ints("1010101010101010") def test_product_snaking_lines() -> None: inst = Line(y, 1, 2, 3) * ~Line(x, 0, 1, 2) assert inst.axes() == [y, x] - dims = inst.calculate() + dims = inst.calculate(bounds=True) assert len(dims) == 2 dim = Path(dims).consume() assert dim.midpoints == { @@ -274,7 +274,7 @@ def test_product_duration() -> None: def test_concat_lines() -> None: inst = Concat(Line(x, 0, 1, 2), Line(x, 1, 2, 3)) assert inst.axes() == [x] - (dim,) = inst.calculate() + (dim,) = inst.calculate(bounds=True) assert dim.midpoints == {x: approx([0, 1, 1, 1.5, 2])} assert dim.lower == {x: approx([-0.5, 0.5, 0.75, 1.25, 1.75])} assert dim.upper == {x: approx([0.5, 1.5, 1.25, 1.75, 2.25])} @@ -294,7 +294,7 @@ def test_concat_lines() -> None: def test_rect_region() -> None: inst = Line(y, 1, 3, 5) * Line(x, 0, 2, 3) & Rectangle(x, y, 0, 1, 1.5, 2.2) assert inst.axes() == [y, x] - (dim,) = inst.calculate() + (dim,) = inst.calculate(bounds=True) assert dim.midpoints == { x: approx([0, 1, 0, 1, 0, 1]), y: approx([1, 1, 1.5, 1.5, 2, 2]), @@ -315,7 +315,7 @@ def test_rect_region_3D() -> None: x, y, 0, 1, 1.5, 2.2 ) assert inst.axes() == [z, y, x] - zdim, xydim = inst.calculate() + zdim, xydim = inst.calculate(bounds=True) assert zdim.midpoints == {z: approx([3.2, 3.2])} assert zdim.midpoints is zdim.upper assert zdim.midpoints is zdim.lower @@ -331,7 +331,7 @@ def test_rect_region_3D() -> None: x: approx([0.5, 1.5, 0.5, 1.5, 0.5, 1.5]), y: approx([1, 1, 1.5, 1.5, 2, 2]), } - assert inst.frames().gap == ints("101010101010") + assert inst.frames(bounds=True).gap == ints("101010101010") def test_rect_region_union() -> None: @@ -339,7 +339,7 @@ def test_rect_region_union() -> None: x, y, 0, 1, 1.5, 2.2 ) | Rectangle(x, y, 0.5, 1.5, 2, 2.5) assert inst.axes() == [y, x] - (dim,) = inst.calculate() + (dim,) = inst.calculate(bounds=True) assert dim.midpoints == { x: approx([0, 1, 0, 1, 2, 0, 1, 2, 1, 2]), y: approx([1, 1, 1.5, 1.5, 1.5, 2, 2, 2, 2.5, 2.5]), @@ -387,7 +387,7 @@ def test_rect_region_symmetricdifference() -> None: x, y, 0, 1, 1.5, 2.2 ) ^ Rectangle(x, y, 0.5, 1.5, 2, 2.5) assert inst.axes() == [y, x] - (dim,) = inst.calculate() + (dim,) = inst.calculate(bounds=True) assert dim.midpoints == { x: approx([0, 1, 0, 2, 0, 2, 1, 2]), y: approx([1, 1, 1.5, 1.5, 2, 2, 2.5, 2.5]), @@ -398,7 +398,7 @@ def test_rect_region_symmetricdifference() -> None: def test_circle_region() -> None: inst = Line(y, 1, 3, 3) * Line(x, 0, 2, 3) & Circle(x, y, 1, 2, 1) assert inst.axes() == [y, x] - (dim,) = inst.calculate() + (dim,) = inst.calculate(bounds=True) assert dim.midpoints == { x: approx([1, 0, 1, 2, 1]), y: approx([1, 2, 2, 2, 3]), @@ -421,7 +421,7 @@ def test_circle_snaked_region() -> None: check_path_changes=False, ) assert inst.axes() == [y, x] - (dim,) = inst.calculate() + (dim,) = inst.calculate(bounds=True) assert dim.midpoints == { x: approx([1, 2, 1, 0, 1]), y: approx([1, 2, 2, 2, 3]), @@ -440,7 +440,7 @@ def test_circle_snaked_region() -> None: def test_ellipse_region() -> None: inst = Line("y", 1, 3, 3) * Line("x", 0, 2, 3) & Ellipse(x, y, 1, 2, 2, 1, 45) assert inst.axes() == [y, x] - (dim,) = inst.calculate() + (dim,) = inst.calculate(bounds=True) assert dim.midpoints == { x: approx([0, 1, 0, 1, 2, 1, 2]), y: approx([1, 1, 2, 2, 2, 3, 3]), @@ -461,7 +461,7 @@ def test_polygon_region() -> None: y_verts = [0, 3.5, 3.5, 0.5] inst = Line("y", 1, 3, 3) * Line("x", 0, 4, 5) & Polygon(x, y, x_verts, y_verts) assert inst.axes() == [y, x] - (dim,) = inst.calculate() + (dim,) = inst.calculate(bounds=True) assert dim.midpoints == { x: approx([1, 2, 1, 2, 3, 1, 2, 3]), y: approx([1, 1, 2, 2, 2, 3, 3, 3]), @@ -481,7 +481,7 @@ def test_xyz_stack() -> None: # Beam selector scan moves bounded between midpoints and lower and upper bounds at # maximum speed. Turnaround sections are where it sends the triggers spec = Line(z, 0, 1, 2) * ~Line(y, 0, 2, 3) * ~Line(x, 0, 3, 4) - dim = spec.frames() + dim = spec.frames(bounds=True) assert len(dim) == 24 assert dim.lower == { z: ints("000000000000111111111111"), @@ -501,7 +501,7 @@ def test_xyz_stack() -> None: assert dim.gap == ints("100010001000100010001000") # Check that it still works if you consume then start on a point that should # be False - p = Path(spec.calculate()) + p = Path(spec.calculate(bounds=True)) assert p.consume(4).gap == ints("1000") assert p.consume(4).gap == ints("1000") assert p.consume(5).gap == ints("10001") @@ -514,7 +514,7 @@ def test_beam_selector() -> None: # Beam selector scan moves bounded between midpoints and lower and upper bounds at # maximum speed. Turnaround sections are where it sends the triggers spec: Spec[str] = 10 * ~Line.bounded(x, 11, 19, 1) - dim = spec.frames() + dim = spec.frames(bounds=True) assert len(dim) == 10 assert dim.lower == {x: approx([11, 19, 11, 19, 11, 19, 11, 19, 11, 19])} assert dim.upper == {x: approx([19, 11, 19, 11, 19, 11, 19, 11, 19, 11])} @@ -525,7 +525,7 @@ def test_beam_selector() -> None: def test_gap_repeat() -> None: # Check that no gap propogates to dim.gap for snaked axis spec = Repeat[str](10, gap=False) * ~Line.bounded(x, 11, 19, 1) - dim = spec.frames() + dim = spec.frames(bounds=True) assert len(dim) == 10 assert dim.lower == {x: approx([11, 19, 11, 19, 11, 19, 11, 19, 11, 19])} assert dim.upper == {x: approx([19, 11, 19, 11, 19, 11, 19, 11, 19, 11])} @@ -536,7 +536,7 @@ def test_gap_repeat() -> None: def test_gap_repeat_non_snake() -> None: # Check that no gap doesn't propogate to dim.gap for non-snaked axis spec = Repeat[str](3, gap=False) * Line.bounded(x, 11, 19, 1) - dim = spec.frames() + dim = spec.frames(bounds=True) assert len(dim) == 3 assert dim.lower == {x: approx([11, 11, 11])} assert dim.upper == {x: approx([19, 19, 19])} @@ -554,7 +554,7 @@ def test_multiple_statics(): {"x": 0.0, "y": 4, "z": 5}, {"x": 10.0, "y": 4, "z": 5}, ] - assert spec.frames().gap == ints("1010") + assert spec.frames(bounds=True).gap == ints("1010") def test_multiple_statics_with_grid(): @@ -572,7 +572,7 @@ def test_multiple_statics_with_grid(): {"x": 0.0, "y": 10.0, "a": 4, "b": 5}, {"x": 10.0, "y": 10.0, "a": 4, "b": 5}, ] - assert spec.frames().gap == ints("10101010") + assert spec.frames(bounds=True).gap == ints("10101010") @pytest.mark.parametrize( From b4e94704d37068b8fa1226868906cc995fc7ea01 Mon Sep 17 00:00:00 2001 From: Luis Segalla Date: Mon, 28 Jul 2025 18:18:36 +0100 Subject: [PATCH 24/38] Updated spec.py Added support for __rmatmul__ Added fly function back but put a deprecation warning on it and the step function --- src/scanspec/specs.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/scanspec/specs.py b/src/scanspec/specs.py index 8d45a262..9eebd826 100644 --- a/src/scanspec/specs.py +++ b/src/scanspec/specs.py @@ -109,6 +109,11 @@ def shape(self) -> tuple[int, ...]: def __rmul__(self, other: int) -> Product[Axis]: return if_instance_do(other, int, lambda o: Product(Repeat(o), self)) + def __rmatmul__(self, other: float) -> Product[Axis]: + return if_instance_do( + other, float, lambda o: Product(ConstantDuration(o), self) + ) + @overload def __mul__(self, other: Spec[Axis]) -> Product[Axis]: ... @@ -789,6 +794,29 @@ def spaced( Spiral.spaced = validate_call(Spiral.spaced) # type:ignore +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) + + """ + warnings.warn( + f"fly method is deprecated! Use {duration} @ spec instead", + DeprecationWarning, + stacklevel=2, + ) + + return ConstantDuration(constant_duration=duration, spec=spec) + + def step(spec: Spec[Axis], duration: float, num: int = 1) -> Spec[Axis]: """Step scan, with num frames of given duration at each frame in the spec. @@ -805,6 +833,11 @@ def step(spec: Spec[Axis], duration: float, num: int = 1) -> Spec[Axis]: spec = step(Line("x", 1, 2, 3), 0.1) """ + warnings.warn( + f"step method is deprecated! Use {duration} @ spec instead", + DeprecationWarning, + stacklevel=2, + ) return ConstantDuration(constant_duration=duration, spec=spec) From 1be894d444078bc875f968851ccea5192ba190dd Mon Sep 17 00:00:00 2001 From: Luis Segalla Date: Mon, 28 Jul 2025 18:21:06 +0100 Subject: [PATCH 25/38] Updated tests tests_cli needed to be changed because previously, the default value for bounds was `True` which would imply now that we're doing a fly scan. This is not the default anymore. Updated specs tests so that they would also catch the error messages --- tests/test_cli.py | 21 +++++++------ tests/test_specs.py | 76 +++++++++++++++++++++------------------------ 2 files changed, 46 insertions(+), 51 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 421667b9..71d0b305 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -83,21 +83,21 @@ def test_plot_1D_line() -> None: lines = axes.lines assert len(lines) == 4 # Splines - assert_min_max_2d(lines[0], 0.5, 1.5, 0, 0) - assert_min_max_2d(lines[1], 1.5, 2.5, 0, 0) + assert_min_max_2d(lines[0], 1.0, 1.0, 0, 0) + assert_min_max_2d(lines[1], 1.0, 2.0, 0, 0) # Capture points - assert_min_max_2d(lines[2], 1, 2, 0, 0, length=2) + assert_min_max_2d(lines[2], 1.0, 2.0, 0.0, 0.0, length=2) # End - assert_min_max_2d(lines[3], 2.5, 2.5, 0, 0) + assert_min_max_2d(lines[3], 2.0, 2.0, 0, 0) # Arrows texts = cast(list[Annotation], axes.texts) assert len(texts) == 1 - assert tuple(texts[0].xy) == (0.5, 0) + assert tuple(texts[0].xy) == (2.0, 0) def test_plot_1D_line_snake_repeat() -> None: runner = CliRunner() - spec = '2 * ~Line.bounded("x", 1, 2, 1)' + spec = 'Fly(2 * ~Line.bounded("x", 1, 2, 1))' with patch("scanspec.plot.plt.show"): result = runner.invoke(cli.cli, ["plot", spec]) assert result.stdout == "" @@ -123,7 +123,7 @@ def test_plot_1D_line_snake_repeat() -> None: def test_plot_1D_step() -> None: runner = CliRunner() - spec = 'step(Line("x", 1, 2, 2), 0.1)' + spec = 'ConstantDuration(0.1,Line("x", 1, 2, 2))' with patch("scanspec.plot.plt.show"): result = runner.invoke(cli.cli, ["plot", spec]) assert result.stdout == "" @@ -146,7 +146,7 @@ def test_plot_1D_step() -> None: def test_plot_2D_line() -> None: runner = CliRunner() - spec = 'Line("y", 2, 3, 2) * Snake(Line("x", 1, 2, 2))' + spec = 'Fly(Line("y", 2, 3, 2) * Snake(Line("x", 1, 2, 2)))' with patch("scanspec.plot.plt.show"): result = runner.invoke(cli.cli, ["plot", spec]) assert result.exit_code == 0 @@ -174,7 +174,8 @@ def test_plot_2D_line() -> None: def test_plot_2D_line_rect_region() -> None: runner = CliRunner() - spec = "Line(y, 1, 3, 5) * Line(x, 0, 2, 3) & Rectangle(x, y, 0, 1.1, 1.5, 2.1, 30)" + spec = "Fly(Line(y, 1, 3, 5) * Line(x, 0, 2, 3)\ + & Rectangle(x, y, 0, 1.1, 1.5, 2.1, 30))" with patch("scanspec.plot.plt.show"): result = runner.invoke(cli.cli, ["plot", spec]) assert result.exit_code == 0 @@ -210,7 +211,7 @@ def test_plot_2D_line_rect_region() -> None: def test_plot_3D_line() -> None: runner = CliRunner() - spec = 'Snake(Line("z", 5, 6, 2) * Line("y", 2, 3, 2) * Line("x", 1, 2, 2))' + spec = 'Fly(Snake(Line("z", 5, 6, 2) * Line("y", 2, 3, 2) * Line("x", 1, 2, 2)))' with patch("scanspec.plot.plt.show"): result = runner.invoke(cli.cli, ["plot", spec]) assert result.exit_code == 0 diff --git a/tests/test_specs.py b/tests/test_specs.py index 3ee200c4..fe7905c2 100644 --- a/tests/test_specs.py +++ b/tests/test_specs.py @@ -17,7 +17,6 @@ Squash, Static, Zip, - step, ) from . import approx @@ -40,6 +39,13 @@ def test_one_point_line() -> None: assert dim.duration is None +def test_one_point_duration() -> None: + duration = ConstantDuration[Any](1.0) + (dim,) = duration.calculate() + assert dim.duration == approx([1.0]) + assert duration.axes() == [] + + def test_two_point_line() -> None: inst = Line(x, 0, 1, 2) (dim,) = inst.calculate(bounds=True) @@ -50,7 +56,7 @@ def test_two_point_line() -> None: def test_two_point_stepped_line() -> None: - inst = step(Line(x, 0, 1, 2), 0.1) + inst = ConstantDuration(0.1, Line(x, 0, 1, 2)) (dim,) = inst.calculate() assert dim.midpoints == dim.lower == dim.upper == {x: approx([0, 1])} assert dim.gap == ints("11") @@ -83,47 +89,33 @@ def test_many_point_line() -> None: def test_empty_dimension() -> None: - with pytest.raises(ValueError): + with pytest.raises(ValueError) as msg: Dimension(midpoints={}, upper={}, lower={}, gap=None, duration=None) + assert "self.gap is undefined" in str(msg.value) def test_concat() -> None: - (dim1,) = Fly( - spec=ConstantDuration(constant_duration=1, spec=Line("x", 0, 1, 2)) - ).calculate() - (dim2,) = Line("x", 3, 4, 2).calculate() + dim1 = Fly(spec=ConstantDuration(constant_duration=1, spec=Line("x", 0, 1, 2))) + dim2 = Line("x", 3, 4, 2) - # spec2 has no duration which will raise an error in concat_duration - with pytest.raises(ValueError): - dim1.concat(dim2) + with pytest.raises(ValueError) as msg: + Concat(dim1, dim2) + assert "Only one of left and right defines a duration" in str(msg.value) - (dim2,) = Fly( - spec=ConstantDuration(constant_duration=1, spec=Line("x", 3, 4, 2)) - ).calculate() + dim2 = Fly(spec=ConstantDuration(constant_duration=1, spec=Line("x", 3, 4, 2))) - dim1.concat(dim2) + spec = Concat(dim1, dim2) - assert dim1.duration == approx([1, 1]) + assert spec.frames().duration == approx([1, 1, 1, 1]) def test_zip() -> None: - (dim1,) = Fly( - spec=ConstantDuration(constant_duration=1, spec=Line("x", 0, 1, 2)) - ).calculate() - - (dim2,) = Fly( - spec=ConstantDuration(constant_duration=2, spec=Line("y", 3, 4, 2)) - ).calculate() + dim1 = Fly(spec=ConstantDuration(constant_duration=1, spec=Line("x", 0, 1, 2))) + dim2 = Fly(spec=ConstantDuration(constant_duration=2, spec=Line("y", 3, 4, 2))) - with pytest.raises(ValueError): - dim1.zip(dim2) - - -def test_one_point_duration() -> None: - duration = ConstantDuration[Any](1.0) - (dim,) = duration.calculate() - assert dim.duration == approx([1.0]) - assert duration.axes() == [] + with pytest.raises(ValueError) as cm: + Zip(dim1, dim2) + assert "Both left and right define a duration" in str(cm.value) def test_one_point_bounded_line() -> None: @@ -265,10 +257,11 @@ def test_product_snaking_lines() -> None: def test_product_duration() -> None: - with pytest.raises(ValueError): + with pytest.raises(ValueError) as msg: _ = Fly(ConstantDuration(1, Line(y, 1, 2, 3))) * Fly( ConstantDuration(1, ~Line(x, 0, 1, 2)) ) + assert "Outer axes defined a duration" in str(msg.value) def test_concat_lines() -> None: @@ -281,8 +274,9 @@ def test_concat_lines() -> None: assert dim.gap == ints("10100") # Test concating one Spec with duration and another one without - with pytest.raises(ValueError): + with pytest.raises(ValueError) as msg: Concat(ConstantDuration(1, Line(x, 0, 1, 2)), Line(x, 1, 2, 3)) + assert "Only one of left and right defines a duration" in str(msg.value) # Variable duration concat spec = Concat( @@ -607,16 +601,16 @@ def test_shape(spec: Spec[Any], expected_shape: tuple[int, ...]): def test_constant_duration(): spec1 = Fly(ConstantDuration(spec=Line("x", 0, 1, 2), constant_duration=1)) - spec2 = step(Line("y", 0, 1, 2), 2) + spec2 = ConstantDuration(2, Line("x", 0, 1, 2)) - with pytest.raises(ValueError): + with pytest.raises(ValueError) as msg: ConstantDuration(2, spec1) + assert f"{spec1} already defines a duration" in str(msg.value) - with pytest.raises(ValueError): + with pytest.raises(ValueError) as msg: spec1.zip(spec2) + assert "Both left and right define a duration" in str(msg.value) - with pytest.raises(ValueError): - spec1 = ConstantDuration(1, spec1) - - with pytest.raises(ValueError): - spec1.concat(Fly(ConstantDuration(1, spec2))) + with pytest.raises(ValueError) as msg: + spec1.concat(Line("x", 0, 1, 2)) + assert "Only one of left and right defines a duration" in str(msg.value) From 63f86eabb223d60251d6e33f0b6e164340cee1c9 Mon Sep 17 00:00:00 2001 From: Luis Segalla Date: Tue, 29 Jul 2025 11:39:00 +0100 Subject: [PATCH 26/38] Updated test Added new tests for fly, step and get_constant_duration functions. Also updated their warning messages --- src/scanspec/specs.py | 2 +- tests/test_specs.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/scanspec/specs.py b/src/scanspec/specs.py index 9eebd826..70bbbc69 100644 --- a/src/scanspec/specs.py +++ b/src/scanspec/specs.py @@ -809,7 +809,7 @@ def fly(spec: Spec[Axis], duration: float) -> Spec[Axis | str]: """ warnings.warn( - f"fly method is deprecated! Use {duration} @ spec instead", + f"fly method is deprecated! Use Fly(ConstnatDuration({duration},spec)) instead", DeprecationWarning, stacklevel=2, ) diff --git a/tests/test_specs.py b/tests/test_specs.py index fe7905c2..ee64b9c8 100644 --- a/tests/test_specs.py +++ b/tests/test_specs.py @@ -17,6 +17,9 @@ Squash, Static, Zip, + fly, + get_constant_duration, + step, ) from . import approx @@ -614,3 +617,42 @@ def test_constant_duration(): with pytest.raises(ValueError) as msg: spec1.concat(Line("x", 0, 1, 2)) assert "Only one of left and right defines a duration" in str(msg.value) + + +@pytest.mark.filterwarnings("ignore:fly") +def test_fly(): + spec = fly(Line("x", 0, 1, 5), 0.1) + (dim,) = spec.calculate() + assert dim.midpoints == {x: approx([0, 0.25, 0.5, 0.75, 1])} + assert dim.duration == approx( + [ + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + ] + ) + + +@pytest.mark.filterwarnings("ignore:step") +def test_step(): + (dim,) = step(Line("x", 0, 1, 5), 0.1).calculate() + assert ( + dim.midpoints == dim.lower == dim.upper == {x: approx([0, 0.25, 0.5, 0.75, 1])} + ) + assert dim.duration == approx( + [ + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + ] + ) + + +@pytest.mark.filterwarnings("ignore:get_constant_duration") +def test_get_constant_duration(): + spec = Fly(ConstantDuration(1, Line("x", 0, 1, 4))).calculate() + assert get_constant_duration(spec) == 1 From 27e4038ee8ed2ef80ff69380b261ec9666e5e96d Mon Sep 17 00:00:00 2001 From: Luis Segalla Date: Tue, 29 Jul 2025 11:39:09 +0100 Subject: [PATCH 27/38] Updated docs --- docs/how-to/iterate-a-spec.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/how-to/iterate-a-spec.rst b/docs/how-to/iterate-a-spec.rst index 5b866771..84103c7f 100644 --- a/docs/how-to/iterate-a-spec.rst +++ b/docs/how-to/iterate-a-spec.rst @@ -30,10 +30,11 @@ If you need to do a fly scan If you are conducting a fly scan then you need the frames that the motor moves through. You can get that from the lower and upper bounds of each point. If the -scan is small enough to fit in memory on the machine you can use the `Spec.frames()` +scan is small enough to fit in memory on the machine you can use the `Fly(spec).frames()` method to produce a single `Dimension` object containing the entire scan: ->>> segment = spec.frames() +>>> from scanspec.specs import Fly +>>> segment = Fly(spec).frames() >>> len(segment) 3 >>> segment.lower From 0ecbfe49dd6ef95ad5c2c81436d9cc2c89bba5ed Mon Sep 17 00:00:00 2001 From: Luis Segalla Date: Tue, 29 Jul 2025 11:50:01 +0100 Subject: [PATCH 28/38] Updated README.md --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index fe60e30f..45e7c3bd 100644 --- a/README.md +++ b/README.md @@ -31,11 +31,11 @@ An example ScanSpec of a 2D snaked grid flyscan inside a circle spending 0.4s at each point: ```python -from scanspec.specs import Line, fly +from scanspec.specs import Line, Fly, ConstantDuration from scanspec.regions import Circle grid = Line(y, 2.1, 3.8, 12) * ~Line(x, 0.5, 1.5, 10) -spec = fly(grid, 0.4) & Circle(x, y, 1.0, 2.8, radius=0.5) +spec = Fly(ConstantDuration(0.4, grid)) & Circle(x, y, 1.0, 2.8, radius=0.5) ``` Which when plotted looks like: @@ -48,8 +48,8 @@ Scan points can be iterated through directly for convenience: for point in spec.midpoints(): print(point) # ... -# {'y': 3.1818181818181817, 'x': 0.8333333333333333, 'DURATION': 0.4} -# {'y': 3.1818181818181817, 'x': 0.7222222222222222, 'DURATION': 0.4} +# {'y': 3.1818181818181817, 'x': 0.8333333333333333} +# {'y': 3.1818181818181817, 'x': 0.7222222222222222} ``` or a Path created from the stack of Frames and chunks of a given length @@ -60,12 +60,13 @@ from scanspec.core import Path stack = spec.calculate() len(stack[0]) # 44 -stack[0].axes() # ['y', 'x', 'DURATION'] +stack[0].axes() # ['y', 'x'] path = Path(stack, start=5, num=30) chunk = path.consume(10) chunk.midpoints # {'x': , 'y': , 'DURATION': } chunk.upper # bounds are same dimensionality as positions +chunk.duration # duration of each frame ``` From 9c819194ac73e0221fdfcad52139a3e8409c94ee Mon Sep 17 00:00:00 2001 From: Luis Segalla Date: Tue, 29 Jul 2025 11:50:08 +0100 Subject: [PATCH 29/38] Fix typos --- src/scanspec/specs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scanspec/specs.py b/src/scanspec/specs.py index 70bbbc69..fb2c1eea 100644 --- a/src/scanspec/specs.py +++ b/src/scanspec/specs.py @@ -809,7 +809,7 @@ def fly(spec: Spec[Axis], duration: float) -> Spec[Axis | str]: """ warnings.warn( - f"fly method is deprecated! Use Fly(ConstnatDuration({duration},spec)) instead", + f"fly method is deprecated! Use Fly(ConstantDuration({duration},spec)) instead", DeprecationWarning, stacklevel=2, ) From 0322a09c5ca7dfb6034df4fec22644f15ce384b9 Mon Sep 17 00:00:00 2001 From: Luis Segalla Date: Tue, 29 Jul 2025 11:59:57 +0100 Subject: [PATCH 30/38] Remove DURATION after rebase --- src/scanspec/core.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/scanspec/core.py b/src/scanspec/core.py index e6a9caa4..68fb9be2 100644 --- a/src/scanspec/core.py +++ b/src/scanspec/core.py @@ -51,7 +51,6 @@ def get_original_bases(cls: type, /) -> tuple[Any, ...]: "Midpoints", "discriminated_union_of_subclasses", "StrictConfig", - "DURATION", "Slice", ] From ec79cb0beb2b3d811fc3c8bbd1ae62b8266c02f2 Mon Sep 17 00:00:00 2001 From: Luis Segalla Date: Tue, 29 Jul 2025 15:10:58 +0100 Subject: [PATCH 31/38] Small changes Reverting test_rect_region_difference to a fly scan Added return values to duration method on Fly class --- src/scanspec/specs.py | 2 +- tests/test_specs.py | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/scanspec/specs.py b/src/scanspec/specs.py index fb2c1eea..98c32a6e 100644 --- a/src/scanspec/specs.py +++ b/src/scanspec/specs.py @@ -611,7 +611,7 @@ class Fly(Spec[Axis]): def axes(self) -> list[Axis]: # noqa: D102 return self.spec.axes() - def duration(self): # noqa: D102 + def duration(self) -> float | None | Literal["VARIABLE_DURATION"]: # noqa: D102 return self.spec.duration() def calculate( # noqa: D102 diff --git a/tests/test_specs.py b/tests/test_specs.py index ee64b9c8..b22d1c1c 100644 --- a/tests/test_specs.py +++ b/tests/test_specs.py @@ -365,9 +365,11 @@ def test_rect_region_difference() -> None: Line(y, 1, 3, 5) * Line(x, 0, 2, 3) & Rectangle(x, y, 0, 1, 1.5, 2.2) ) - Rectangle(x, y, 0.5, 1.5, 2, 2.5) - inst = ConstantDuration( - 0.1, - spec, + inst = Fly( + ConstantDuration( + 0.1, + spec, + ) ) assert inst.axes() == [y, x] (dim,) = inst.calculate() @@ -376,7 +378,7 @@ def test_rect_region_difference() -> None: y: approx([1, 1, 1.5, 2]), } assert dim.duration == approx([0.1, 0.1, 0.1, 0.1]) - assert dim.gap == ints("1111") + assert dim.gap == ints("1011") def test_rect_region_symmetricdifference() -> None: From 698016da4d5340c5586a118fbebd33fd18db1390 Mon Sep 17 00:00:00 2001 From: Luis Segalla Date: Wed, 30 Jul 2025 08:14:43 +0100 Subject: [PATCH 32/38] Fixing __rmatmul__ --- src/scanspec/specs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scanspec/specs.py b/src/scanspec/specs.py index 98c32a6e..bcc47d8c 100644 --- a/src/scanspec/specs.py +++ b/src/scanspec/specs.py @@ -109,9 +109,9 @@ def shape(self) -> tuple[int, ...]: def __rmul__(self, other: int) -> Product[Axis]: return if_instance_do(other, int, lambda o: Product(Repeat(o), self)) - def __rmatmul__(self, other: float) -> Product[Axis]: + def __rmatmul__(self, other: float) -> ConstantDuration[Axis]: return if_instance_do( - other, float, lambda o: Product(ConstantDuration(o), self) + other, float, lambda o: ConstantDuration(constant_duration=o, spec=self) ) @overload From 84807905a6f3eb047cf70c86fe1379ed018cc75c Mon Sep 17 00:00:00 2001 From: Luis Segalla Date: Wed, 30 Jul 2025 08:35:04 +0100 Subject: [PATCH 33/38] Updated tests to improve code coverage --- tests/test_specs.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/test_specs.py b/tests/test_specs.py index b22d1c1c..f36feb23 100644 --- a/tests/test_specs.py +++ b/tests/test_specs.py @@ -59,7 +59,7 @@ def test_two_point_line() -> None: def test_two_point_stepped_line() -> None: - inst = ConstantDuration(0.1, Line(x, 0, 1, 2)) + inst = 0.1 @ Line("x", 0, 1, 2) (dim,) = inst.calculate() assert dim.midpoints == dim.lower == dim.upper == {x: approx([0, 1])} assert dim.gap == ints("11") @@ -111,6 +111,16 @@ def test_concat() -> None: assert spec.frames().duration == approx([1, 1, 1, 1]) + # Check that concat on the Dimension class works as expected + (dim1,) = Line("x", 3, 4, 2).calculate() + (dim2,) = ConstantDuration(1, Line("x", 3, 4, 2)).calculate() + + with pytest.raises(ValueError) as msg: + dim1.concat(dim2) + assert "Can't concatenate dimensions unless all or none provide durations" in str( + msg.value + ) + def test_zip() -> None: dim1 = Fly(spec=ConstantDuration(constant_duration=1, spec=Line("x", 0, 1, 2))) @@ -120,6 +130,14 @@ def test_zip() -> None: Zip(dim1, dim2) assert "Both left and right define a duration" in str(cm.value) + # Forcing the Specs into dimensions and trying to zip them + (dim1,) = dim1.calculate() + (dim2,) = dim2.calculate() + + with pytest.raises(ValueError) as cm: + dim1.zip(dim2) + assert "Can't have more than one durations array" in str(cm.value) + def test_one_point_bounded_line() -> None: inst = Line.bounded(x, 0, 1, 1) @@ -658,3 +676,9 @@ def test_step(): def test_get_constant_duration(): spec = Fly(ConstantDuration(1, Line("x", 0, 1, 4))).calculate() assert get_constant_duration(spec) == 1 + + spec = Concat( + ConstantDuration(1, Line(x, 0, 1, 2)), ConstantDuration(2, Line(x, 1, 2, 3)) + ).calculate() + + assert get_constant_duration(spec) is None From bba01056c53731488b5df9ce57369c822cce478d Mon Sep 17 00:00:00 2001 From: Luis Segalla Date: Wed, 30 Jul 2025 10:31:55 +0100 Subject: [PATCH 34/38] Updated Tests --- tests/test_cli.py | 12 ++++++------ tests/test_service.py | 20 ++++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 71d0b305..27c98bd6 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -75,7 +75,7 @@ def assert_3d_arrow( def test_plot_1D_line() -> None: runner = CliRunner() - spec = 'Line("x", 1, 2, 2)' + spec = 'Fly(Line("x", 1, 2, 2))' with patch("scanspec.plot.plt.show"): result = runner.invoke(cli.cli, ["plot", spec]) assert result.stdout == "" @@ -83,16 +83,16 @@ def test_plot_1D_line() -> None: lines = axes.lines assert len(lines) == 4 # Splines - assert_min_max_2d(lines[0], 1.0, 1.0, 0, 0) - assert_min_max_2d(lines[1], 1.0, 2.0, 0, 0) + assert_min_max_2d(lines[0], 0.5, 1.5, 0, 0) + assert_min_max_2d(lines[1], 1.5, 2.5, 0, 0) # Capture points - assert_min_max_2d(lines[2], 1.0, 2.0, 0.0, 0.0, length=2) + assert_min_max_2d(lines[2], 1, 2, 0, 0, length=2) # End - assert_min_max_2d(lines[3], 2.0, 2.0, 0, 0) + assert_min_max_2d(lines[3], 2.5, 2.5, 0, 0) # Arrows texts = cast(list[Annotation], axes.texts) assert len(texts) == 1 - assert tuple(texts[0].xy) == (2.0, 0) + assert tuple(texts[0].xy) == (0.5, 0) def test_plot_1D_line_snake_repeat() -> None: diff --git a/tests/test_service.py b/tests/test_service.py index 3f9786de..d69b0412 100644 --- a/tests/test_service.py +++ b/tests/test_service.py @@ -5,7 +5,7 @@ from fastapi.testclient import TestClient from scanspec.service import PointsFormat, PointsRequest, app -from scanspec.specs import Line +from scanspec.specs import Fly, Line @pytest.fixture @@ -59,18 +59,18 @@ def test_subsampling(client: TestClient) -> None: [ ( PointsFormat.FLOAT_LIST, - [0, 0.25, 0.5, 0.75, 1], - [0, 0.25, 0.5, 0.75, 1], + [-0.125, 0.125, 0.375, 0.625, 0.875], + [0.125, 0.375, 0.625, 0.875, 1.125], ), ( PointsFormat.STRING, - "[0. 0.25 0.5 0.75 1. ]", - "[0. 0.25 0.5 0.75 1. ]", + "[-0.125 0.125 0.375 0.625 0.875]", + "[0.125 0.375 0.625 0.875 1.125]", ), ( PointsFormat.BASE64_ENCODED, - "AAAAAAAAAAAAAAAAAADQPwAAAAAAAOA/AAAAAAAA6D8AAAAAAADwPw==", - "AAAAAAAAAAAAAAAAAADQPwAAAAAAAOA/AAAAAAAA6D8AAAAAAADwPw==", + "AAAAAAAAwL8AAAAAAADAPwAAAAAAANg/AAAAAAAA5D8AAAAAAADsPw==", + "AAAAAAAAwD8AAAAAAADYPwAAAAAAAOQ/AAAAAAAA7D8AAAAAAADyPw==", ), ], ids=["float_list", "string", "base64"], @@ -78,7 +78,7 @@ def test_subsampling(client: TestClient) -> None: def test_bounds( client: TestClient, format: PointsFormat, expected_lower: Any, expected_upper: Any ) -> None: - request = PointsRequest(Line("x", 0.0, 1.0, 5), max_frames=5, format=format) + request = PointsRequest(Fly(Line("x", 0.0, 1.0, 5)), max_frames=5, format=format) response = client.post("/bounds", json=asdict(request)) assert response.status_code == 200 assert response.json() == { @@ -93,11 +93,11 @@ def test_bounds( # GAP TEST(S) # def test_gap(client: TestClient) -> None: # If not defined specs will default to a step scan - spec = Line("y", 0.0, 10.0, 3) * Line("x", 0.0, 10.0, 3) + spec = Fly(Line("y", 0.0, 10.0, 3) * Line("x", 0.0, 10.0, 3)) response = client.post("/gap", json=spec.serialize()) assert response.status_code == 200 assert response.json() == { - "gap": [True, True, True, True, True, True, True, True, True] + "gap": [True, False, False, True, False, False, True, False, False] } From 6e997f15c874e7185f4b69f5757534e7f31fe8c8 Mon Sep 17 00:00:00 2001 From: Luis Segalla Date: Wed, 30 Jul 2025 14:27:41 +0100 Subject: [PATCH 35/38] Updated README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 45e7c3bd..af672a0c 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ stack[0].axes() # ['y', 'x'] path = Path(stack, start=5, num=30) chunk = path.consume(10) -chunk.midpoints # {'x': , 'y': , 'DURATION': } +chunk.midpoints # {'x': , 'y': } chunk.upper # bounds are same dimensionality as positions chunk.duration # duration of each frame ``` From d0d7cb3a5839872fc17e0f630a97bbf241d97f35 Mon Sep 17 00:00:00 2001 From: Luis Segalla Date: Wed, 30 Jul 2025 15:30:16 +0100 Subject: [PATCH 36/38] Updating cases where ConstantDuration was used to use @ instead --- README.md | 4 ++-- docs/how-to/iterate-a-spec.rst | 5 ++--- src/scanspec/specs.py | 7 ++++--- tests/test_specs.py | 2 ++ 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index af672a0c..99034f95 100644 --- a/README.md +++ b/README.md @@ -31,11 +31,11 @@ An example ScanSpec of a 2D snaked grid flyscan inside a circle spending 0.4s at each point: ```python -from scanspec.specs import Line, Fly, ConstantDuration +from scanspec.specs import Line, Fly from scanspec.regions import Circle grid = Line(y, 2.1, 3.8, 12) * ~Line(x, 0.5, 1.5, 10) -spec = Fly(ConstantDuration(0.4, grid)) & Circle(x, y, 1.0, 2.8, radius=0.5) +spec = Fly(0.4 @ grid) & Circle(x, y, 1.0, 2.8, radius=0.5) ``` Which when plotted looks like: diff --git a/docs/how-to/iterate-a-spec.rst b/docs/how-to/iterate-a-spec.rst index 84103c7f..e345ef5e 100644 --- a/docs/how-to/iterate-a-spec.rst +++ b/docs/how-to/iterate-a-spec.rst @@ -90,9 +90,8 @@ You may need to know where there is a gap between points, so that you can do something in the turnaround. For example, if we take the x axis of a grid scan, you can see it snakes back and forth: ->>> from scanspec.specs import Line, Fly, ConstantDuration ->>> spec = Line("y", 0, 1, 2) * ~Line("x", 1, 2, 3) ->>> grid = Fly(ConstantDuration(0.1,spec)) +>>> from scanspec.specs import Line, Fly +>>> grid = Fly(0.1 @ Line("y", 0, 1, 2) * ~Line("x", 1, 2, 3)) >>> chunk = grid.frames() >>> chunk.midpoints["x"] array([1. , 1.5, 2. , 2. , 1.5, 1. ]) diff --git a/src/scanspec/specs.py b/src/scanspec/specs.py index bcc47d8c..460f12b8 100644 --- a/src/scanspec/specs.py +++ b/src/scanspec/specs.py @@ -46,6 +46,7 @@ "Spiral", "Fly", "step", + "fly", ] VARIABLE_DURATION = "VARIABLE_DURATION" @@ -809,12 +810,12 @@ def fly(spec: Spec[Axis], duration: float) -> Spec[Axis | str]: """ warnings.warn( - f"fly method is deprecated! Use Fly(ConstantDuration({duration},spec)) instead", + f"fly method is deprecated! Use Fly({duration} @ spec) instead", DeprecationWarning, stacklevel=2, ) - return ConstantDuration(constant_duration=duration, spec=spec) + return Fly(duration @ spec) def step(spec: Spec[Axis], duration: float, num: int = 1) -> Spec[Axis]: @@ -838,7 +839,7 @@ def step(spec: Spec[Axis], duration: float, num: int = 1) -> Spec[Axis]: DeprecationWarning, stacklevel=2, ) - return ConstantDuration(constant_duration=duration, spec=spec) + return duration @ spec def get_constant_duration(frames: list[Dimension[Any]]) -> float | None: diff --git a/tests/test_specs.py b/tests/test_specs.py index f36feb23..f541d038 100644 --- a/tests/test_specs.py +++ b/tests/test_specs.py @@ -644,6 +644,8 @@ def test_fly(): spec = fly(Line("x", 0, 1, 5), 0.1) (dim,) = spec.calculate() assert dim.midpoints == {x: approx([0, 0.25, 0.5, 0.75, 1])} + assert dim.upper == {x: approx([0.125, 0.375, 0.625, 0.875, 1.125])} + assert dim.lower == {x: approx([-0.125, 0.125, 0.375, 0.625, 0.875])} assert dim.duration == approx( [ 0.1, From 12e39838a6de4cb8293deb9fba126952e736c598 Mon Sep 17 00:00:00 2001 From: Luis Segalla Date: Wed, 30 Jul 2025 15:40:15 +0100 Subject: [PATCH 37/38] Fix docs --- docs/how-to/iterate-a-spec.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/how-to/iterate-a-spec.rst b/docs/how-to/iterate-a-spec.rst index e345ef5e..24e465a5 100644 --- a/docs/how-to/iterate-a-spec.rst +++ b/docs/how-to/iterate-a-spec.rst @@ -91,7 +91,7 @@ something in the turnaround. For example, if we take the x axis of a grid scan, you can see it snakes back and forth: >>> from scanspec.specs import Line, Fly ->>> grid = Fly(0.1 @ Line("y", 0, 1, 2) * ~Line("x", 1, 2, 3)) +>>> grid = Fly(0.1 @ (Line("y", 0, 1, 2) * ~Line("x", 1, 2, 3))) >>> chunk = grid.frames() >>> chunk.midpoints["x"] array([1. , 1.5, 2. , 2. , 1.5, 1. ]) From 2555880a6cc92c9e2a900522ac7b98ba1cdb170d Mon Sep 17 00:00:00 2001 From: Luis Segalla Date: Thu, 31 Jul 2025 08:57:15 +0100 Subject: [PATCH 38/38] Updated all tests to use @ instead of ConstantDuration --- tests/test_cli.py | 2 +- tests/test_specs.py | 43 ++++++++++++++++--------------------------- 2 files changed, 17 insertions(+), 28 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 27c98bd6..920ce78b 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -123,7 +123,7 @@ def test_plot_1D_line_snake_repeat() -> None: def test_plot_1D_step() -> None: runner = CliRunner() - spec = 'ConstantDuration(0.1,Line("x", 1, 2, 2))' + spec = '0.1 @ Line("x", 1, 2, 2)' with patch("scanspec.plot.plt.show"): result = runner.invoke(cli.cli, ["plot", spec]) assert result.stdout == "" diff --git a/tests/test_specs.py b/tests/test_specs.py index f541d038..a276ca9e 100644 --- a/tests/test_specs.py +++ b/tests/test_specs.py @@ -67,7 +67,7 @@ def test_two_point_stepped_line() -> None: def test_two_point_fly_line() -> None: - inst = Fly(ConstantDuration(constant_duration=0.1, spec=Line(x, 0, 1, 2))) + inst = Fly(0.1 @ Line(x, 0, 1, 2)) (dim,) = inst.calculate() assert dim.midpoints == { x: approx([0, 1]), @@ -98,14 +98,14 @@ def test_empty_dimension() -> None: def test_concat() -> None: - dim1 = Fly(spec=ConstantDuration(constant_duration=1, spec=Line("x", 0, 1, 2))) + dim1 = Fly(1.0 @ Line("x", 0, 1, 2)) dim2 = Line("x", 3, 4, 2) with pytest.raises(ValueError) as msg: Concat(dim1, dim2) assert "Only one of left and right defines a duration" in str(msg.value) - dim2 = Fly(spec=ConstantDuration(constant_duration=1, spec=Line("x", 3, 4, 2))) + dim2 = Fly(1.0 @ Line("x", 3, 4, 2)) spec = Concat(dim1, dim2) @@ -113,7 +113,7 @@ def test_concat() -> None: # Check that concat on the Dimension class works as expected (dim1,) = Line("x", 3, 4, 2).calculate() - (dim2,) = ConstantDuration(1, Line("x", 3, 4, 2)).calculate() + (dim2,) = (1.0 @ Line("x", 3, 4, 2)).calculate() with pytest.raises(ValueError) as msg: dim1.concat(dim2) @@ -123,8 +123,8 @@ def test_concat() -> None: def test_zip() -> None: - dim1 = Fly(spec=ConstantDuration(constant_duration=1, spec=Line("x", 0, 1, 2))) - dim2 = Fly(spec=ConstantDuration(constant_duration=2, spec=Line("y", 3, 4, 2))) + dim1 = Fly(1.0 @ Line("x", 0, 1, 2)) + dim2 = Fly(2.0 @ Line("y", 3, 4, 2)) with pytest.raises(ValueError) as cm: Zip(dim1, dim2) @@ -242,7 +242,7 @@ def test_squashed_multiplied_snake_scan() -> None: inst = Line(z, 1, 2, 2) * Squash( Line(y, 1, 2, 2) * ~Line.bounded(x, 3, 7, 2) - * ConstantDuration(9, Repeat[str](2, gap=False)) # until #177 + * (9.0 @ Repeat[str](2, gap=False)) # until #177 ) assert inst.axes() == [z, y, x] (dimz, dimxyt) = inst.calculate() @@ -279,9 +279,7 @@ def test_product_snaking_lines() -> None: def test_product_duration() -> None: with pytest.raises(ValueError) as msg: - _ = Fly(ConstantDuration(1, Line(y, 1, 2, 3))) * Fly( - ConstantDuration(1, ~Line(x, 0, 1, 2)) - ) + _ = Fly(1.0 @ Line(y, 1, 2, 3)) * Fly(1.0 @ ~Line(x, 0, 1, 2)) assert "Outer axes defined a duration" in str(msg.value) @@ -296,13 +294,11 @@ def test_concat_lines() -> None: # Test concating one Spec with duration and another one without with pytest.raises(ValueError) as msg: - Concat(ConstantDuration(1, Line(x, 0, 1, 2)), Line(x, 1, 2, 3)) + Concat((1.0 @ Line(x, 0, 1, 2)), Line(x, 1, 2, 3)) assert "Only one of left and right defines a duration" in str(msg.value) # Variable duration concat - spec = Concat( - ConstantDuration(1, Line(x, 0, 1, 2)), ConstantDuration(2, Line(x, 1, 2, 3)) - ) + spec = Concat(1.0 @ Line(x, 0, 1, 2), 2.0 @ Line(x, 1, 2, 3)) assert spec.duration() == VARIABLE_DURATION @@ -383,12 +379,7 @@ def test_rect_region_difference() -> None: Line(y, 1, 3, 5) * Line(x, 0, 2, 3) & Rectangle(x, y, 0, 1, 1.5, 2.2) ) - Rectangle(x, y, 0.5, 1.5, 2, 2.5) - inst = Fly( - ConstantDuration( - 0.1, - spec, - ) - ) + inst = Fly(0.1 @ spec) assert inst.axes() == [y, x] (dim,) = inst.calculate() assert dim.midpoints == { @@ -623,11 +614,11 @@ def test_shape(spec: Spec[Any], expected_shape: tuple[int, ...]): def test_constant_duration(): - spec1 = Fly(ConstantDuration(spec=Line("x", 0, 1, 2), constant_duration=1)) - spec2 = ConstantDuration(2, Line("x", 0, 1, 2)) + spec1 = Fly(1.0 @ Line("x", 0, 1, 2)) + spec2 = 2.0 @ Line("x", 0, 1, 2) with pytest.raises(ValueError) as msg: - ConstantDuration(2, spec1) + 2.0 @ spec1 # type: ignore assert f"{spec1} already defines a duration" in str(msg.value) with pytest.raises(ValueError) as msg: @@ -676,11 +667,9 @@ def test_step(): @pytest.mark.filterwarnings("ignore:get_constant_duration") def test_get_constant_duration(): - spec = Fly(ConstantDuration(1, Line("x", 0, 1, 4))).calculate() + spec = Fly(1.0 @ Line("x", 0, 1, 4)).calculate() assert get_constant_duration(spec) == 1 - spec = Concat( - ConstantDuration(1, Line(x, 0, 1, 2)), ConstantDuration(2, Line(x, 1, 2, 3)) - ).calculate() + spec = Concat(1.0 @ Line(x, 0, 1, 2), 2.0 @ Line(x, 1, 2, 3)).calculate() assert get_constant_duration(spec) is None