From 509c056b6bac9504b121b6cd33c72b9e638b290a Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Fri, 13 Sep 2024 15:55:22 +0100 Subject: [PATCH] Set pyright to strict mode and fix type errors --- schema.json | 2072 +---------------------------------- src/scanspec/cli.py | 4 +- src/scanspec/core.py | 116 +- src/scanspec/plot.py | 118 +- src/scanspec/regions.py | 51 +- src/scanspec/service.py | 28 +- src/scanspec/specs.py | 152 ++- src/scanspec/sphinxext.py | 21 +- tests/__init__.py | 27 + tests/test_basemodel.py | 4 +- tests/test_cli.py | 89 +- tests/test_errors.py | 7 +- tests/test_iteration.py | 8 +- tests/test_serialization.py | 2 +- tests/test_specs.py | 254 ++--- 15 files changed, 524 insertions(+), 2429 deletions(-) diff --git a/schema.json b/schema.json index 30a5ae8e..ce0b2b58 100644 --- a/schema.json +++ b/schema.json @@ -1,2071 +1 @@ -{ - "openapi": "3.1.0", - "info": { - "title": "FastAPI", - "version": "0.1.1" - }, - "paths": { - "/valid": { - "post": { - "summary": "Valid", - "description": "Validate wether a ScanSpec 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": { - "allOf": [ - { - "$ref": "#/components/schemas/Spec-Input" - } - ], - "title": "Spec", - "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": { - "allOf": [ - { - "$ref": "#/components/schemas/PointsRequest" - } - ], - "title": "Request", - "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": { - "allOf": [ - { - "$ref": "#/components/schemas/PointsRequest" - } - ], - "title": "Request", - "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 request: Scanspec and formatting info.\n\nReturns:\n GapResponse: Bounds of the scan", - "operationId": "gap_gap_post", - "requestBody": { - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/Spec-Input" - } - ], - "title": "Spec", - "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": { - "allOf": [ - { - "$ref": "#/components/schemas/Spec-Input" - } - ], - "title": "Spec", - "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": { - "allOf": [ - { - "$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": { - "properties": { - "x_axis": { - "title": "X Axis", - "description": "The name matching the x axis of the spec" - }, - "y_axis": { - "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", - "enum": [ - "Circle" - ], - "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-Input": { - "properties": { - "left": { - "allOf": [ - { - "$ref": "#/components/schemas/Region-Input" - } - ], - "description": "The left-hand Region to combine" - }, - "right": { - "allOf": [ - { - "$ref": "#/components/schemas/Region-Input" - } - ], - "description": "The right-hand Region to combine" - }, - "type": { - "type": "string", - "enum": [ - "CombinationOf" - ], - "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-Output": { - "properties": { - "left": { - "allOf": [ - { - "$ref": "#/components/schemas/Region-Output" - } - ], - "description": "The left-hand Region to combine" - }, - "right": { - "allOf": [ - { - "$ref": "#/components/schemas/Region-Output" - } - ], - "description": "The right-hand Region to combine" - }, - "type": { - "type": "string", - "enum": [ - "CombinationOf" - ], - "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-Input": { - "properties": { - "left": { - "allOf": [ - { - "$ref": "#/components/schemas/Spec-Input" - } - ], - "description": "The left-hand Spec to Concat, midpoints will appear earlier" - }, - "right": { - "allOf": [ - { - "$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", - "enum": [ - "Concat" - ], - "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-Output": { - "properties": { - "left": { - "allOf": [ - { - "$ref": "#/components/schemas/Spec-Output" - } - ], - "description": "The left-hand Spec to Concat, midpoints will appear earlier" - }, - "right": { - "allOf": [ - { - "$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", - "enum": [ - "Concat" - ], - "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-Input": { - "properties": { - "left": { - "allOf": [ - { - "$ref": "#/components/schemas/Region-Input" - } - ], - "description": "The left-hand Region to combine" - }, - "right": { - "allOf": [ - { - "$ref": "#/components/schemas/Region-Input" - } - ], - "description": "The right-hand Region to combine" - }, - "type": { - "type": "string", - "enum": [ - "DifferenceOf" - ], - "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-Output": { - "properties": { - "left": { - "allOf": [ - { - "$ref": "#/components/schemas/Region-Output" - } - ], - "description": "The left-hand Region to combine" - }, - "right": { - "allOf": [ - { - "$ref": "#/components/schemas/Region-Output" - } - ], - "description": "The right-hand Region to combine" - }, - "type": { - "type": "string", - "enum": [ - "DifferenceOf" - ], - "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": { - "properties": { - "x_axis": { - "title": "X Axis", - "description": "The name matching the x axis of the spec" - }, - "y_axis": { - "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", - "enum": [ - "Ellipse" - ], - "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-Input": { - "properties": { - "left": { - "allOf": [ - { - "$ref": "#/components/schemas/Region-Input" - } - ], - "description": "The left-hand Region to combine" - }, - "right": { - "allOf": [ - { - "$ref": "#/components/schemas/Region-Input" - } - ], - "description": "The right-hand Region to combine" - }, - "type": { - "type": "string", - "enum": [ - "IntersectionOf" - ], - "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-Output": { - "properties": { - "left": { - "allOf": [ - { - "$ref": "#/components/schemas/Region-Output" - } - ], - "description": "The left-hand Region to combine" - }, - "right": { - "allOf": [ - { - "$ref": "#/components/schemas/Region-Output" - } - ], - "description": "The right-hand Region to combine" - }, - "type": { - "type": "string", - "enum": [ - "IntersectionOf" - ], - "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": { - "properties": { - "axis": { - "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", - "enum": [ - "Line" - ], - "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-Input": { - "properties": { - "spec": { - "allOf": [ - { - "$ref": "#/components/schemas/Spec-Input" - } - ], - "description": "The Spec containing the source midpoints" - }, - "region": { - "allOf": [ - { - "$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", - "enum": [ - "Mask" - ], - "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 Frames objects, they will be squashed together.\n\n.. example_spec::\n\n from scanspec.regions import Circle\n from scanspec.specs import Line\n\n spec = Line(\"y\", 1, 3, 3) * Line(\"x\", 3, 5, 5) & Circle(\"x\", \"y\", 4, 2, 1.2)\n\nSee Also: `why-squash-can-change-path`" - }, - "Mask-Output": { - "properties": { - "spec": { - "allOf": [ - { - "$ref": "#/components/schemas/Spec-Output" - } - ], - "description": "The Spec containing the source midpoints" - }, - "region": { - "allOf": [ - { - "$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", - "enum": [ - "Mask" - ], - "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 Frames objects, they will be squashed together.\n\n.. example_spec::\n\n from scanspec.regions import Circle\n from scanspec.specs import Line\n\n spec = Line(\"y\", 1, 3, 3) * Line(\"x\", 3, 5, 5) & Circle(\"x\", \"y\", 4, 2, 1.2)\n\nSee Also: `why-squash-can-change-path`" - }, - "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": { - "allOf": [ - { - "$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": { - "allOf": [ - { - "$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": { - "allOf": [ - { - "$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": { - "properties": { - "x_axis": { - "title": "X Axis", - "description": "The name matching the x axis of the spec" - }, - "y_axis": { - "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", - "enum": [ - "Polygon" - ], - "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-Input": { - "properties": { - "outer": { - "allOf": [ - { - "$ref": "#/components/schemas/Spec-Input" - } - ], - "description": "Will be executed once" - }, - "inner": { - "allOf": [ - { - "$ref": "#/components/schemas/Spec-Input" - } - ], - "description": "Will be executed len(outer) times" - }, - "type": { - "type": "string", - "enum": [ - "Product" - ], - "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-Output": { - "properties": { - "outer": { - "allOf": [ - { - "$ref": "#/components/schemas/Spec-Output" - } - ], - "description": "Will be executed once" - }, - "inner": { - "allOf": [ - { - "$ref": "#/components/schemas/Spec-Output" - } - ], - "description": "Will be executed len(outer) times" - }, - "type": { - "type": "string", - "enum": [ - "Product" - ], - "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": { - "properties": { - "axis": { - "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", - "enum": [ - "Range" - ], - "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": { - "properties": { - "x_axis": { - "title": "X Axis", - "description": "The name matching the x axis of the spec" - }, - "y_axis": { - "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", - "enum": [ - "Rectangle" - ], - "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-Input" - }, - { - "$ref": "#/components/schemas/UnionOf-Input" - }, - { - "$ref": "#/components/schemas/IntersectionOf-Input" - }, - { - "$ref": "#/components/schemas/DifferenceOf-Input" - }, - { - "$ref": "#/components/schemas/SymmetricDifferenceOf-Input" - }, - { - "$ref": "#/components/schemas/Range" - }, - { - "$ref": "#/components/schemas/Rectangle" - }, - { - "$ref": "#/components/schemas/Polygon" - }, - { - "$ref": "#/components/schemas/Circle" - }, - { - "$ref": "#/components/schemas/Ellipse" - } - ], - "discriminator": { - "propertyName": "type", - "mapping": { - "Circle": "#/components/schemas/Circle", - "CombinationOf": "#/components/schemas/CombinationOf-Input", - "DifferenceOf": "#/components/schemas/DifferenceOf-Input", - "Ellipse": "#/components/schemas/Ellipse", - "IntersectionOf": "#/components/schemas/IntersectionOf-Input", - "Polygon": "#/components/schemas/Polygon", - "Range": "#/components/schemas/Range", - "Rectangle": "#/components/schemas/Rectangle", - "SymmetricDifferenceOf": "#/components/schemas/SymmetricDifferenceOf-Input", - "UnionOf": "#/components/schemas/UnionOf-Input" - } - } - }, - "Region-Output": { - "oneOf": [ - { - "$ref": "#/components/schemas/CombinationOf-Output" - }, - { - "$ref": "#/components/schemas/UnionOf-Output" - }, - { - "$ref": "#/components/schemas/IntersectionOf-Output" - }, - { - "$ref": "#/components/schemas/DifferenceOf-Output" - }, - { - "$ref": "#/components/schemas/SymmetricDifferenceOf-Output" - }, - { - "$ref": "#/components/schemas/Range" - }, - { - "$ref": "#/components/schemas/Rectangle" - }, - { - "$ref": "#/components/schemas/Polygon" - }, - { - "$ref": "#/components/schemas/Circle" - }, - { - "$ref": "#/components/schemas/Ellipse" - } - ], - "discriminator": { - "propertyName": "type", - "mapping": { - "Circle": "#/components/schemas/Circle", - "CombinationOf": "#/components/schemas/CombinationOf-Output", - "DifferenceOf": "#/components/schemas/DifferenceOf-Output", - "Ellipse": "#/components/schemas/Ellipse", - "IntersectionOf": "#/components/schemas/IntersectionOf-Output", - "Polygon": "#/components/schemas/Polygon", - "Range": "#/components/schemas/Range", - "Rectangle": "#/components/schemas/Rectangle", - "SymmetricDifferenceOf": "#/components/schemas/SymmetricDifferenceOf-Output", - "UnionOf": "#/components/schemas/UnionOf-Output" - } - } - }, - "Repeat": { - "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 Frames is snaked then the end and start of consecutive iterations of Spec will have no gap", - "default": true - }, - "type": { - "type": "string", - "enum": [ - "Repeat" - ], - "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-Input": { - "properties": { - "spec": { - "allOf": [ - { - "$ref": "#/components/schemas/Spec-Input" - } - ], - "description": "The Spec to run in reverse every other iteration" - }, - "type": { - "type": "string", - "enum": [ - "Snake" - ], - "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-Output": { - "properties": { - "spec": { - "allOf": [ - { - "$ref": "#/components/schemas/Spec-Output" - } - ], - "description": "The Spec to run in reverse every other iteration" - }, - "type": { - "type": "string", - "enum": [ - "Snake" - ], - "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-Input" - }, - { - "$ref": "#/components/schemas/Repeat" - }, - { - "$ref": "#/components/schemas/Zip-Input" - }, - { - "$ref": "#/components/schemas/Mask-Input" - }, - { - "$ref": "#/components/schemas/Snake-Input" - }, - { - "$ref": "#/components/schemas/Concat-Input" - }, - { - "$ref": "#/components/schemas/Squash-Input" - }, - { - "$ref": "#/components/schemas/Line" - }, - { - "$ref": "#/components/schemas/Static" - }, - { - "$ref": "#/components/schemas/Spiral" - } - ], - "discriminator": { - "propertyName": "type", - "mapping": { - "Concat": "#/components/schemas/Concat-Input", - "Line": "#/components/schemas/Line", - "Mask": "#/components/schemas/Mask-Input", - "Product": "#/components/schemas/Product-Input", - "Repeat": "#/components/schemas/Repeat", - "Snake": "#/components/schemas/Snake-Input", - "Spiral": "#/components/schemas/Spiral", - "Squash": "#/components/schemas/Squash-Input", - "Static": "#/components/schemas/Static", - "Zip": "#/components/schemas/Zip-Input" - } - } - }, - "Spec-Output": { - "oneOf": [ - { - "$ref": "#/components/schemas/Product-Output" - }, - { - "$ref": "#/components/schemas/Repeat" - }, - { - "$ref": "#/components/schemas/Zip-Output" - }, - { - "$ref": "#/components/schemas/Mask-Output" - }, - { - "$ref": "#/components/schemas/Snake-Output" - }, - { - "$ref": "#/components/schemas/Concat-Output" - }, - { - "$ref": "#/components/schemas/Squash-Output" - }, - { - "$ref": "#/components/schemas/Line" - }, - { - "$ref": "#/components/schemas/Static" - }, - { - "$ref": "#/components/schemas/Spiral" - } - ], - "discriminator": { - "propertyName": "type", - "mapping": { - "Concat": "#/components/schemas/Concat-Output", - "Line": "#/components/schemas/Line", - "Mask": "#/components/schemas/Mask-Output", - "Product": "#/components/schemas/Product-Output", - "Repeat": "#/components/schemas/Repeat", - "Snake": "#/components/schemas/Snake-Output", - "Spiral": "#/components/schemas/Spiral", - "Squash": "#/components/schemas/Squash-Output", - "Static": "#/components/schemas/Static", - "Zip": "#/components/schemas/Zip-Output" - } - } - }, - "Spiral": { - "properties": { - "x_axis": { - "title": "X Axis", - "description": "An identifier for what to move for x" - }, - "y_axis": { - "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", - "enum": [ - "Spiral" - ], - "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-Input": { - "properties": { - "spec": { - "allOf": [ - { - "$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", - "enum": [ - "Squash" - ], - "const": "Squash", - "title": "Type", - "default": "Squash" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "spec" - ], - "title": "Squash", - "description": "Squash a stack of Frames together into a single expanded Frames object.\n\nSee Also:\n `why-squash-can-change-path`\n\n.. example_spec::\n\n from scanspec.specs import Line, Squash\n\n spec = Squash(Line(\"y\", 1, 2, 3) * Line(\"x\", 0, 1, 4))" - }, - "Squash-Output": { - "properties": { - "spec": { - "allOf": [ - { - "$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", - "enum": [ - "Squash" - ], - "const": "Squash", - "title": "Type", - "default": "Squash" - } - }, - "additionalProperties": false, - "type": "object", - "required": [ - "spec" - ], - "title": "Squash", - "description": "Squash a stack of Frames together into a single expanded Frames object.\n\nSee Also:\n `why-squash-can-change-path`\n\n.. example_spec::\n\n from scanspec.specs import Line, Squash\n\n spec = Squash(Line(\"y\", 1, 2, 3) * Line(\"x\", 0, 1, 4))" - }, - "Static": { - "properties": { - "axis": { - "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", - "enum": [ - "Static" - ], - "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-Input": { - "properties": { - "left": { - "allOf": [ - { - "$ref": "#/components/schemas/Region-Input" - } - ], - "description": "The left-hand Region to combine" - }, - "right": { - "allOf": [ - { - "$ref": "#/components/schemas/Region-Input" - } - ], - "description": "The right-hand Region to combine" - }, - "type": { - "type": "string", - "enum": [ - "SymmetricDifferenceOf" - ], - "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-Output": { - "properties": { - "left": { - "allOf": [ - { - "$ref": "#/components/schemas/Region-Output" - } - ], - "description": "The left-hand Region to combine" - }, - "right": { - "allOf": [ - { - "$ref": "#/components/schemas/Region-Output" - } - ], - "description": "The right-hand Region to combine" - }, - "type": { - "type": "string", - "enum": [ - "SymmetricDifferenceOf" - ], - "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-Input": { - "properties": { - "left": { - "allOf": [ - { - "$ref": "#/components/schemas/Region-Input" - } - ], - "description": "The left-hand Region to combine" - }, - "right": { - "allOf": [ - { - "$ref": "#/components/schemas/Region-Input" - } - ], - "description": "The right-hand Region to combine" - }, - "type": { - "type": "string", - "enum": [ - "UnionOf" - ], - "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-Output": { - "properties": { - "left": { - "allOf": [ - { - "$ref": "#/components/schemas/Region-Output" - } - ], - "description": "The left-hand Region to combine" - }, - "right": { - "allOf": [ - { - "$ref": "#/components/schemas/Region-Output" - } - ], - "description": "The right-hand Region to combine" - }, - "type": { - "type": "string", - "enum": [ - "UnionOf" - ], - "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": { - "allOf": [ - { - "$ref": "#/components/schemas/Spec-Output" - } - ], - "description": "The input scanspec" - }, - "valid_spec": { - "allOf": [ - { - "$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-Input": { - "properties": { - "left": { - "allOf": [ - { - "$ref": "#/components/schemas/Spec-Input" - } - ], - "description": "The left-hand Spec to Zip, will appear earlier in axes" - }, - "right": { - "allOf": [ - { - "$ref": "#/components/schemas/Spec-Input" - } - ], - "description": "The right-hand Spec to Zip, will appear later in axes" - }, - "type": { - "type": "string", - "enum": [ - "Zip" - ], - "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 Frames are merged by:\n\n- If right creates a stack of a single Frames object of size 1, expand it to\n the size of the fastest Frames object created by left\n- Merge individual Frames objects together from fastest to slowest\n\nThis means that Zipping a Spec producing stack [l2, l1] with a Spec\nproducing stack [r1] will assert len(l1)==len(r1), and produce\nstack [l2, l1.zip(r1)].\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"z\", 1, 2, 3) * Line(\"y\", 3, 4, 5).zip(Line(\"x\", 4, 5, 5))" - }, - "Zip-Output": { - "properties": { - "left": { - "allOf": [ - { - "$ref": "#/components/schemas/Spec-Output" - } - ], - "description": "The left-hand Spec to Zip, will appear earlier in axes" - }, - "right": { - "allOf": [ - { - "$ref": "#/components/schemas/Spec-Output" - } - ], - "description": "The right-hand Spec to Zip, will appear later in axes" - }, - "type": { - "type": "string", - "enum": [ - "Zip" - ], - "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 Frames are merged by:\n\n- If right creates a stack of a single Frames object of size 1, expand it to\n the size of the fastest Frames object created by left\n- Merge individual Frames objects together from fastest to slowest\n\nThis means that Zipping a Spec producing stack [l2, l1] with a Spec\nproducing stack [r1] will assert len(l1)==len(r1), and produce\nstack [l2, l1.zip(r1)].\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"z\", 1, 2, 3) * Line(\"y\", 3, 4, 5).zip(Line(\"x\", 4, 5, 5))" - } - } - } -} \ No newline at end of file +{"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": {"allOf": [{"$ref": "#/components/schemas/Spec-Input"}], "title": "Spec", "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": {"allOf": [{"$ref": "#/components/schemas/PointsRequest"}], "title": "Request", "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": {"allOf": [{"$ref": "#/components/schemas/PointsRequest"}], "title": "Request", "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 request: Scanspec and formatting info.\n\nReturns:\n GapResponse: Bounds of the scan", "operationId": "gap_gap_post", "requestBody": {"content": {"application/json": {"schema": {"allOf": [{"$ref": "#/components/schemas/Spec-Input"}], "title": "Spec", "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": {"allOf": [{"$ref": "#/components/schemas/Spec-Input"}], "title": "Spec", "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": {"allOf": [{"$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": {"properties": {"x_axis": {"title": "X Axis", "description": "The name matching the x axis of the spec"}, "y_axis": {"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", "enum": ["Circle"], "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-Input": {"properties": {"left": {"allOf": [{"$ref": "#/components/schemas/Region-Input"}], "description": "The left-hand Region to combine"}, "right": {"allOf": [{"$ref": "#/components/schemas/Region-Input"}], "description": "The right-hand Region to combine"}, "type": {"type": "string", "enum": ["CombinationOf"], "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-Output": {"properties": {"left": {"allOf": [{"$ref": "#/components/schemas/Region-Output"}], "description": "The left-hand Region to combine"}, "right": {"allOf": [{"$ref": "#/components/schemas/Region-Output"}], "description": "The right-hand Region to combine"}, "type": {"type": "string", "enum": ["CombinationOf"], "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-Input": {"properties": {"left": {"allOf": [{"$ref": "#/components/schemas/Spec-Input"}], "description": "The left-hand Spec to Concat, midpoints will appear earlier"}, "right": {"allOf": [{"$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", "enum": ["Concat"], "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-Output": {"properties": {"left": {"allOf": [{"$ref": "#/components/schemas/Spec-Output"}], "description": "The left-hand Spec to Concat, midpoints will appear earlier"}, "right": {"allOf": [{"$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", "enum": ["Concat"], "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-Input": {"properties": {"left": {"allOf": [{"$ref": "#/components/schemas/Region-Input"}], "description": "The left-hand Region to combine"}, "right": {"allOf": [{"$ref": "#/components/schemas/Region-Input"}], "description": "The right-hand Region to combine"}, "type": {"type": "string", "enum": ["DifferenceOf"], "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-Output": {"properties": {"left": {"allOf": [{"$ref": "#/components/schemas/Region-Output"}], "description": "The left-hand Region to combine"}, "right": {"allOf": [{"$ref": "#/components/schemas/Region-Output"}], "description": "The right-hand Region to combine"}, "type": {"type": "string", "enum": ["DifferenceOf"], "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": {"properties": {"x_axis": {"title": "X Axis", "description": "The name matching the x axis of the spec"}, "y_axis": {"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", "enum": ["Ellipse"], "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-Input": {"properties": {"left": {"allOf": [{"$ref": "#/components/schemas/Region-Input"}], "description": "The left-hand Region to combine"}, "right": {"allOf": [{"$ref": "#/components/schemas/Region-Input"}], "description": "The right-hand Region to combine"}, "type": {"type": "string", "enum": ["IntersectionOf"], "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-Output": {"properties": {"left": {"allOf": [{"$ref": "#/components/schemas/Region-Output"}], "description": "The left-hand Region to combine"}, "right": {"allOf": [{"$ref": "#/components/schemas/Region-Output"}], "description": "The right-hand Region to combine"}, "type": {"type": "string", "enum": ["IntersectionOf"], "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": {"properties": {"axis": {"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", "enum": ["Line"], "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-Input": {"properties": {"spec": {"allOf": [{"$ref": "#/components/schemas/Spec-Input"}], "description": "The Spec containing the source midpoints"}, "region": {"allOf": [{"$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", "enum": ["Mask"], "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 Frames objects, they will be squashed together.\n\n.. example_spec::\n\n from scanspec.regions import Circle\n from scanspec.specs import Line\n\n spec = Line(\"y\", 1, 3, 3) * Line(\"x\", 3, 5, 5) & Circle(\"x\", \"y\", 4, 2, 1.2)\n\nSee Also: `why-squash-can-change-path`"}, "Mask-Output": {"properties": {"spec": {"allOf": [{"$ref": "#/components/schemas/Spec-Output"}], "description": "The Spec containing the source midpoints"}, "region": {"allOf": [{"$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", "enum": ["Mask"], "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 Frames objects, they will be squashed together.\n\n.. example_spec::\n\n from scanspec.regions import Circle\n from scanspec.specs import Line\n\n spec = Line(\"y\", 1, 3, 3) * Line(\"x\", 3, 5, 5) & Circle(\"x\", \"y\", 4, 2, 1.2)\n\nSee Also: `why-squash-can-change-path`"}, "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": {"allOf": [{"$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": {"allOf": [{"$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": {"allOf": [{"$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": {"properties": {"x_axis": {"title": "X Axis", "description": "The name matching the x axis of the spec"}, "y_axis": {"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", "enum": ["Polygon"], "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-Input": {"properties": {"outer": {"allOf": [{"$ref": "#/components/schemas/Spec-Input"}], "description": "Will be executed once"}, "inner": {"allOf": [{"$ref": "#/components/schemas/Spec-Input"}], "description": "Will be executed len(outer) times"}, "type": {"type": "string", "enum": ["Product"], "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-Output": {"properties": {"outer": {"allOf": [{"$ref": "#/components/schemas/Spec-Output"}], "description": "Will be executed once"}, "inner": {"allOf": [{"$ref": "#/components/schemas/Spec-Output"}], "description": "Will be executed len(outer) times"}, "type": {"type": "string", "enum": ["Product"], "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": {"properties": {"axis": {"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", "enum": ["Range"], "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": {"properties": {"x_axis": {"title": "X Axis", "description": "The name matching the x axis of the spec"}, "y_axis": {"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", "enum": ["Rectangle"], "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-Input"}, {"$ref": "#/components/schemas/UnionOf-Input"}, {"$ref": "#/components/schemas/IntersectionOf-Input"}, {"$ref": "#/components/schemas/DifferenceOf-Input"}, {"$ref": "#/components/schemas/SymmetricDifferenceOf-Input"}, {"$ref": "#/components/schemas/Range"}, {"$ref": "#/components/schemas/Rectangle"}, {"$ref": "#/components/schemas/Polygon"}, {"$ref": "#/components/schemas/Circle"}, {"$ref": "#/components/schemas/Ellipse"}], "discriminator": {"propertyName": "type", "mapping": {"Circle": "#/components/schemas/Circle", "CombinationOf": "#/components/schemas/CombinationOf-Input", "DifferenceOf": "#/components/schemas/DifferenceOf-Input", "Ellipse": "#/components/schemas/Ellipse", "IntersectionOf": "#/components/schemas/IntersectionOf-Input", "Polygon": "#/components/schemas/Polygon", "Range": "#/components/schemas/Range", "Rectangle": "#/components/schemas/Rectangle", "SymmetricDifferenceOf": "#/components/schemas/SymmetricDifferenceOf-Input", "UnionOf": "#/components/schemas/UnionOf-Input"}}}, "Region-Output": {"oneOf": [{"$ref": "#/components/schemas/CombinationOf-Output"}, {"$ref": "#/components/schemas/UnionOf-Output"}, {"$ref": "#/components/schemas/IntersectionOf-Output"}, {"$ref": "#/components/schemas/DifferenceOf-Output"}, {"$ref": "#/components/schemas/SymmetricDifferenceOf-Output"}, {"$ref": "#/components/schemas/Range"}, {"$ref": "#/components/schemas/Rectangle"}, {"$ref": "#/components/schemas/Polygon"}, {"$ref": "#/components/schemas/Circle"}, {"$ref": "#/components/schemas/Ellipse"}], "discriminator": {"propertyName": "type", "mapping": {"Circle": "#/components/schemas/Circle", "CombinationOf": "#/components/schemas/CombinationOf-Output", "DifferenceOf": "#/components/schemas/DifferenceOf-Output", "Ellipse": "#/components/schemas/Ellipse", "IntersectionOf": "#/components/schemas/IntersectionOf-Output", "Polygon": "#/components/schemas/Polygon", "Range": "#/components/schemas/Range", "Rectangle": "#/components/schemas/Rectangle", "SymmetricDifferenceOf": "#/components/schemas/SymmetricDifferenceOf-Output", "UnionOf": "#/components/schemas/UnionOf-Output"}}}, "Repeat": {"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 Frames is snaked then the end and start of consecutive iterations of Spec will have no gap", "default": true}, "type": {"type": "string", "enum": ["Repeat"], "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-Input": {"properties": {"spec": {"allOf": [{"$ref": "#/components/schemas/Spec-Input"}], "description": "The Spec to run in reverse every other iteration"}, "type": {"type": "string", "enum": ["Snake"], "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-Output": {"properties": {"spec": {"allOf": [{"$ref": "#/components/schemas/Spec-Output"}], "description": "The Spec to run in reverse every other iteration"}, "type": {"type": "string", "enum": ["Snake"], "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-Input"}, {"$ref": "#/components/schemas/Repeat"}, {"$ref": "#/components/schemas/Zip-Input"}, {"$ref": "#/components/schemas/Mask-Input"}, {"$ref": "#/components/schemas/Snake-Input"}, {"$ref": "#/components/schemas/Concat-Input"}, {"$ref": "#/components/schemas/Squash-Input"}, {"$ref": "#/components/schemas/Line"}, {"$ref": "#/components/schemas/Static"}, {"$ref": "#/components/schemas/Spiral"}], "discriminator": {"propertyName": "type", "mapping": {"Concat": "#/components/schemas/Concat-Input", "Line": "#/components/schemas/Line", "Mask": "#/components/schemas/Mask-Input", "Product": "#/components/schemas/Product-Input", "Repeat": "#/components/schemas/Repeat", "Snake": "#/components/schemas/Snake-Input", "Spiral": "#/components/schemas/Spiral", "Squash": "#/components/schemas/Squash-Input", "Static": "#/components/schemas/Static", "Zip": "#/components/schemas/Zip-Input"}}}, "Spec-Output": {"oneOf": [{"$ref": "#/components/schemas/Product-Output"}, {"$ref": "#/components/schemas/Repeat"}, {"$ref": "#/components/schemas/Zip-Output"}, {"$ref": "#/components/schemas/Mask-Output"}, {"$ref": "#/components/schemas/Snake-Output"}, {"$ref": "#/components/schemas/Concat-Output"}, {"$ref": "#/components/schemas/Squash-Output"}, {"$ref": "#/components/schemas/Line"}, {"$ref": "#/components/schemas/Static"}, {"$ref": "#/components/schemas/Spiral"}], "discriminator": {"propertyName": "type", "mapping": {"Concat": "#/components/schemas/Concat-Output", "Line": "#/components/schemas/Line", "Mask": "#/components/schemas/Mask-Output", "Product": "#/components/schemas/Product-Output", "Repeat": "#/components/schemas/Repeat", "Snake": "#/components/schemas/Snake-Output", "Spiral": "#/components/schemas/Spiral", "Squash": "#/components/schemas/Squash-Output", "Static": "#/components/schemas/Static", "Zip": "#/components/schemas/Zip-Output"}}}, "Spiral": {"properties": {"x_axis": {"title": "X Axis", "description": "An identifier for what to move for x"}, "y_axis": {"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", "enum": ["Spiral"], "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-Input": {"properties": {"spec": {"allOf": [{"$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", "enum": ["Squash"], "const": "Squash", "title": "Type", "default": "Squash"}}, "additionalProperties": false, "type": "object", "required": ["spec"], "title": "Squash", "description": "Squash a stack of Frames together into a single expanded Frames object.\n\nSee Also:\n `why-squash-can-change-path`\n\n.. example_spec::\n\n from scanspec.specs import Line, Squash\n\n spec = Squash(Line(\"y\", 1, 2, 3) * Line(\"x\", 0, 1, 4))"}, "Squash-Output": {"properties": {"spec": {"allOf": [{"$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", "enum": ["Squash"], "const": "Squash", "title": "Type", "default": "Squash"}}, "additionalProperties": false, "type": "object", "required": ["spec"], "title": "Squash", "description": "Squash a stack of Frames together into a single expanded Frames object.\n\nSee Also:\n `why-squash-can-change-path`\n\n.. example_spec::\n\n from scanspec.specs import Line, Squash\n\n spec = Squash(Line(\"y\", 1, 2, 3) * Line(\"x\", 0, 1, 4))"}, "Static": {"properties": {"axis": {"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", "enum": ["Static"], "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-Input": {"properties": {"left": {"allOf": [{"$ref": "#/components/schemas/Region-Input"}], "description": "The left-hand Region to combine"}, "right": {"allOf": [{"$ref": "#/components/schemas/Region-Input"}], "description": "The right-hand Region to combine"}, "type": {"type": "string", "enum": ["SymmetricDifferenceOf"], "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-Output": {"properties": {"left": {"allOf": [{"$ref": "#/components/schemas/Region-Output"}], "description": "The left-hand Region to combine"}, "right": {"allOf": [{"$ref": "#/components/schemas/Region-Output"}], "description": "The right-hand Region to combine"}, "type": {"type": "string", "enum": ["SymmetricDifferenceOf"], "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-Input": {"properties": {"left": {"allOf": [{"$ref": "#/components/schemas/Region-Input"}], "description": "The left-hand Region to combine"}, "right": {"allOf": [{"$ref": "#/components/schemas/Region-Input"}], "description": "The right-hand Region to combine"}, "type": {"type": "string", "enum": ["UnionOf"], "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-Output": {"properties": {"left": {"allOf": [{"$ref": "#/components/schemas/Region-Output"}], "description": "The left-hand Region to combine"}, "right": {"allOf": [{"$ref": "#/components/schemas/Region-Output"}], "description": "The right-hand Region to combine"}, "type": {"type": "string", "enum": ["UnionOf"], "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": {"allOf": [{"$ref": "#/components/schemas/Spec-Output"}], "description": "The input scanspec"}, "valid_spec": {"allOf": [{"$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-Input": {"properties": {"left": {"allOf": [{"$ref": "#/components/schemas/Spec-Input"}], "description": "The left-hand Spec to Zip, will appear earlier in axes"}, "right": {"allOf": [{"$ref": "#/components/schemas/Spec-Input"}], "description": "The right-hand Spec to Zip, will appear later in axes"}, "type": {"type": "string", "enum": ["Zip"], "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 Frames are merged by:\n\n- If right creates a stack of a single Frames object of size 1, expand it to\n the size of the fastest Frames object created by left\n- Merge individual Frames objects together from fastest to slowest\n\nThis means that Zipping a Spec producing stack [l2, l1] with a Spec\nproducing stack [r1] will assert len(l1)==len(r1), and produce\nstack [l2, l1.zip(r1)].\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"z\", 1, 2, 3) * Line(\"y\", 3, 4, 5).zip(Line(\"x\", 4, 5, 5))"}, "Zip-Output": {"properties": {"left": {"allOf": [{"$ref": "#/components/schemas/Spec-Output"}], "description": "The left-hand Spec to Zip, will appear earlier in axes"}, "right": {"allOf": [{"$ref": "#/components/schemas/Spec-Output"}], "description": "The right-hand Spec to Zip, will appear later in axes"}, "type": {"type": "string", "enum": ["Zip"], "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 Frames are merged by:\n\n- If right creates a stack of a single Frames object of size 1, expand it to\n the size of the fastest Frames object created by left\n- Merge individual Frames objects together from fastest to slowest\n\nThis means that Zipping a Spec producing stack [l2, l1] with a Spec\nproducing stack [r1] will assert len(l1)==len(r1), and produce\nstack [l2, l1.zip(r1)].\n\n.. example_spec::\n\n from scanspec.specs import Line\n\n spec = Line(\"z\", 1, 2, 3) * Line(\"y\", 3, 4, 5).zip(Line(\"x\", 4, 5, 5))"}}}} diff --git a/src/scanspec/cli.py b/src/scanspec/cli.py index 33f56520..fa8edfd2 100644 --- a/src/scanspec/cli.py +++ b/src/scanspec/cli.py @@ -18,7 +18,7 @@ ) @click.version_option(prog_name="scanspec", message="%(version)s") @click.pass_context -def cli(ctx, log_level: str): +def cli(ctx: click.Context, log_level: str): """Top level scanspec command line interface.""" level = getattr(logging, log_level.upper(), None) logging.basicConfig(format="%(levelname)s:%(message)s", level=level) @@ -48,7 +48,7 @@ def plot(spec: str): @click.option( "--port", default=8080, help="The port that the scanspec service will be hosted on." ) -def service(cors, port): +def service(cors: bool, port: int): """Run up a REST service.""" from scanspec.service import run_app diff --git a/src/scanspec/core.py b/src/scanspec/core.py index 6cb62d47..70d201bf 100644 --- a/src/scanspec/core.py +++ b/src/scanspec/core.py @@ -1,5 +1,6 @@ from __future__ import annotations +import itertools from collections.abc import Callable, Iterable, Iterator, Sequence from functools import lru_cache from inspect import isclass @@ -10,9 +11,11 @@ TypeVar, get_origin, get_type_hints, + overload, ) import numpy as np +import numpy.typing as npt from pydantic import BaseModel, ConfigDict, Field, GetCoreSchemaHandler from pydantic.dataclasses import is_pydantic_dataclass, rebuild_dataclass from pydantic_core import CoreSchema @@ -20,7 +23,6 @@ __all__ = [ "if_instance_do", - "Axis", "AxesPoints", "Frames", "SnakedFrames", @@ -36,7 +38,9 @@ StrictConfig: ConfigDict = {"extra": "forbid"} C = TypeVar("C") -T = TypeVar("T", type, Callable) +T = TypeVar("T") + +GapArray = npt.NDArray[np.bool] def discriminated_union_of_subclasses( @@ -117,7 +121,7 @@ def calculate(self) -> int: tagged_union = _TaggedUnion(super_cls, discriminator) _tagged_unions[super_cls] = tagged_union - def add_subclass_to_union(subclass): + def add_subclass_to_union(subclass: type[C]): # Add a discriminator field to a subclass so it can # be identified when deserializing subclass.__annotations__ = { @@ -126,7 +130,9 @@ def add_subclass_to_union(subclass): } setattr(subclass, discriminator, Field(subclass.__name__, repr=False)) # type: ignore - def get_schema_of_union(cls, source_type: Any, handler: GetCoreSchemaHandler): + def get_schema_of_union( + cls: type[C], source_type: Any, handler: GetCoreSchemaHandler + ): if cls is not super_cls: tagged_union.add_member(cls) return handler(cls) @@ -140,7 +146,17 @@ def get_schema_of_union(cls, source_type: Any, handler: GetCoreSchemaHandler): return super_cls -def uses_tagged_union(cls_or_func: T) -> T: +@overload +def uses_tagged_union(cls_or_func: type[C]) -> type[C]: ... + + +@overload +def uses_tagged_union(cls_or_func: Callable[..., T]) -> Callable[..., T]: ... + + +def uses_tagged_union( + cls_or_func: type[C] | Callable[..., T], +) -> type[C] | Callable[..., T]: """ T = TypeVar("T", type, Callable) Decorator that processes the type hints of a class or function to detect and @@ -162,13 +178,13 @@ def uses_tagged_union(cls_or_func: T) -> T: class _TaggedUnion: - def __init__(self, base_class: type, discriminator: str): + def __init__(self, base_class: type[Any], discriminator: str): self._base_class = base_class # Classes and their field names that refer to this tagged union self._discriminator = discriminator # The members of the tagged union, i.e. subclasses of the baseclass self._subclasses: list[type] = [] - self._references: set[type | Callable] = set() + self._references: set[type | Callable[..., Any]] = set() def add_member(self, cls: type): if cls in self._subclasses: @@ -180,12 +196,12 @@ def add_member(self, cls: type): for ref in self._references: _TaggedUnion._rebuild(ref) - def add_reference(self, cls_or_func: type | Callable): + def add_reference(self, cls_or_func: type | Callable[..., Any]): self._references.add(cls_or_func) @staticmethod # https://github.com/bluesky/scanspec/issues/133 - def _rebuild(cls_or_func: type | Callable): + def _rebuild(cls_or_func: type[Any] | Callable[..., Any]): if isclass(cls_or_func): if is_pydantic_dataclass(cls_or_func): rebuild_dataclass(cls_or_func, force=True) @@ -201,11 +217,13 @@ def schema(self, handler: GetCoreSchemaHandler) -> CoreSchema: @lru_cache(1) -def make_schema(members: tuple[type, ...], handler): +def make_schema( + members: tuple[type[Any], ...], handler: Callable[[Any], CoreSchema] +) -> dict[str, CoreSchema]: return {member.__name__: handler(member) for member in members} -def if_instance_do(x: Any, cls: type, func: Callable): +def if_instance_do(x: C, cls: type[C], func: Callable[[C], T]): """If x is of type cls then return func(x), otherwise return NotImplemented. Used as a helper when implementing operator overloading. @@ -219,9 +237,12 @@ def if_instance_do(x: Any, cls: type, func: Callable): #: A type variable for an `axis_` that can be specified for a scan Axis = TypeVar("Axis") +#: Alternative axis variable to be used when two are required in the same type binding +OtherAxis = TypeVar("OtherAxis") + #: Map of axes to float ndarray of points #: E.g. {xmotor: array([0, 1, 2]), ymotor: array([2, 2, 2])} -AxesPoints = dict[Axis, np.ndarray] +AxesPoints = dict[Axis, npt.NDArray[np.floating[Any]]] class Frames(Generic[Axis]): @@ -256,7 +277,7 @@ def __init__( midpoints: AxesPoints[Axis], lower: AxesPoints[Axis] | None = None, upper: AxesPoints[Axis] | None = None, - gap: np.ndarray | None = None, + gap: GapArray | None = None, ): #: The midpoints of scan frames for each axis self.midpoints = midpoints @@ -304,7 +325,9 @@ def __len__(self) -> int: # All axespoints arrays are same length, pick the first one return len(self.gap) - def extract(self, indices: np.ndarray, calculate_gap=True) -> Frames[Axis]: + def extract( + self, indices: npt.NDArray[np.signedinteger[Any]], calculate_gap: bool = True + ) -> Frames[Axis]: """Return a new Frames object restricted to the indices provided. Args: @@ -322,7 +345,7 @@ def extract_dict(ds: Iterable[AxesPoints[Axis]]) -> AxesPoints[Axis]: return {k: v[dim_indices] for k, v in d.items()} return {} - def extract_gap(gaps: Iterable[np.ndarray]) -> np.ndarray | None: + def extract_gap(gaps: Iterable[GapArray]) -> GapArray | None: for gap in gaps: if not calculate_gap: return gap[dim_indices] @@ -354,7 +377,7 @@ def concat_dict(ds: Sequence[AxesPoints[Axis]]) -> AxesPoints[Axis]: # lower[ax] = np.concatenate(self.lower[ax], other.lower[ax]) return {a: np.concatenate([d[a] for d in ds]) for a in self.axes()} - def concat_gap(gaps: Sequence[np.ndarray]) -> np.ndarray: + def concat_gap(gaps: Sequence[GapArray]) -> GapArray: g = np.concatenate(gaps) # Calc the first frame g[0] = gap_between_frames(other, self) @@ -382,7 +405,7 @@ def zip_dict(ds: Sequence[AxesPoints[Axis]]) -> AxesPoints[Axis]: # lower[ax] = {**self.lower[ax], **other.lower[ax]} return dict(kv for d in ds for kv in d.items()) - def zip_gap(gaps: Sequence[np.ndarray]) -> np.ndarray: + def zip_gap(gaps: Sequence[GapArray]) -> GapArray: # Gap if either frames has a gap. E.g. # gap[i] = self.gap[i] | other.gap[i] return np.logical_or.reduce(gaps) @@ -392,24 +415,24 @@ def zip_gap(gaps: Sequence[np.ndarray]) -> np.ndarray: def _merge_frames( *stack: Frames[Axis], - dict_merge=Callable[[Sequence[AxesPoints[Axis]]], AxesPoints[Axis]], # type: ignore - gap_merge=Callable[[Sequence[np.ndarray]], np.ndarray | None], + dict_merge: Callable[[Sequence[AxesPoints[Axis]]], AxesPoints[Axis]], # type: ignore + gap_merge: Callable[[Sequence[GapArray]], GapArray | None], ) -> Frames[Axis]: types = {type(fs) for fs in stack} assert len(types) == 1, f"Mismatching types for {stack}" cls = types.pop() - # If any lower or upper are different, apply to those - kwargs = {} - for a in ("lower", "upper"): - if any(fs.midpoints is not getattr(fs, a) for fs in stack): - kwargs[a] = dict_merge([getattr(fs, a) for fs in stack]) - # Apply to midpoints, force calculation of gap return cls( midpoints=dict_merge([fs.midpoints for fs in stack]), gap=gap_merge([fs.gap for fs in stack]), - **kwargs, + # If any lower or upper are different, apply to those + lower=dict_merge([fs.lower for fs in stack]) + if any(fs.midpoints is not fs.lower for fs in stack) + else None, + upper=dict_merge([fs.upper for fs in stack]) + if any(fs.midpoints is not fs.upper for fs in stack) + else None, ) @@ -421,7 +444,7 @@ def __init__( midpoints: AxesPoints[Axis], lower: AxesPoints[Axis] | None = None, upper: AxesPoints[Axis] | None = None, - gap: np.ndarray | None = None, + gap: GapArray | None = None, ): super().__init__(midpoints, lower=lower, upper=upper, gap=gap) # Override first element of gap to be True, as subsequent runs @@ -429,11 +452,15 @@ def __init__( self.gap[0] = False @classmethod - def from_frames(cls, frames: Frames[Axis]) -> SnakedFrames[Axis]: + def from_frames( + cls: type[SnakedFrames[Any]], frames: Frames[OtherAxis] + ) -> SnakedFrames[OtherAxis]: """Create a snaked version of a `Frames` object.""" return cls(frames.midpoints, frames.lower, frames.upper, frames.gap) - def extract(self, indices: np.ndarray, calculate_gap=True) -> Frames[Axis]: + def extract( + self, indices: npt.NDArray[np.int32], calculate_gap: bool = True + ) -> Frames[Axis]: """Return a new Frames object restricted to the indices provided. Args: @@ -461,23 +488,23 @@ def extract(self, indices: np.ndarray, calculate_gap=True) -> Frames[Axis]: cls = type(self) gap = None - # If lower or upper are different, apply to those - kwargs = {} - if self.midpoints is not self.lower: - # If going backwards select from the opposite bound - kwargs["lower"] = { + # Apply to midpoints + return cls( + {k: v[snake_indices] for k, v in self.midpoints.items()}, + gap=gap, + # If lower or upper are different, apply to those + lower={ k: np.where(backwards, self.upper[k][snake_indices], v[snake_indices]) for k, v in self.lower.items() } - if self.midpoints is not self.upper: - kwargs["upper"] = { + if self.midpoints is not self.lower + else None, + upper={ k: np.where(backwards, self.lower[k][snake_indices], v[snake_indices]) for k, v in self.upper.items() } - - # Apply to midpoints - return cls( - {k: v[snake_indices] for k, v in self.midpoints.items()}, gap=gap, **kwargs + if self.midpoints is not self.upper + else None, ) @@ -486,7 +513,9 @@ def gap_between_frames(frames1: Frames[Axis], frames2: Frames[Axis]) -> bool: return any(frames1.upper[a][-1] != frames2.lower[a][0] for a in frames1.axes()) -def squash_frames(stack: list[Frames[Axis]], check_path_changes=True) -> Frames[Axis]: +def squash_frames( + stack: list[Frames[Axis]], check_path_changes: bool = True +) -> Frames[Axis]: """Squash a stack of nested Frames into a single one. Args: @@ -648,10 +677,7 @@ def __init__(self, stack: list[Frames[Axis]]): @property def axes(self) -> list[Axis]: """The axes that will be present in each points dictionary.""" - axes = [] - for frames in self.stack: - axes += frames.axes() - return axes + return list(itertools.chain(*(frames.axes() for frames in self.stack))) def __len__(self) -> int: """The number of dictionaries that will be produced if iterated over.""" diff --git a/src/scanspec/plot.py b/src/scanspec/plot.py index e4fc1e8c..67d14ad6 100644 --- a/src/scanspec/plot.py +++ b/src/scanspec/plot.py @@ -1,12 +1,14 @@ -from collections.abc import Iterator +from collections.abc import Iterable, Iterator from itertools import cycle from typing import Any import numpy as np +import numpy.typing as npt from matplotlib import colors, patches from matplotlib import pyplot as plt -from mpl_toolkits.mplot3d import Axes3D, proj3d -from scipy import interpolate +from matplotlib.axes import Axes +from mpl_toolkits.mplot3d import Axes3D, proj3d # type: ignore +from scipy import interpolate # type: ignore from .core import Path from .regions import Circle, Ellipse, Polygon, Rectangle, Region, find_regions @@ -15,51 +17,73 @@ __all__ = ["plot_spec"] -def _plot_arrays(axes, arrays: list[np.ndarray], **kwargs): +def _plot_arrays( + axes: Axes, arrays: list[npt.NDArray[np.floating[Any]]], **kwargs: Any +): if len(arrays) > 2: - axes.plot3D(arrays[2], arrays[1], arrays[0], **kwargs) + axes.plot3D(arrays[2], arrays[1], arrays[0], **kwargs) # type: ignore elif len(arrays) == 2: - axes.plot(arrays[1], arrays[0], **kwargs) + axes.plot(arrays[1], arrays[0], **kwargs) # type: ignore else: - axes.plot(arrays[0], np.zeros(len(arrays[0])), **kwargs) + axes.plot(arrays[0], np.zeros(len(arrays[0])), **kwargs) # type: ignore # https://stackoverflow.com/a/11156353 -class _Arrow3D(patches.FancyArrowPatch): - def __init__(self, xs, ys, zs, *args, **kwargs): - super().__init__((0, 0), (0, 0), *args, **kwargs) +class Arrow3D(patches.FancyArrowPatch): + def __init__( + self, + xs: npt.NDArray[np.floating[Any]], + ys: npt.NDArray[np.floating[Any]], + zs: npt.NDArray[np.floating[Any]], + *args: Any, + **kwargs: Any, + ): + super().__init__((0, 0), (0, 0), *args, **kwargs) # type: ignore self._verts3d = xs, ys, zs # Added here because of https://github.com/matplotlib/matplotlib/issues/21688 - def do_3d_projection(self, renderer=None): + def do_3d_projection(self, renderer: Any = None): xs3d, ys3d, zs3d = self._verts3d xs, ys, zs = proj3d.proj_transform(xs3d, ys3d, zs3d, self.axes.M) # type: ignore self.set_positions((xs[0], ys[0]), (xs[1], ys[1])) return np.min(zs) + @property + def verts3d( + self, + ) -> tuple[ + npt.NDArray[np.floating[Any]], + npt.NDArray[np.floating[Any]], + npt.NDArray[np.floating[Any]], + ]: + return self._verts3d -def _plot_arrow(axes, arrays: list[np.ndarray]): + +def _plot_arrow(axes: Axes, arrays: list[npt.NDArray[np.floating[Any]]]): if len(arrays) == 1: arrays = [np.array([0, 0])] + arrays if len(arrays) == 2: head = [a[-1] for a in reversed(arrays)] tail = [a[-1] - (a[-1] - a[-2]) * 0.1 for a in reversed(arrays)] - axes.annotate( + axes.annotate( # type: ignore "", - head[:2], - tail[:2], + tuple(head[:2]), + tuple(tail[:2]), arrowprops={"color": "lightgrey", "arrowstyle": "-|>"}, ) elif len(arrays) == 3: arrows = [a[-2:] for a in reversed(arrays)] - a = _Arrow3D( - *arrows[:3], mutation_scale=10, arrowstyle="-|>", color="lightgrey" - ) + a = Arrow3D(*arrows[:3], mutation_scale=10, arrowstyle="-|>", color="lightgrey") axes.add_artist(a) -def _plot_spline(axes, ranges, arrays: list[np.ndarray], index_colours: dict[int, str]): +def _plot_spline( + axes: Axes, + ranges: list[float], + arrays: list[npt.NDArray[np.floating[Any]]], + index_colours: dict[int, str], +) -> Iterable[list[npt.NDArray[np.floating[Any]]]]: scaled_arrays = [a / r for a, r in zip(arrays, ranges, strict=False)] # Define curves parametrically t = np.zeros(len(arrays[0])) @@ -74,12 +98,14 @@ def _plot_spline(axes, ranges, arrays: list[np.ndarray], index_colours: dict[int # There are no duplicated points, plot a spline t /= t[-1] # Scale the arrays so splines don't favour larger scaled axes - tck, _ = interpolate.splprep(scaled_arrays, k=2, s=0) + tck, _ = interpolate.splprep(scaled_arrays, k=2, s=0) # type: ignore starts = sorted(index_colours) stops = starts[1:] + [len(arrays[0]) - 1] for start, stop in zip(starts, stops, strict=False): - tnew = np.linspace(t[start], t[stop], num=1001) - spline = interpolate.splev(tnew, tck) + start_value: float = t[start] + stop_value: float = t[stop] + tnew = np.linspace(start_value, stop_value, num=1001) + spline: npt.NDArray[np.floating[Any]] = interpolate.splev(tnew, tck) # type: ignore # Scale the splines back to the original scaling unscaled_splines = [a * r for a, r in zip(spline, ranges, strict=False)] _plot_arrays(axes, unscaled_splines, color=index_colours[start]) @@ -108,31 +134,31 @@ def plot_spec(spec: Spec[Any], title: str | None = None): # Setup axes if ndims > 2: - plt.figure(figsize=(6, 6)) - plt_axes = plt.axes(projection="3d") - plt_axes.grid(False) + plt.figure(figsize=(6, 6)) # type: ignore + plt_axes: Axes = plt.axes(projection="3d") # type: ignore + plt_axes.grid(False) # type: ignore if isinstance(plt_axes, Axes3D): - plt_axes.set_zlabel(axes[-3]) - plt_axes.set_ylabel(axes[-2]) - plt_axes.view_init(elev=15) + plt_axes.set_zlabel(axes[-3]) # type: ignore + plt_axes.set_ylabel(axes[-2]) # type: ignore + plt_axes.view_init(elev=15) # type: ignore else: raise TypeError( "Expected matplotlib to create an Axes3D object, " f"instead got: {plt_axes}" ) elif ndims == 2: - plt.figure(figsize=(6, 6)) - plt_axes = plt.axes() - plt_axes.set_ylabel(axes[-2]) + plt.figure(figsize=(6, 6)) # type: ignore + plt_axes = plt.axes() # type: ignore + plt_axes.set_ylabel(axes[-2]) # type: ignore else: - plt.figure(figsize=(6, 2)) - plt_axes = plt.axes() + plt.figure(figsize=(6, 2)) # type: ignore + plt_axes = plt.axes() # type: ignore plt_axes.yaxis.set_visible(False) - plt_axes.set_xlabel(axes[-1]) + plt_axes.set_xlabel(axes[-1]) # type: ignore # Title with dimension sizes title = title or ", ".join(f"Dim[{' '.join(d.axes())} len={len(d)}]" for d in dims) - plt.title(title) + plt.title(title) # type: ignore # Plot any Regions if ndims <= 2: @@ -162,8 +188,8 @@ def plot_spec(spec: Spec[Any], title: str | None = None): plt_axes.add_patch(patches.Polygon(xy_verts, fill=False)) # Plot the splines - tail: Any = {a: None for a in axes} - ranges = [max(np.max(v) - np.min(v), 0.0001) for k, v in dim.midpoints.items()] + tail: dict[str, npt.NDArray[np.floating[Any]] | None] = {a: None for a in axes} + ranges = [max(float(np.max(v) - np.min(v)), 0.0001) for v in dim.midpoints.values()] seg_col = cycle(colors.TABLEAU_COLORS) last_index = 0 splines = None @@ -172,8 +198,8 @@ def plot_spec(spec: Spec[Any], title: str | None = None): gap_indices = list(np.nonzero(dim.gap[1:])[0] + 1) for index in gap_indices + [len(dim)]: num_points = index - last_index - arrays = [] - turnaround = [] + arrays: list[npt.NDArray[np.floating[Any]]] = [] + turnaround: list[npt.NDArray[np.floating[Any]]] = [] for a in axes: # Add the midpoints and the lower and upper bounds arr = np.empty(num_points * 2 + 1) @@ -182,13 +208,15 @@ def plot_spec(spec: Spec[Any], title: str | None = None): arr[-1] = dim.upper[a][index - 1] arrays.append(arr) # Add the turnaround - if tail[a] is not None: + axis_tail = tail[a] + if axis_tail is not None: # Already had a tail, add lead in points - tail[a][2:] = np.linspace(-0.01, 0, 2) * (arr[1] - arr[0]) + arr[0] - turnaround.append(tail[a]) + axis_tail[2:] = np.linspace(-0.01, 0, 2) * (arr[1] - arr[0]) + arr[0] + turnaround.append(axis_tail) # Add tail off points - tail[a] = np.empty(4) - tail[a][:2] = np.linspace(0, 0.01, 2) * (arr[-1] - arr[-2]) + arr[-1] + axis_tail = np.empty(4) + axis_tail[:2] = np.linspace(0, 0.01, 2) * (arr[-1] - arr[-2]) + arr[-1] + tail[a] = axis_tail last_index = index arrow_arr = None @@ -238,4 +266,4 @@ def plot_spec(spec: Spec[Any], title: str | None = None): color="lightgrey", ) - plt.show() + plt.show() # type: ignore diff --git a/src/scanspec/regions.py b/src/scanspec/regions.py index e60a3494..d50fb498 100644 --- a/src/scanspec/regions.py +++ b/src/scanspec/regions.py @@ -2,9 +2,10 @@ from collections.abc import Iterator, Mapping from dataclasses import is_dataclass -from typing import Any, Generic +from typing import Any, Generic, cast import numpy as np +import numpy.typing as npt from pydantic import BaseModel, Field, TypeAdapter from pydantic.dataclasses import dataclass @@ -32,6 +33,8 @@ "find_regions", ] +NpMask = npt.NDArray[np.bool] + @discriminated_union_of_subclasses class Region(Generic[Axis]): @@ -49,33 +52,33 @@ def axis_sets(self) -> list[set[Axis]]: """Produce the non-overlapping sets of axes this region spans.""" raise NotImplementedError(self) - def mask(self, points: AxesPoints[Axis]) -> np.ndarray: + def mask(self, points: AxesPoints[Axis]) -> NpMask: """Produce a mask of which points are in the region.""" raise NotImplementedError(self) - def __or__(self, other) -> UnionOf[Axis]: + def __or__(self, other: Region[Axis]) -> UnionOf[Axis]: return if_instance_do(other, Region, lambda o: UnionOf(self, o)) - def __and__(self, other) -> IntersectionOf[Axis]: + def __and__(self, other: Region[Axis]) -> IntersectionOf[Axis]: return if_instance_do(other, Region, lambda o: IntersectionOf(self, o)) - def __sub__(self, other) -> DifferenceOf[Axis]: + def __sub__(self, other: Region[Axis]) -> DifferenceOf[Axis]: return if_instance_do(other, Region, lambda o: DifferenceOf(self, o)) - def __xor__(self, other) -> SymmetricDifferenceOf[Axis]: + def __xor__(self, other: Region[Axis]) -> SymmetricDifferenceOf[Axis]: return if_instance_do(other, Region, lambda o: SymmetricDifferenceOf(self, o)) def serialize(self) -> Mapping[str, Any]: """Serialize the Region to a dictionary.""" - return TypeAdapter(Region).dump_python(self) + return TypeAdapter(Region[Any]).dump_python(self) @staticmethod - def deserialize(obj) -> Region: + def deserialize(obj: Any) -> Region[Any]: """Deserialize a Region from a dictionary.""" - return TypeAdapter(Region).validate_python(obj) + return TypeAdapter(Region[Any]).validate_python(obj) -def get_mask(region: Region[Axis], points: AxesPoints[Axis]) -> np.ndarray: +def get_mask(region: Region[Axis], points: AxesPoints[Axis]) -> NpMask: """Return a mask of the points inside the region. If there is an overlap of axes of region and points return a @@ -86,7 +89,7 @@ def get_mask(region: Region[Axis], points: AxesPoints[Axis]) -> np.ndarray: if needs_mask: return region.mask(points) else: - return np.ones(len(list(points.values())[0])) + return np.ones(len(list(points.values())[0]), dtype=np.bool) def _merge_axis_sets(axis_sets: list[set[Axis]]) -> Iterator[set[Axis]]: @@ -130,7 +133,7 @@ class UnionOf(CombinationOf[Axis]): array([False, True, True, True, False]) """ - def mask(self, points: AxesPoints[Axis]) -> np.ndarray: + def mask(self, points: AxesPoints[Axis]) -> NpMask: mask = get_mask(self.left, points) | get_mask(self.right, points) return mask @@ -146,7 +149,7 @@ class IntersectionOf(CombinationOf[Axis]): array([False, False, True, False, False]) """ - def mask(self, points: AxesPoints[Axis]) -> np.ndarray: + def mask(self, points: AxesPoints[Axis]) -> NpMask: mask = get_mask(self.left, points) & get_mask(self.right, points) return mask @@ -162,7 +165,7 @@ class DifferenceOf(CombinationOf[Axis]): array([False, True, False, False, False]) """ - def mask(self, points: AxesPoints[Axis]) -> np.ndarray: + def mask(self, points: AxesPoints[Axis]) -> NpMask: left_mask = get_mask(self.left, points) # Return the xor restricted to the left region mask = left_mask ^ get_mask(self.right, points) & left_mask @@ -180,7 +183,7 @@ class SymmetricDifferenceOf(CombinationOf[Axis]): array([False, True, False, True, False]) """ - def mask(self, points: AxesPoints[Axis]) -> np.ndarray: + def mask(self, points: AxesPoints[Axis]) -> NpMask: mask = get_mask(self.left, points) ^ get_mask(self.right, points) return mask @@ -201,7 +204,7 @@ class Range(Region[Axis]): def axis_sets(self) -> list[set[Axis]]: return [{self.axis}] - def mask(self, points: AxesPoints[Axis]) -> np.ndarray: + def mask(self, points: AxesPoints[Axis]) -> NpMask: v = points[self.axis] mask = np.bitwise_and(v >= self.min, v <= self.max) return mask @@ -233,7 +236,7 @@ class Rectangle(Region[Axis]): def axis_sets(self) -> list[set[Axis]]: return [{self.x_axis, self.y_axis}] - def mask(self, points: AxesPoints[Axis]) -> np.ndarray: + def mask(self, points: AxesPoints[Axis]) -> NpMask: x = points[self.x_axis] - self.x_min y = points[self.y_axis] - self.y_min if self.angle != 0: @@ -273,15 +276,15 @@ class Polygon(Region[Axis]): def axis_sets(self) -> list[set[Axis]]: return [{self.x_axis, self.y_axis}] - def mask(self, points: AxesPoints[Axis]) -> np.ndarray: + def mask(self, points: AxesPoints[Axis]) -> NpMask: x = points[self.x_axis] y = points[self.y_axis] v1x, v1y = self.x_verts[-1], self.y_verts[-1] - mask = np.full(len(x), False, dtype=np.int8) + mask = np.full(len(x), False, dtype=np.bool) for v2x, v2y in zip(self.x_verts, self.y_verts, strict=False): # skip horizontal edges if v2y != v1y: - vmask = np.full(len(x), False, dtype=np.int8) + vmask = np.full(len(x), False, dtype=np.bool) vmask |= (y < v2y) & (y >= v1y) vmask |= (y < v1y) & (y >= v2y) t = (y - v1y) / (v2y - v1y) @@ -313,7 +316,7 @@ class Circle(Region[Axis]): def axis_sets(self) -> list[set[Axis]]: return [{self.x_axis, self.y_axis}] - def mask(self, points: AxesPoints[Axis]) -> np.ndarray: + def mask(self, points: AxesPoints[Axis]) -> NpMask: x = points[self.x_axis] - self.x_middle y = points[self.y_axis] - self.y_middle mask = x * x + y * y <= (self.radius * self.radius) @@ -348,7 +351,7 @@ class Ellipse(Region[Axis]): def axis_sets(self) -> list[set[Axis]]: return [{self.x_axis, self.y_axis}] - def mask(self, points: AxesPoints[Axis]) -> np.ndarray: + def mask(self, points: AxesPoints[Axis]) -> NpMask: x = points[self.x_axis] - self.x_middle y = points[self.y_axis] - self.y_middle if self.angle != 0: @@ -362,7 +365,7 @@ def mask(self, points: AxesPoints[Axis]) -> np.ndarray: return mask -def find_regions(obj) -> Iterator[Region[Axis]]: +def find_regions(obj: Any) -> Iterator[Region[Any]]: """Recursively yield Regions from obj and its children.""" if ( hasattr(obj, "__pydantic_model__") @@ -372,5 +375,5 @@ def find_regions(obj) -> Iterator[Region[Axis]]: if isinstance(obj, Region): yield obj for name in obj.__dict__.keys(): - regions: Iterator[Region[Axis]] = find_regions(getattr(obj, name)) + regions: Iterator[Region[Any]] = find_regions(getattr(cast(Any, obj), name)) yield from regions diff --git a/src/scanspec/service.py b/src/scanspec/service.py index b05e4113..390ad473 100644 --- a/src/scanspec/service.py +++ b/src/scanspec/service.py @@ -2,8 +2,10 @@ import json from collections.abc import Mapping from enum import Enum +from typing import Any import numpy as np +import numpy.typing as npt from fastapi import Body, FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.openapi.utils import get_openapi @@ -30,8 +32,8 @@ class ValidResponse: """Response model for spec validation.""" - input_spec: Spec = Field(description="The input scanspec") - valid_spec: Spec = Field(description="The validated version of the spec") + input_spec: Spec[str] = Field(description="The input scanspec") + valid_spec: Spec[str] = Field(description="The validated version of the spec") class PointsFormat(str, Enum): @@ -47,7 +49,7 @@ class PointsFormat(str, Enum): class PointsRequest: """A request for generated scan points.""" - spec: Spec = Field(description="The spec from which to generate points") + spec: Spec[str] = Field(description="The spec from which to generate points") max_frames: int | None = Field( description="The maximum number of points to return, if None will return " "as many as calculated", @@ -125,9 +127,9 @@ class SmallestStepResponse: @app.post("/valid", response_model=ValidResponse) def valid( - spec: Spec = Body(..., examples=[_EXAMPLE_SPEC]), + spec: Spec[str] = Body(..., examples=[_EXAMPLE_SPEC]), ) -> ValidResponse: - """Validate wether a ScanSpec can produce a viable scan. + """Validate wether a ScanSpec[str] can produce a viable scan. Args: spec: The scanspec to validate @@ -197,7 +199,7 @@ def bounds( @app.post("/gap", response_model=GapResponse) def gap( - spec: Spec = Body( + spec: Spec[str] = Body( ..., examples=[_EXAMPLE_SPEC], ), @@ -222,7 +224,7 @@ def gap( @app.post("/smalleststep", response_model=SmallestStepResponse) def smallest_step( - spec: Spec = Body(..., examples=[_EXAMPLE_SPEC]), + spec: Spec[str] = Body(..., examples=[_EXAMPLE_SPEC]), ) -> SmallestStepResponse: """Calculate the smallest step in a scan, both absolutely and per-axis. @@ -251,7 +253,7 @@ def smallest_step( # -def _to_chunk(request: PointsRequest) -> tuple[Frames, int]: +def _to_chunk(request: PointsRequest) -> tuple[Frames[str], int]: spec = Spec.deserialize(request.spec) dims = spec.calculate() # Grab dimensions from spec path = Path(dims) # Convert to a path @@ -297,7 +299,7 @@ def _format_axes_points( raise KeyError(f"Unknown format: {format}") -def _reduce_frames(stack: list[Frames[str]], max_frames: int) -> Path: +def _reduce_frames(stack: list[Frames[str]], max_frames: int) -> Path[str]: """Removes frames from a spec so len(path) < max_frames. Args: @@ -316,7 +318,7 @@ def _reduce_frames(stack: list[Frames[str]], max_frames: int) -> Path: return Path(sub_frames) -def _sub_sample(frames: Frames[str], ratio: float) -> Frames: +def _sub_sample(frames: Frames[str], ratio: float) -> Frames[str]: """Provides a sub-sample Frames object whilst preserving its core structure. Args: @@ -328,7 +330,7 @@ def _sub_sample(frames: Frames[str], ratio: float) -> Frames: return frames.extract(indexes, calculate_gap=False) -def _calc_smallest_step(points: list[np.ndarray]) -> float: +def _calc_smallest_step(points: list[npt.NDArray[np.float64]]) -> float: # Calc abs diffs of all axes, ignoring any zero values absolute_diffs = [_abs_diffs(axis_midpoints) for axis_midpoints in points] # Normalize and remove zeros @@ -338,7 +340,7 @@ def _calc_smallest_step(points: list[np.ndarray]) -> float: return np.amin(norm_diffs) -def _abs_diffs(array: np.ndarray) -> np.ndarray: +def _abs_diffs(array: npt.NDArray[np.number[Any]]) -> npt.NDArray[np.number[Any]]: """Calculates the absolute differences between adjacent elements in the array. Args: @@ -365,7 +367,7 @@ def run_app(cors: bool = False, port: int = 8080) -> None: import uvicorn - uvicorn.run(app, port=port) + uvicorn.run(app=app, port=port) def scanspec_schema_text() -> str: diff --git a/src/scanspec/specs.py b/src/scanspec/specs.py index 672f3b68..1f064e6a 100644 --- a/src/scanspec/specs.py +++ b/src/scanspec/specs.py @@ -1,17 +1,14 @@ from __future__ import annotations from collections.abc import Callable, Mapping -from typing import ( - Any, - Generic, -) +from typing import Any, Generic, TypeVar, overload import numpy as np +import numpy.typing as npt from pydantic import Field, TypeAdapter, validate_call from pydantic.dataclasses import dataclass from .core import ( - Axis, Frames, Midpoints, Path, @@ -45,6 +42,12 @@ #: Can be used as a special key to indicate how long each point should be DURATION = "DURATION" +#: A type variable for an `axis_` that can be specified for a scan +Axis = TypeVar("Axis", covariant=True) + +#: Alternative axis variable to be used when two are required in the same type binding +OtherAxis = TypeVar("OtherAxis") + @discriminated_union_of_subclasses class Spec(Generic[Axis]): @@ -65,7 +68,9 @@ def axes(self) -> list[Axis]: """ raise NotImplementedError(self) - def calculate(self, bounds=True, nested=False) -> list[Frames[Axis]]: + def calculate( + self, bounds: bool = True, nested: bool = False + ) -> list[Frames[Axis]]: """Produce a stack of nested `Frames` that form the scan. Ordered from slowest moving to fastest moving. @@ -84,21 +89,29 @@ def shape(self) -> tuple[int, ...]: """Return the final, simplified shape of the scan.""" return tuple(len(dim) for dim in self.calculate()) - def __rmul__(self, other: Spec[Axis]) -> Product[Axis]: + def __rmul__(self, other: int) -> Product[Axis]: return if_instance_do(other, int, lambda o: Product(Repeat(o), self)) - def __mul__(self, other: Spec[Axis]) -> Product[Axis]: + @overload + def __mul__(self, other: Spec[Axis]) -> Product[Axis]: ... + + @overload + def __mul__(self, other: Spec[OtherAxis]) -> Product[Axis | OtherAxis]: ... + + def __mul__( + self, other: Spec[Axis] | Spec[OtherAxis] + ) -> Product[Axis] | Product[Axis | OtherAxis]: return if_instance_do(other, Spec, lambda o: Product(self, o)) - def __and__(self, other: Spec[Axis]) -> Mask[Axis]: + def __and__(self, other: Region[Axis]) -> Mask[Axis]: return if_instance_do(other, Region, lambda o: Mask(self, o)) def __invert__(self) -> Snake[Axis]: return Snake(self) - def zip(self, other: Spec[Axis]) -> Zip[Axis]: + def zip(self, other: Spec[OtherAxis]) -> Zip[Axis | OtherAxis]: """`Zip` the Spec with another, iterating in tandem.""" - return Zip(self, other) + return Zip(left=self, right=other) def concat(self, other: Spec[Axis]) -> Concat[Axis]: """`Concat` the Spec with another, iterating one after the other.""" @@ -106,12 +119,12 @@ def concat(self, other: Spec[Axis]) -> Concat[Axis]: def serialize(self) -> Mapping[str, Any]: """Serialize the Spec to a dictionary.""" - return TypeAdapter(Spec).dump_python(self) + return TypeAdapter(Spec[Any]).dump_python(self) @staticmethod - def deserialize(obj) -> Spec: + def deserialize(obj: Any) -> Spec[Any]: """Deserialize a Spec from a dictionary.""" - return TypeAdapter(Spec).validate_python(obj) + return TypeAdapter(Spec[Any]).validate_python(obj) @dataclass(config=StrictConfig) @@ -130,10 +143,12 @@ class Product(Spec[Axis]): outer: Spec[Axis] = Field(description="Will be executed once") inner: Spec[Axis] = Field(description="Will be executed len(outer) times") - def axes(self) -> list: + def axes(self) -> list[Axis]: return self.outer.axes() + self.inner.axes() - def calculate(self, bounds=True, nested=False) -> list[Frames[Axis]]: + def calculate( + self, bounds: bool = True, nested: bool = False + ) -> list[Frames[Axis]]: frames_outer = self.outer.calculate(bounds=False, nested=nested) frames_inner = self.inner.calculate(bounds, nested=True) return frames_outer + frames_inner @@ -169,10 +184,12 @@ class Repeat(Spec[Axis]): default=True, ) - def axes(self) -> list: + def axes(self) -> list[Axis]: return [] - def calculate(self, bounds=True, nested=False) -> list[Frames[Axis]]: + def calculate( + self, bounds: bool = True, nested: bool = False + ) -> list[Frames[Axis]]: return [Frames({}, gap=np.full(self.num, self.gap))] @@ -206,10 +223,12 @@ class Zip(Spec[Axis]): description="The right-hand Spec to Zip, will appear later in axes" ) - def axes(self) -> list: + def axes(self) -> list[Axis]: return self.left.axes() + self.right.axes() - def calculate(self, bounds=True, nested=False) -> list[Frames[Axis]]: + def calculate( + self, bounds: bool = True, nested: bool = False + ) -> list[Frames[Axis]]: frames_left = self.left.calculate(bounds, nested) frames_right = self.right.calculate(bounds, nested) assert len(frames_left) >= len( @@ -234,7 +253,7 @@ def calculate(self, bounds=True, nested=False) -> list[Frames[Axis]]: padded_right += frames_right # type: ignore # Work through, zipping them together one by one - frames = [] + frames: list[Frames[Axis]] = [] for left, right in zip(frames_left, padded_right, strict=False): if right is None: combined = left @@ -274,10 +293,12 @@ class Mask(Spec[Axis]): default=True, ) - def axes(self) -> list: + def axes(self) -> list[Axis]: return self.spec.axes() - def calculate(self, bounds=True, nested=False) -> list[Frames[Axis]]: + def calculate( + self, bounds: bool = True, nested: bool = False + ) -> list[Frames[Axis]]: frames = self.spec.calculate(bounds, nested) for axis_set in self.region.axis_sets(): # Find the start and end index of any dimensions containing these axes @@ -292,7 +313,7 @@ def calculate(self, bounds=True, nested=False) -> list[Frames[Axis]]: squashed = squash_frames(frames[si : ei + 1], check_path_changes) frames = frames[:si] + [squashed] + frames[ei + 1 :] # Generate masks from the midpoints showing what's inside - masked_frames = [] + masked_frames: list[Frames[Axis]] = [] for f in frames: indices = get_mask(self.region, f.midpoints).nonzero()[0] masked_frames.append(f.extract(indices)) @@ -332,10 +353,12 @@ class Snake(Spec[Axis]): description="The Spec to run in reverse every other iteration" ) - def axes(self) -> list: + def axes(self) -> list[Axis]: return self.spec.axes() - def calculate(self, bounds=True, nested=False) -> list[Frames[Axis]]: + def calculate( + self, bounds: bool = True, nested: bool = False + ) -> list[Frames[Axis]]: return [ SnakedFrames.from_frames(segment) for segment in self.spec.calculate(bounds, nested) @@ -371,14 +394,16 @@ class Concat(Spec[Axis]): default=True, ) - def axes(self) -> list: + def axes(self) -> list[Axis]: left_axes, right_axes = self.left.axes(), self.right.axes() # Assuming the axes are the same, the order does not matter, we inherit the # order from the left-hand side. See also scanspec.core.concat. assert set(left_axes) == set(right_axes), f"axes {left_axes} != {right_axes}" return left_axes - def calculate(self, bounds=True, nested=False) -> list[Frames[Axis]]: + def calculate( + self, bounds: bool = True, nested: bool = False + ) -> list[Frames[Axis]]: dim_left = squash_frames( self.left.calculate(bounds, nested), nested and self.check_path_changes ) @@ -409,18 +434,20 @@ class Squash(Spec[Axis]): default=True, ) - def axes(self) -> list: + def axes(self) -> list[Axis]: return self.spec.axes() - def calculate(self, bounds=True, nested=False) -> list[Frames[Axis]]: + def calculate( + self, bounds: bool = True, nested: bool = False + ) -> list[Frames[Axis]]: dims = self.spec.calculate(bounds, nested) dim = squash_frames(dims, nested and self.check_path_changes) return [dim] def _dimensions_from_indexes( - func: Callable[[np.ndarray], dict[Axis, np.ndarray]], - axes: list, + func: Callable[[npt.NDArray[np.float64]], dict[Axis, npt.NDArray[np.float64]]], + axes: list[Axis], num: int, bounds: bool, ) -> list[Frames[Axis]]: @@ -461,10 +488,12 @@ class Line(Spec[Axis]): stop: float = Field(description="Midpoint of the last point of the line") num: int = Field(ge=1, description="Number of frames to produce") - def axes(self) -> list: + def axes(self) -> list[Axis]: return [self.axis] - def _line_from_indexes(self, indexes: np.ndarray) -> dict[Axis, np.ndarray]: + def _line_from_indexes( + self, indexes: npt.NDArray[np.float64] + ) -> dict[Axis, npt.NDArray[np.float64]]: if self.num == 1: # Only one point, stop-start gives length of one point step = self.stop - self.start @@ -476,19 +505,21 @@ def _line_from_indexes(self, indexes: np.ndarray) -> dict[Axis, np.ndarray]: first = self.start - step / 2 return {self.axis: indexes * step + first} - def calculate(self, bounds=True, nested=False) -> list[Frames[Axis]]: + def calculate( + self, bounds: bool = True, nested: bool = False + ) -> list[Frames[Axis]]: return _dimensions_from_indexes( self._line_from_indexes, self.axes(), self.num, bounds ) @classmethod def bounded( - cls, - axis: Axis = Field(description="An identifier for what to move"), + cls: type[Line[Any]], + axis: OtherAxis = Field(description="An identifier for what to move"), lower: float = Field(description="Lower bound of the first point of the line"), upper: float = Field(description="Upper bound of the last point of the line"), num: int = Field(ge=1, description="Number of frames to produce"), - ) -> Line[Axis]: + ) -> Line[OtherAxis]: """Specify a Line by extreme bounds instead of midpoints. .. example_spec:: @@ -533,7 +564,7 @@ class Static(Spec[Axis]): @classmethod def duration( - cls: type[Static[str]], + 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]: @@ -545,15 +576,19 @@ def duration( spec = Line("y", 1, 2, 3).zip(Static.duration(0.1)) """ - return cls(DURATION, duration, num) + return Static(DURATION, duration, num) - def axes(self) -> list: + def axes(self) -> list[Axis]: return [self.axis] - def _repeats_from_indexes(self, indexes: np.ndarray) -> dict[Axis, np.ndarray]: + 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(self, bounds=True, nested=False) -> list[Frames[Axis]]: + def calculate( + self, bounds: bool = True, nested: bool = False + ) -> list[Frames[Axis]]: return _dimensions_from_indexes( self._repeats_from_indexes, self.axes(), self.num, bounds ) @@ -593,7 +628,9 @@ def axes(self) -> list[Axis]: # TODO: reversed from __init__ args, a good idea? return [self.y_axis, self.x_axis] - def _spiral_from_indexes(self, indexes: np.ndarray) -> dict[Axis, np.ndarray]: + def _spiral_from_indexes( + self, indexes: npt.NDArray[np.float64] + ) -> dict[Axis, npt.NDArray[np.float64]]: # simplest spiral equation: r = phi # we want point spacing across area to be the same as between rings # so: sqrt(area / num) = ring_spacing @@ -610,16 +647,18 @@ def _spiral_from_indexes(self, indexes: np.ndarray) -> dict[Axis, np.ndarray]: self.x_axis: self.x_start + x_scale * phi * np.sin(phi + self.rotate), } - def calculate(self, bounds=True, nested=False) -> list[Frames[Axis]]: + def calculate( + self, bounds: bool = True, nested: bool = False + ) -> list[Frames[Axis]]: return _dimensions_from_indexes( self._spiral_from_indexes, self.axes(), self.num, bounds ) @classmethod def spaced( - cls, - x_axis: Axis = Field(description="An identifier for what to move for x"), - y_axis: Axis = Field(description="An identifier for what to move for y"), + cls: type[Spiral[Any]], + x_axis: OtherAxis = Field(description="An identifier for what to move for x"), + y_axis: OtherAxis = Field(description="An identifier for what to move for y"), x_start: float = Field(description="x centre of the spiral"), y_start: float = Field(description="y centre of the spiral"), radius: float = Field(description="radius of the spiral"), @@ -627,7 +666,7 @@ def spaced( rotate: float = Field( description="How much to rotate the angle of the spiral", default=0.0 ), - ) -> Spiral[Axis]: + ) -> Spiral[OtherAxis]: """Specify a Spiral equally spaced in "x_axis" and "y_axis". .. example_spec:: @@ -643,14 +682,21 @@ def spaced( n_rings = radius / dr num = int(n_rings**2 * np.pi) return cls( - x_axis, y_axis, x_start, y_start, radius * 2, radius * 2, num, rotate + x_axis, + y_axis, + x_start, + y_start, + radius * 2, + radius * 2, + num, + rotate, ) Spiral.spaced = validate_call(Spiral.spaced) # type:ignore -def fly(spec: Spec[Axis], duration: float) -> Spec[Axis]: +def fly(spec: Spec[Axis], duration: float) -> Spec[Axis | str]: """Flyscan, zipping with fixed duration for every frame. Args: @@ -666,7 +712,7 @@ def fly(spec: Spec[Axis], duration: float) -> Spec[Axis]: return spec.zip(Static.duration(duration)) -def step(spec: Spec[Axis], duration: float, num: int = 1) -> Spec[Axis]: +def step(spec: Spec[Axis], duration: float, num: int = 1) -> Spec[Axis | str]: """Step scan, with num frames of given duration at each frame in the spec. Args: @@ -684,7 +730,7 @@ def step(spec: Spec[Axis], duration: float, num: int = 1) -> Spec[Axis]: return spec * Static.duration(duration, num) -def get_constant_duration(frames: list[Frames]) -> float | None: +def get_constant_duration(frames: list[Frames[Any]]) -> float | None: """ Returns the duration of a number of ScanSpec frames, if known and consistent. diff --git a/src/scanspec/sphinxext.py b/src/scanspec/sphinxext.py index 6a1e2630..812ecd30 100644 --- a/src/scanspec/sphinxext.py +++ b/src/scanspec/sphinxext.py @@ -1,7 +1,9 @@ from contextlib import contextmanager +from typing import Any, cast from docutils.statemachine import StringList from matplotlib.sphinxext import plot_directive +from sphinx.application import Sphinx from . import __version__ @@ -13,9 +15,18 @@ def always_create_figures(): This is needed even if source rst hasn't changed, as we often use example_spec from within docstrings """ - orig_f = plot_directive.out_of_date + + def always_true( + original: Any, + derived: Any, + includes: Any = None, + ) -> bool: + return True + + # Type ignored because we never manipulate this object + orig_f = plot_directive.out_of_date # type: ignore # Patch the plot directive so it thinks all sources are out of date - plot_directive.out_of_date = lambda *args, **kwargs: True + plot_directive.out_of_date = always_true try: yield finally: @@ -25,17 +36,17 @@ def always_create_figures(): class ExampleSpecDirective(plot_directive.PlotDirective): """Runs `plot_spec` on the ``spec`` definied in the content.""" - def run(self): + def run(self) -> Any: self.content = StringList( ["# Example Spec", "", "from scanspec.plot import plot_spec"] + [str(x) for x in self.content] + ["plot_spec(spec)"] ) with always_create_figures(): - return super().run() + return cast(Any, super().run()) -def setup(app): +def setup(app: Sphinx): """Setup this extension in sphinx.""" app.add_directive("example_spec", ExampleSpecDirective) diff --git a/tests/__init__.py b/tests/__init__.py index e69de29b..dceb31a2 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,27 @@ +from typing import Any + +import pytest + + +def approx( + expected: Any, + rel: float | None = None, + abs: float | None = None, + nan_ok: bool = False, +) -> Any: + """ + Temporary loosely typed wrapper around approx. + To be removed pending: + https://github.com/pytest-dev/pytest/issues/7469 + + Args: + expected: Expected value + rel: Relative tolerance. Defaults to None. + abs: Absolute tolerance. Defaults to None. + nan_ok: Permit nan. Defaults to False. + + Returns: + Any: Approximate comparator + """ + + return pytest.approx(expected, rel=rel, abs=abs, nan_ok=nan_ok) # type: ignore diff --git a/tests/test_basemodel.py b/tests/test_basemodel.py index 85d83741..6a43feea 100644 --- a/tests/test_basemodel.py +++ b/tests/test_basemodel.py @@ -8,7 +8,7 @@ @uses_tagged_union class Foo(BaseModel): - spec: Spec + spec: Spec[str] simple_foo = Foo(spec=Line("x", 1, 2, 5)) @@ -47,7 +47,7 @@ def test_schema_updates_with_new_values(): old_schema = TypeAdapter(Foo).json_schema() @dataclass(config=StrictConfig) - class Splat(Spec[str]): # NOSONAR + class Splat(Spec[str]): # type: ignore def axes(self) -> list[str]: return ["*"] diff --git a/tests/test_cli.py b/tests/test_cli.py index 37894ff0..421667b9 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -7,42 +7,70 @@ import matplotlib.pyplot as plt import numpy as np -import pytest +import numpy.typing as npt from click.testing import CliRunner +from matplotlib.lines import Line2D from matplotlib.patches import Rectangle from matplotlib.text import Annotation +from mpl_toolkits.mplot3d.art3d import Line3D # type: ignore from scanspec import __version__, cli -from scanspec.plot import _Arrow3D +from scanspec.plot import Arrow3D +from . import approx -def assert_min_max_2d(line, xmin, xmax, ymin, ymax, length=None): + +def assert_min_max_2d( + line: Line2D, + xmin: float, + xmax: float, + ymin: float, + ymax: float, + length: float | None = None, +): + line_data = cast(npt.NDArray[np.float64], line.get_data()) if length is not None: - assert len(line.get_data()[0]) == length - mins = np.min(line.get_data(), axis=1) - maxs = np.max(line.get_data(), axis=1) + assert len(line_data[0]) == length + mins = np.min(line_data, axis=1) + maxs = np.max(line_data, axis=1) assert list(zip(mins, maxs, strict=False)) == [ - pytest.approx([xmin, xmax]), - pytest.approx([ymin, ymax]), + approx([xmin, xmax]), + approx([ymin, ymax]), ] -def assert_min_max_3d(line, xmin, xmax, ymin, ymax, zmin, zmax, length=None): +def assert_min_max_3d( + line: Line3D, + xmin: float, + xmax: float, + ymin: float, + ymax: float, + zmin: float, + zmax: float, + length: float | None = None, +): + data_3d = cast(npt.NDArray[np.float64], line.get_data_3d()) if length is not None: - assert len(line.get_data_3d()[0]) == length - mins = np.min(line.get_data_3d(), axis=1) - maxs = np.max(line.get_data_3d(), axis=1) + assert len(data_3d[0]) == length + mins = np.min(data_3d, axis=1) + maxs = np.max(data_3d, axis=1) assert list(zip(mins, maxs, strict=False)) == [ - pytest.approx([xmin, xmax]), - pytest.approx([ymin, ymax]), - pytest.approx([zmin, zmax]), + approx([xmin, xmax]), + approx([ymin, ymax]), + approx([zmin, zmax]), ] -def assert_3d_arrow(artist, x, y, z): - assert artist._verts3d[0][1] == pytest.approx(x) - assert artist._verts3d[1][1] == pytest.approx(y) - assert artist._verts3d[2][1] == pytest.approx(z) +def assert_3d_arrow( + artist: Line3D, + x: float, + y: float, + z: float, +): + assert isinstance(artist, Arrow3D) + assert artist.verts3d[0][1] == approx(x) + assert artist.verts3d[1][1] == approx(y) + assert artist.verts3d[2][1] == approx(z) def test_plot_1D_line() -> None: @@ -90,7 +118,7 @@ def test_plot_1D_line_snake_repeat() -> None: texts = cast(list[Annotation], axes.texts) assert len(texts) == 2 assert tuple(texts[0].xy) == (1, 0) - assert tuple(texts[1].xy) == pytest.approx([2, 0]) + assert tuple(texts[1].xy) == approx([2, 0]) def test_plot_1D_step() -> None: @@ -141,7 +169,7 @@ def test_plot_2D_line() -> None: texts = cast(list[Annotation], axes.texts) assert len(texts) == 2 assert tuple(texts[0].xy) == (0.5, 2) - assert tuple(texts[1].xy) == pytest.approx([2.5, 3]) + assert tuple(texts[1].xy) == approx([2.5, 3]) def test_plot_2D_line_rect_region() -> None: @@ -187,7 +215,7 @@ def test_plot_3D_line() -> None: result = runner.invoke(cli.cli, ["plot", spec]) assert result.exit_code == 0 axes = plt.gcf().axes[0] - lines = axes.lines + lines = cast(list[Line3D], axes.lines) assert len(lines) == 13 # First grid # First row @@ -216,13 +244,16 @@ def test_plot_3D_line() -> None: # Arrows extra_artists = axes.get_children() - arrow_artists = list( - filter( - lambda artist: isinstance(artist, _Arrow3D) - and artist.get_visible() - and artist.get_in_layout(), - extra_artists, - ) + arrow_artists = cast( + list[Line3D], + list( + filter( + lambda artist: isinstance(artist, Arrow3D) + and artist.get_visible() + and artist.get_in_layout(), + extra_artists, + ) + ), ) assert len(arrow_artists) == 4 assert_3d_arrow(arrow_artists[0], 0.5, 2, 5) diff --git a/tests/test_errors.py b/tests/test_errors.py index a7ff923c..7dd23dda 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -1,3 +1,5 @@ +from typing import Any + import pytest from scanspec.regions import Region @@ -8,13 +10,14 @@ def test_not_implemented() -> None: with pytest.raises(NotImplementedError): Region().axis_sets() with pytest.raises(NotImplementedError): - Region().mask({}) + region: Region[Any] = Region() + region.mask({}) with pytest.raises(NotImplementedError): Spec().axes() with pytest.raises(NotImplementedError): Spec().calculate() with pytest.raises(TypeError): - Spec() * Region() + Spec() * Region() # type: ignore def test_non_snake_not_allowed_inside_snaking_dim() -> None: diff --git a/tests/test_iteration.py b/tests/test_iteration.py index f3e2e75c..be31a26d 100644 --- a/tests/test_iteration.py +++ b/tests/test_iteration.py @@ -1,8 +1,8 @@ -import pytest - from scanspec.core import Path from scanspec.specs import Line +from . import approx + def test_line_path() -> None: x = "x" @@ -11,7 +11,7 @@ def test_line_path() -> None: path = Path(dims) assert len(path) == 5 dim = path.consume() - assert dim.midpoints == {x: pytest.approx([0, 0.25, 0.5, 0.75, 1.0])} + assert dim.midpoints == {x: approx([0, 0.25, 0.5, 0.75, 1.0])} assert len(path) == 0 @@ -22,4 +22,4 @@ def test_line_midpoints() -> None: assert it.axes == [x] assert len(it) == 5 midpoints = [d[x] for d in it] - assert midpoints == pytest.approx([0, 0.25, 0.5, 0.75, 1.0]) + assert midpoints == approx([0, 0.25, 0.5, 0.75, 1.0]) diff --git a/tests/test_serialization.py b/tests/test_serialization.py index d10dc3bf..f2f73b70 100644 --- a/tests/test_serialization.py +++ b/tests/test_serialization.py @@ -171,7 +171,7 @@ def test_vanilla_serialization(): ), ) - adapter = TypeAdapter(Spec) + adapter = TypeAdapter(Spec[str]) serialized = adapter.dump_json(ob) deserialized = adapter.validate_json(serialized) assert deserialized == ob diff --git a/tests/test_specs.py b/tests/test_specs.py index ce348d95..6d44be06 100644 --- a/tests/test_specs.py +++ b/tests/test_specs.py @@ -21,19 +21,21 @@ step, ) +from . import approx + x, y, z = "x", "y", "z" -def ints(s): - return pytest.approx([int(t) for t in s]) +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: pytest.approx([1.0])} - assert dim.lower == {DURATION: pytest.approx([1.0])} - assert dim.upper == {DURATION: pytest.approx([1.0])} + 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, SnakedFrames) assert dim.gap == ints("0") @@ -41,9 +43,9 @@ def test_one_point_duration() -> None: def test_one_point_line() -> None: inst = Line(x, 0, 1, 1) (dim,) = inst.calculate() - assert dim.midpoints == {x: pytest.approx([0])} - assert dim.lower == {x: pytest.approx([-0.5])} - assert dim.upper == {x: pytest.approx([0.5])} + assert dim.midpoints == {x: approx([0])} + assert dim.lower == {x: approx([-0.5])} + assert dim.upper == {x: approx([0.5])} assert not isinstance(dim, SnakedFrames) assert dim.gap == ints("1") @@ -51,19 +53,17 @@ def test_one_point_line() -> None: def test_two_point_line() -> None: inst = Line(x, 0, 1, 2) (dim,) = inst.calculate() - assert dim.midpoints == {x: pytest.approx([0, 1])} - assert dim.lower == {x: pytest.approx([-0.5, 0.5])} - assert dim.upper == {x: pytest.approx([0.5, 1.5])} + 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])} assert dim.gap == ints("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: pytest.approx([0, 1])} - assert ( - dimt.midpoints == dimt.lower == dimt.upper == {DURATION: pytest.approx([0.1])} - ) + assert dimx.midpoints == dimx.lower == dimx.upper == {x: approx([0, 1])} + assert dimt.midpoints == dimt.lower == dimt.upper == {DURATION: approx([0.1])} assert inst.frames().gap == ints("11") @@ -71,16 +71,16 @@ def test_two_point_fly_line() -> None: inst = fly(Line(x, 0, 1, 2), 0.1) (dim,) = inst.calculate() assert dim.midpoints == { - x: pytest.approx([0, 1]), - DURATION: pytest.approx([0.1, 0.1]), + x: approx([0, 1]), + DURATION: approx([0.1, 0.1]), } assert dim.lower == { - x: pytest.approx([-0.5, 0.5]), - DURATION: pytest.approx([0.1, 0.1]), + x: approx([-0.5, 0.5]), + DURATION: approx([0.1, 0.1]), } assert dim.upper == { - x: pytest.approx([0.5, 1.5]), - DURATION: pytest.approx([0.1, 0.1]), + x: approx([0.5, 1.5]), + DURATION: approx([0.1, 0.1]), } assert dim.gap == ints("10") @@ -88,9 +88,9 @@ def test_two_point_fly_line() -> None: def test_many_point_line() -> None: inst = Line(x, 0, 1, 5) (dim,) = inst.calculate() - assert dim.midpoints == {x: pytest.approx([0, 0.25, 0.5, 0.75, 1])} - assert dim.lower == {x: pytest.approx([-0.125, 0.125, 0.375, 0.625, 0.875])} - assert dim.upper == {x: pytest.approx([0.125, 0.375, 0.625, 0.875, 1.125])} + 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])} assert dim.gap == ints("10000") @@ -108,28 +108,16 @@ def test_spiral() -> None: inst = Spiral(x, y, 0, 10, 5, 50, 10) (dim,) = inst.calculate() assert dim.midpoints == { - y: pytest.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: pytest.approx( - [0.3, -0.9, -0.7, 0.5, 1.5, 1.6, 0.7, -0.6, -1.8, -2.4], abs=0.1 - ), + 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), } assert dim.lower == { - y: pytest.approx( - [10.0, 2.7, 13.3, 23.5, 20.9, 8.7, -4.2, -10.8, -8.4, 1.6], abs=0.1 - ), - x: pytest.approx( - [0.0, -0.3, -1.0, -0.1, 1.1, 1.7, 1.3, 0.0, -1.2, -2.2], abs=0.1 - ), + y: approx([10.0, 2.7, 13.3, 23.5, 20.9, 8.7, -4.2, -10.8, -8.4, 1.6], abs=0.1), + x: approx([0.0, -0.3, -1.0, -0.1, 1.1, 1.7, 1.3, 0.0, -1.2, -2.2], abs=0.1), } assert dim.upper == { - y: pytest.approx( - [2.7, 13.3, 23.5, 20.9, 8.7, -4.2, -10.8, -8.4, 1.6, 15.3], abs=0.1 - ), - x: pytest.approx( - [-0.3, -1.0, -0.1, 1.1, 1.7, 1.3, 0.0, -1.2, -2.2, -2.4], abs=0.1 - ), + y: approx([2.7, 13.3, 23.5, 20.9, 8.7, -4.2, -10.8, -8.4, 1.6, 15.3], abs=0.1), + x: approx([-0.3, -1.0, -0.1, 1.1, 1.7, 1.3, 0.0, -1.2, -2.2, -2.4], abs=0.1), } assert not isinstance(dim, SnakedFrames) assert dim.gap == ints("1000000000") @@ -145,8 +133,8 @@ def test_zipped_lines() -> None: assert inst.axes() == [x, y] (dim,) = inst.calculate() assert dim.midpoints == { - x: pytest.approx([0, 0.25, 0.5, 0.75, 1]), - y: pytest.approx([1, 1.25, 1.5, 1.75, 2]), + x: approx([0, 0.25, 0.5, 0.75, 1]), + y: approx([1, 1.25, 1.5, 1.75, 2]), } assert dim.gap == ints("10000") @@ -158,16 +146,16 @@ def test_product_lines() -> None: assert len(dims) == 2 dim = Path(dims).consume() assert dim.midpoints == { - x: pytest.approx([0, 1, 0, 1, 0, 1]), - y: pytest.approx([1, 1, 1.5, 1.5, 2, 2]), + x: approx([0, 1, 0, 1, 0, 1]), + y: approx([1, 1, 1.5, 1.5, 2, 2]), } assert dim.lower == { - x: pytest.approx([-0.5, 0.5, -0.5, 0.5, -0.5, 0.5]), - y: pytest.approx([1, 1, 1.5, 1.5, 2, 2]), + x: approx([-0.5, 0.5, -0.5, 0.5, -0.5, 0.5]), + y: approx([1, 1, 1.5, 1.5, 2, 2]), } assert dim.upper == { - x: pytest.approx([0.5, 1.5, 0.5, 1.5, 0.5, 1.5]), - y: pytest.approx([1, 1, 1.5, 1.5, 2, 2]), + 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 dim.gap == ints("101010") @@ -177,11 +165,11 @@ def test_zipped_product_lines() -> None: assert inst.axes() == [y, x, z] dimy, dimxz = inst.calculate() assert dimxz.midpoints == { - x: pytest.approx([0, 0.25, 0.5, 0.75, 1]), - z: pytest.approx([2, 2.25, 2.5, 2.75, 3]), + x: approx([0, 0.25, 0.5, 0.75, 1]), + z: approx([2, 2.25, 2.5, 2.75, 3]), } assert dimy.midpoints == { - y: pytest.approx([1, 1.5, 2]), + y: approx([1, 1.5, 2]), } assert inst.frames().gap == ints("100001000010000") @@ -191,16 +179,16 @@ def test_squashed_product() -> None: assert inst.axes() == [y, x] (dim,) = inst.calculate() assert dim.midpoints == { - x: pytest.approx([0, 1, 0, 1, 0, 1]), - y: pytest.approx([1, 1, 1.5, 1.5, 2, 2]), + x: approx([0, 1, 0, 1, 0, 1]), + y: approx([1, 1, 1.5, 1.5, 2, 2]), } assert dim.lower == { - x: pytest.approx([-0.5, 0.5, -0.5, 0.5, -0.5, 0.5]), - y: pytest.approx([1, 1, 1.5, 1.5, 2, 2]), + x: approx([-0.5, 0.5, -0.5, 0.5, -0.5, 0.5]), + y: approx([1, 1, 1.5, 1.5, 2, 2]), } assert dim.upper == { - x: pytest.approx([0.5, 1.5, 0.5, 1.5, 0.5, 1.5]), - y: pytest.approx([1, 1, 1.5, 1.5, 2, 2]), + 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 dim.gap == ints("101010") @@ -213,11 +201,11 @@ def test_squashed_multiplied_snake_scan() -> None: dimz, dimxyt = inst.calculate() for d in dimxyt.midpoints, dimxyt.lower, dimxyt.upper: assert d == { - x: pytest.approx([4, 4, 6, 6, 6, 6, 4, 4]), - y: pytest.approx([1, 1, 1, 1, 2, 2, 2, 2]), - DURATION: pytest.approx([9, 9, 9, 9, 9, 9, 9, 9]), + 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: pytest.approx([1, 2])} + assert dimz.midpoints == dimz.lower == dimz.upper == {z: approx([1, 2])} assert inst.frames().gap == ints("1010101010101010") @@ -228,16 +216,16 @@ def test_product_snaking_lines() -> None: assert len(dims) == 2 dim = Path(dims).consume() assert dim.midpoints == { - x: pytest.approx([0, 1, 1, 0, 0, 1]), - y: pytest.approx([1, 1, 1.5, 1.5, 2, 2]), + x: approx([0, 1, 1, 0, 0, 1]), + y: approx([1, 1, 1.5, 1.5, 2, 2]), } assert dim.lower == { - x: pytest.approx([-0.5, 0.5, 1.5, 0.5, -0.5, 0.5]), - y: pytest.approx([1, 1, 1.5, 1.5, 2, 2]), + x: approx([-0.5, 0.5, 1.5, 0.5, -0.5, 0.5]), + y: approx([1, 1, 1.5, 1.5, 2, 2]), } assert dim.upper == { - x: pytest.approx([0.5, 1.5, 0.5, -0.5, 0.5, 1.5]), - y: pytest.approx([1, 1, 1.5, 1.5, 2, 2]), + x: approx([0.5, 1.5, 0.5, -0.5, 0.5, 1.5]), + y: approx([1, 1, 1.5, 1.5, 2, 2]), } assert dim.gap == ints("101010") @@ -246,9 +234,9 @@ def test_concat_lines() -> None: inst = Concat(Line(x, 0, 1, 2), Line(x, 1, 2, 3)) assert inst.axes() == [x] (dim,) = inst.calculate() - assert dim.midpoints == {x: pytest.approx([0, 1, 1, 1.5, 2])} - assert dim.lower == {x: pytest.approx([-0.5, 0.5, 0.75, 1.25, 1.75])} - assert dim.upper == {x: pytest.approx([0.5, 1.5, 1.25, 1.75, 2.25])} + 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])} assert dim.gap == ints("10100") @@ -257,16 +245,16 @@ def test_rect_region() -> None: assert inst.axes() == [y, x] (dim,) = inst.calculate() assert dim.midpoints == { - x: pytest.approx([0, 1, 0, 1, 0, 1]), - y: pytest.approx([1, 1, 1.5, 1.5, 2, 2]), + x: approx([0, 1, 0, 1, 0, 1]), + y: approx([1, 1, 1.5, 1.5, 2, 2]), } assert dim.lower == { - x: pytest.approx([-0.5, 0.5, -0.5, 0.5, -0.5, 0.5]), - y: pytest.approx([1, 1, 1.5, 1.5, 2, 2]), + x: approx([-0.5, 0.5, -0.5, 0.5, -0.5, 0.5]), + y: approx([1, 1, 1.5, 1.5, 2, 2]), } assert dim.upper == { - x: pytest.approx([0.5, 1.5, 0.5, 1.5, 0.5, 1.5]), - y: pytest.approx([1, 1, 1.5, 1.5, 2, 2]), + 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 dim.gap == ints("101010") @@ -277,20 +265,20 @@ def test_rect_region_3D() -> None: ) assert inst.axes() == [z, y, x] zdim, xydim = inst.calculate() - assert zdim.midpoints == {z: pytest.approx([3.2, 3.2])} + assert zdim.midpoints == {z: approx([3.2, 3.2])} assert zdim.midpoints is zdim.upper assert zdim.midpoints is zdim.lower assert xydim.midpoints == { - x: pytest.approx([0, 1, 0, 1, 0, 1]), - y: pytest.approx([1, 1, 1.5, 1.5, 2, 2]), + x: approx([0, 1, 0, 1, 0, 1]), + y: approx([1, 1, 1.5, 1.5, 2, 2]), } assert xydim.lower == { - x: pytest.approx([-0.5, 0.5, -0.5, 0.5, -0.5, 0.5]), - y: pytest.approx([1, 1, 1.5, 1.5, 2, 2]), + x: approx([-0.5, 0.5, -0.5, 0.5, -0.5, 0.5]), + y: approx([1, 1, 1.5, 1.5, 2, 2]), } assert xydim.upper == { - x: pytest.approx([0.5, 1.5, 0.5, 1.5, 0.5, 1.5]), - y: pytest.approx([1, 1, 1.5, 1.5, 2, 2]), + 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") @@ -302,8 +290,8 @@ def test_rect_region_union() -> None: assert inst.axes() == [y, x] (dim,) = inst.calculate() assert dim.midpoints == { - x: pytest.approx([0, 1, 0, 1, 2, 0, 1, 2, 1, 2]), - y: pytest.approx([1, 1, 1.5, 1.5, 1.5, 2, 2, 2, 2.5, 2.5]), + 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]), } assert dim.gap == ints("1010010010") @@ -317,8 +305,8 @@ def test_rect_region_intersection() -> None: assert inst.axes() == [y, x] (dim,) = inst.calculate() assert dim.midpoints == { - x: pytest.approx([1, 1]), - y: pytest.approx([1.5, 2]), + x: approx([1, 1]), + y: approx([1.5, 2]), } assert dim.gap == ints("11") @@ -332,9 +320,9 @@ def test_rect_region_difference() -> None: assert inst.axes() == [y, x, DURATION] (dim,) = inst.calculate() assert dim.midpoints == { - x: pytest.approx([0, 1, 0, 0]), - y: pytest.approx([1, 1, 1.5, 2]), - DURATION: pytest.approx([0.1, 0.1, 0.1, 0.1]), + 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.gap == ints("1011") @@ -346,8 +334,8 @@ def test_rect_region_symmetricdifference() -> None: assert inst.axes() == [y, x] (dim,) = inst.calculate() assert dim.midpoints == { - x: pytest.approx([0, 1, 0, 2, 0, 2, 1, 2]), - y: pytest.approx([1, 1, 1.5, 1.5, 2, 2, 2.5, 2.5]), + x: approx([0, 1, 0, 2, 0, 2, 1, 2]), + y: approx([1, 1, 1.5, 1.5, 2, 2, 2.5, 2.5]), } assert dim.gap == ints("10111110") @@ -357,16 +345,16 @@ def test_circle_region() -> None: assert inst.axes() == [y, x] (dim,) = inst.calculate() assert dim.midpoints == { - x: pytest.approx([1, 0, 1, 2, 1]), - y: pytest.approx([1, 2, 2, 2, 3]), + x: approx([1, 0, 1, 2, 1]), + y: approx([1, 2, 2, 2, 3]), } assert dim.lower == { - x: pytest.approx([0.5, -0.5, 0.5, 1.5, 0.5]), - y: pytest.approx([1, 2, 2, 2, 3]), + x: approx([0.5, -0.5, 0.5, 1.5, 0.5]), + y: approx([1, 2, 2, 2, 3]), } assert dim.upper == { - x: pytest.approx([1.5, 0.5, 1.5, 2.5, 1.5]), - y: pytest.approx([1, 2, 2, 2, 3]), + x: approx([1.5, 0.5, 1.5, 2.5, 1.5]), + y: approx([1, 2, 2, 2, 3]), } assert dim.gap == ints("11001") @@ -380,16 +368,16 @@ def test_circle_snaked_region() -> None: assert inst.axes() == [y, x] (dim,) = inst.calculate() assert dim.midpoints == { - x: pytest.approx([1, 2, 1, 0, 1]), - y: pytest.approx([1, 2, 2, 2, 3]), + x: approx([1, 2, 1, 0, 1]), + y: approx([1, 2, 2, 2, 3]), } assert dim.lower == { - x: pytest.approx([0.5, 2.5, 1.5, 0.5, 0.5]), - y: pytest.approx([1, 2, 2, 2, 3]), + x: approx([0.5, 2.5, 1.5, 0.5, 0.5]), + y: approx([1, 2, 2, 2, 3]), } assert dim.upper == { - x: pytest.approx([1.5, 1.5, 0.5, -0.5, 1.5]), - y: pytest.approx([1, 2, 2, 2, 3]), + x: approx([1.5, 1.5, 0.5, -0.5, 1.5]), + y: approx([1, 2, 2, 2, 3]), } assert dim.gap == ints("11001") @@ -399,16 +387,16 @@ def test_ellipse_region() -> None: assert inst.axes() == [y, x] (dim,) = inst.calculate() assert dim.midpoints == { - x: pytest.approx([0, 1, 0, 1, 2, 1, 2]), - y: pytest.approx([1, 1, 2, 2, 2, 3, 3]), + x: approx([0, 1, 0, 1, 2, 1, 2]), + y: approx([1, 1, 2, 2, 2, 3, 3]), } assert dim.lower == { - x: pytest.approx([-0.5, 0.5, -0.5, 0.5, 1.5, 0.5, 1.5]), - y: pytest.approx([1, 1, 2, 2, 2, 3, 3]), + x: approx([-0.5, 0.5, -0.5, 0.5, 1.5, 0.5, 1.5]), + y: approx([1, 1, 2, 2, 2, 3, 3]), } assert dim.upper == { - x: pytest.approx([0.5, 1.5, 0.5, 1.5, 2.5, 1.5, 2.5]), - y: pytest.approx([1, 1, 2, 2, 2, 3, 3]), + x: approx([0.5, 1.5, 0.5, 1.5, 2.5, 1.5, 2.5]), + y: approx([1, 1, 2, 2, 2, 3, 3]), } assert dim.gap == ints("1010010") @@ -420,16 +408,16 @@ def test_polygon_region() -> None: assert inst.axes() == [y, x] (dim,) = inst.calculate() assert dim.midpoints == { - x: pytest.approx([1, 2, 1, 2, 3, 1, 2, 3]), - y: pytest.approx([1, 1, 2, 2, 2, 3, 3, 3]), + x: approx([1, 2, 1, 2, 3, 1, 2, 3]), + y: approx([1, 1, 2, 2, 2, 3, 3, 3]), } assert dim.lower == { - x: pytest.approx([0.5, 1.5, 0.5, 1.5, 2.5, 0.5, 1.5, 2.5]), - y: pytest.approx([1, 1, 2, 2, 2, 3, 3, 3]), + x: approx([0.5, 1.5, 0.5, 1.5, 2.5, 0.5, 1.5, 2.5]), + y: approx([1, 1, 2, 2, 2, 3, 3, 3]), } assert dim.upper == { - x: pytest.approx([1.5, 2.5, 1.5, 2.5, 3.5, 1.5, 2.5, 3.5]), - y: pytest.approx([1, 1, 2, 2, 2, 3, 3, 3]), + x: approx([1.5, 2.5, 1.5, 2.5, 3.5, 1.5, 2.5, 3.5]), + y: approx([1, 1, 2, 2, 2, 3, 3, 3]), } assert dim.gap == ints("10100100") @@ -443,12 +431,12 @@ def test_xyz_stack() -> None: assert dim.lower == { z: ints("000000000000111111111111"), y: ints("000011112222222211110000"), - x: pytest.approx([-0.5, 0.5, 1.5, 2.5, 3.5, 2.5, 1.5, 0.5] * 3), + x: approx([-0.5, 0.5, 1.5, 2.5, 3.5, 2.5, 1.5, 0.5] * 3), } assert dim.upper == { z: ints("000000000000111111111111"), y: ints("000011112222222211110000"), - x: pytest.approx([0.5, 1.5, 2.5, 3.5, 2.5, 1.5, 0.5, -0.5] * 3), + x: approx([0.5, 1.5, 2.5, 3.5, 2.5, 1.5, 0.5, -0.5] * 3), } assert dim.midpoints == { z: ints("000000000000111111111111"), @@ -470,34 +458,34 @@ def test_xyz_stack() -> None: 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 = 10 * ~Line.bounded(x, 11, 19, 1) + spec: Spec[str] = 10 * ~Line.bounded(x, 11, 19, 1) dim = spec.frames() assert len(dim) == 10 - assert dim.lower == {x: pytest.approx([11, 19, 11, 19, 11, 19, 11, 19, 11, 19])} - assert dim.upper == {x: pytest.approx([19, 11, 19, 11, 19, 11, 19, 11, 19, 11])} - assert dim.midpoints == {x: pytest.approx([15, 15, 15, 15, 15, 15, 15, 15, 15, 15])} + 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("1111111111") def test_gap_repeat() -> None: # Check that no gap propogates to dim.gap for snaked axis - spec: Spec[Any] = Repeat(10, gap=False) * ~Line.bounded(x, 11, 19, 1) + spec: Spec[str] = Repeat(10, gap=False) * ~Line.bounded(x, 11, 19, 1) dim = spec.frames() assert len(dim) == 10 - assert dim.lower == {x: pytest.approx([11, 19, 11, 19, 11, 19, 11, 19, 11, 19])} - assert dim.upper == {x: pytest.approx([19, 11, 19, 11, 19, 11, 19, 11, 19, 11])} - assert dim.midpoints == {x: pytest.approx([15, 15, 15, 15, 15, 15, 15, 15, 15, 15])} + 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[Any] = Repeat(3, gap=False) * Line.bounded(x, 11, 19, 1) + spec: Spec[str] = Repeat(3, gap=False) * Line.bounded(x, 11, 19, 1) dim = spec.frames() assert len(dim) == 3 - assert dim.lower == {x: pytest.approx([11, 11, 11])} - assert dim.upper == {x: pytest.approx([19, 19, 19])} - assert dim.midpoints == {x: pytest.approx([15, 15, 15])} + 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") @@ -558,7 +546,7 @@ def test_multiple_statics_with_grid(): ), ], ) -def test_shape(spec: Spec, expected_shape: tuple[int, ...]): +def test_shape(spec: Spec[Any], expected_shape: tuple[int, ...]): assert expected_shape == spec.shape()