From f49320c078142c0f14cbd847adf0c7bb84d515ac Mon Sep 17 00:00:00 2001 From: Zoheb Shaikh Date: Wed, 7 Aug 2024 11:59:10 +0100 Subject: [PATCH] move to pydantic v2 (#127) Refactored discriminated union of subclasses --- pyproject.toml | 8 +- schema.json | 3806 ++++++++++++++++++++++++++++++++++- src/scanspec/core.py | 216 +- src/scanspec/plot.py | 3 +- src/scanspec/regions.py | 31 +- src/scanspec/service.py | 9 +- src/scanspec/specs.py | 42 +- tests/test_cli.py | 5 +- tests/test_serialization.py | 16 +- 9 files changed, 4029 insertions(+), 107 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2d064382..38c13f8f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,11 @@ classifiers = [ "Programming Language :: Python :: 3.11", ] description = "Specify step and flyscan paths in a serializable, efficient and Pythonic way" -dependencies = ["numpy>=2", "click>=8.1", "pydantic<2.0", "httpx==0.26.0"] +dependencies = [ + "numpy>=2", + "click>=8.1", + "pydantic>=2.0", +] dynamic = ["version"] license.file = "LICENSE" readme = "README.md" @@ -21,7 +25,7 @@ requires-python = ">=3.10" # Plotting plotting = ["scipy", "matplotlib"] # REST service support -service = ["fastapi==0.99", "uvicorn"] +service = ["fastapi>=0.100.0", "uvicorn"] # For development tests/docs dev = [ # This syntax is supported since pip 21.2 diff --git a/schema.json b/schema.json index 156e3d8b..e35c026a 100644 --- a/schema.json +++ b/schema.json @@ -1 +1,3805 @@ -{"openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "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": {"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": {"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": {"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."}, "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"}, "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": {"title": "Spec", "description": "The spec from which to generate points"}, "max_frames": {"type": "integer", "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."}, "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."}, "ValidResponse": {"properties": {"input_spec": {"title": "Input Spec", "description": "The input scanspec"}, "valid_spec": {"title": "Valid Spec", "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"}}}} +{ + "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": { + "anyOf": [ + { + "$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" + } + ], + "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": { + "anyOf": [ + { + "$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" + } + ], + "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": { + "anyOf": [ + { + "$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" + } + ], + "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": { + "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" + } + ], + "title": "Left", + "description": "The left-hand Region to combine", + "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" + } + } + }, + "right": { + "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" + } + ], + "title": "Right", + "description": "The right-hand Region to combine", + "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" + } + } + }, + "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": { + "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" + } + ], + "title": "Left", + "description": "The left-hand Region to combine", + "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" + } + } + }, + "right": { + "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" + } + ], + "title": "Right", + "description": "The right-hand Region to combine", + "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" + } + } + }, + "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": { + "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" + } + ], + "title": "Left", + "description": "The left-hand Spec to Concat, midpoints will appear earlier", + "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" + } + } + }, + "right": { + "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" + } + ], + "title": "Right", + "description": "The right-hand Spec to Concat, midpoints will appear later", + "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" + } + } + }, + "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": { + "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" + } + ], + "title": "Left", + "description": "The left-hand Spec to Concat, midpoints will appear earlier", + "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" + } + } + }, + "right": { + "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" + } + ], + "title": "Right", + "description": "The right-hand Spec to Concat, midpoints will appear later", + "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" + } + } + }, + "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": { + "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" + } + ], + "title": "Left", + "description": "The left-hand Region to combine", + "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" + } + } + }, + "right": { + "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" + } + ], + "title": "Right", + "description": "The right-hand Region to combine", + "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" + } + } + }, + "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": { + "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" + } + ], + "title": "Left", + "description": "The left-hand Region to combine", + "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" + } + } + }, + "right": { + "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" + } + ], + "title": "Right", + "description": "The right-hand Region to combine", + "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" + } + } + }, + "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": { + "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" + } + ], + "title": "Left", + "description": "The left-hand Region to combine", + "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" + } + } + }, + "right": { + "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" + } + ], + "title": "Right", + "description": "The right-hand Region to combine", + "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" + } + } + }, + "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": { + "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" + } + ], + "title": "Left", + "description": "The left-hand Region to combine", + "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" + } + } + }, + "right": { + "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" + } + ], + "title": "Right", + "description": "The right-hand Region to combine", + "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" + } + } + }, + "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": { + "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" + } + ], + "title": "Spec", + "description": "The Spec containing the source midpoints", + "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" + } + } + }, + "region": { + "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" + } + ], + "title": "Region", + "description": "The Region that midpoints will be inside", + "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" + } + } + }, + "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": { + "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" + } + ], + "title": "Spec", + "description": "The Spec containing the source midpoints", + "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" + } + } + }, + "region": { + "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" + } + ], + "title": "Region", + "description": "The Region that midpoints will be inside", + "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" + } + } + }, + "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": { + "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" + } + ], + "title": "Spec", + "description": "The spec from which to generate points", + "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" + } + } + }, + "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": { + "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" + } + ], + "title": "Outer", + "description": "Will be executed once", + "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" + } + } + }, + "inner": { + "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" + } + ], + "title": "Inner", + "description": "Will be executed len(outer) times", + "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" + } + } + }, + "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": { + "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" + } + ], + "title": "Outer", + "description": "Will be executed once", + "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" + } + } + }, + "inner": { + "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" + } + ], + "title": "Inner", + "description": "Will be executed len(outer) times", + "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" + } + } + }, + "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)" + }, + "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": { + "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" + } + ], + "title": "Spec", + "description": "The Spec to run in reverse every other iteration", + "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" + } + } + }, + "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": { + "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" + } + ], + "title": "Spec", + "description": "The Spec to run in reverse every other iteration", + "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" + } + } + }, + "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)" + }, + "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": { + "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" + } + ], + "title": "Spec", + "description": "The Spec to squash the dimensions of", + "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" + } + } + }, + "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": { + "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" + } + ], + "title": "Spec", + "description": "The Spec to squash the dimensions of", + "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" + } + } + }, + "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": { + "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" + } + ], + "title": "Left", + "description": "The left-hand Region to combine", + "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" + } + } + }, + "right": { + "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" + } + ], + "title": "Right", + "description": "The right-hand Region to combine", + "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" + } + } + }, + "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": { + "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" + } + ], + "title": "Left", + "description": "The left-hand Region to combine", + "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" + } + } + }, + "right": { + "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" + } + ], + "title": "Right", + "description": "The right-hand Region to combine", + "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" + } + } + }, + "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": { + "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" + } + ], + "title": "Left", + "description": "The left-hand Region to combine", + "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" + } + } + }, + "right": { + "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" + } + ], + "title": "Right", + "description": "The right-hand Region to combine", + "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" + } + } + }, + "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": { + "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" + } + ], + "title": "Left", + "description": "The left-hand Region to combine", + "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" + } + } + }, + "right": { + "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" + } + ], + "title": "Right", + "description": "The right-hand Region to combine", + "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" + } + } + }, + "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": { + "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" + } + ], + "title": "Input Spec", + "description": "The input scanspec", + "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" + } + } + }, + "valid_spec": { + "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" + } + ], + "title": "Valid Spec", + "description": "The validated version of the spec", + "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" + } + } + } + }, + "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": { + "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" + } + ], + "title": "Left", + "description": "The left-hand Spec to Zip, will appear earlier in axes", + "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" + } + } + }, + "right": { + "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" + } + ], + "title": "Right", + "description": "The right-hand Spec to Zip, will appear later in axes", + "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" + } + } + }, + "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": { + "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" + } + ], + "title": "Left", + "description": "The left-hand Spec to Zip, will appear earlier in axes", + "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" + } + } + }, + "right": { + "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" + } + ], + "title": "Right", + "description": "The right-hand Spec to Zip, will appear later in axes", + "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" + } + } + }, + "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 diff --git a/src/scanspec/core.py b/src/scanspec/core.py index ca162565..39f2a57a 100644 --- a/src/scanspec/core.py +++ b/src/scanspec/core.py @@ -1,12 +1,28 @@ from __future__ import annotations +import dataclasses from collections.abc import Callable, Iterable, Iterator, Sequence -from dataclasses import field -from typing import Any, Generic, Literal, TypeVar, Union +from functools import partial +from inspect import isclass +from typing import ( + Any, + Generic, + Literal, + TypeVar, + Union, + get_origin, + get_type_hints, +) import numpy as np -from pydantic import BaseConfig, Extra, Field, ValidationError, create_model -from pydantic.error_wrappers import ErrorWrapper +from pydantic import ( + ConfigDict, + Field, + GetCoreSchemaHandler, + TypeAdapter, +) +from pydantic.dataclasses import rebuild_dataclass +from pydantic.fields import FieldInfo __all__ = [ "if_instance_do", @@ -23,24 +39,18 @@ ] -class StrictConfig(BaseConfig): - """Pydantic configuration for scanspecs and regions.""" - - extra: Extra = Extra.forbid +StrictConfig: ConfigDict = {"extra": "forbid"} def discriminated_union_of_subclasses( - super_cls: type | None = None, - *, + cls, discriminator: str = "type", - config: type[BaseConfig] | None = None, -) -> type | Callable[[type], type]: +): """Add all subclasses of super_cls to a discriminated union. For all subclasses of super_cls, add a discriminator field to identify the type. Raw JSON should look like {"type": , params for ...}. - Add validation methods to super_cls so it can be parsed by pydantic.parse_obj_as. Example:: @@ -104,72 +114,124 @@ def calculate(self) -> int: Type | Callable[[Type], Type]: A decorator that adds the necessary functionality to a class. """ + tagged_union = _TaggedUnion(cls, discriminator) + _tagged_unions[cls] = tagged_union + cls.__init_subclass__ = classmethod(partial(__init_subclass__, discriminator)) + cls.__get_pydantic_core_schema__ = classmethod( + partial(__get_pydantic_core_schema__, tagged_union=tagged_union) + ) + return cls - def wrap(cls): - return _discriminated_union_of_subclasses(cls, discriminator, config) - # Work out if the call was @discriminated_union_of_subclasses or - # @discriminated_union_of_subclasses(...) - if super_cls is None: - return wrap - else: - return wrap(super_cls) - - -def _discriminated_union_of_subclasses( - super_cls: type, - discriminator: str, - config: type[BaseConfig] | None = None, -) -> type | Callable[[type], type]: - super_cls._ref_classes = set() # type: ignore - super_cls._model = None # type: ignore - - def __init_subclass__(cls) -> None: - # Keep track of inherting classes in super class - cls._ref_classes.add(cls) - - # Add a discriminator field to the class so it can - # be identified when deserailizing. - cls.__annotations__ = { - **cls.__annotations__, - discriminator: Literal[cls.__name__], - } - setattr(cls, discriminator, field(default=cls.__name__, repr=False)) - - def __get_validators__(cls) -> Any: - yield cls.__validate__ - - def __validate__(cls, v: Any) -> Any: - # Lazily initialize model on first use because this - # needs to be done once, after all subclasses have been - # declared - if cls._model is None: - root = Union[tuple(cls._ref_classes)] # type: ignore # noqa - cls._model = create_model( - super_cls.__name__, - __root__=(root, Field(..., discriminator=discriminator)), - __config__=config, - ) - - try: - return cls._model(__root__=v).__root__ - except ValidationError as e: - for ( - error - ) in e.raw_errors: # need in to remove redundant __root__ from error path - if ( - isinstance(error, ErrorWrapper) - and error.loc_tuple()[0] == "__root__" - ): - error._loc = error.loc_tuple()[1:] - - raise e - - # Inject magic methods into super_cls - for method in __init_subclass__, __get_validators__, __validate__: - setattr(super_cls, method.__name__, classmethod(method)) # type: ignore - - return super_cls +T = TypeVar("T", type, Callable) + + +def deserialize_as(cls, obj): + return _tagged_unions[cls].type_adapter.validate_python(obj) + + +def uses_tagged_union(cls_or_func: T) -> T: + """ + Decorator that processes the type hints of a class or function to detect and + register any tagged unions. If a tagged union is detected in the type hints, + it registers the class or function as a referrer to that tagged union. + Args: + cls_or_func (T): The class or function to be processed for tagged unions. + Returns: + T: The original class or function, unmodified. + """ + for k, v in get_type_hints(cls_or_func).items(): + tagged_union = _tagged_unions.get(get_origin(v) or v, None) + if tagged_union: + tagged_union.add_referrer(cls_or_func, k) + return cls_or_func + + +class _TaggedUnion: + def __init__(self, base_class: type, discriminator: str): + self._base_class = base_class + # The members of the tagged union, i.e. subclasses of the baseclasses + self._members: list[type] = [] + # Classes and their field names that refer to this tagged union + self._referrers: dict[type | Callable, set[str]] = {} + self.type_adapter: TypeAdapter = TypeAdapter(None) + self._discriminator = discriminator + + def _make_union(self): + if len(self._members) > 0: + return Union[tuple(self._members)] # type: ignore # noqa + + def _set_discriminator(self, cls: type | Callable, field_name: str, field: Any): + # Set the field to use the `type` discriminator on deserialize + # https://docs.pydantic.dev/2.8/concepts/unions/#discriminated-unions-with-str-discriminators + if isclass(cls): + assert isinstance( + field, FieldInfo + ), f"Expected {cls.__name__}.{field_name} to be a Pydantic field, not {field!r}" # noqa: E501 + field.discriminator = self._discriminator + + def add_member(self, cls: type): + if cls in self._members: + # A side effect of hooking to __get_pydantic_core_schema__ is that it is + # called muliple times for the same member, do no process if it wouldn't + # change the member list + return + + self._members.append(cls) + union = self._make_union() + if union: + # There are more than 1 subclasses in the union, so set all the referrers + # to use this union + for referrer, fields in self._referrers.items(): + if isclass(referrer): + for field in dataclasses.fields(referrer): + if field.name in fields: + field.type = union + self._set_discriminator(referrer, field.name, field.default) + rebuild_dataclass(referrer, force=True) + # Make a type adapter for use in deserialization + self.type_adapter = TypeAdapter(union) + + def add_referrer(self, cls: type | Callable, attr_name: str): + self._referrers.setdefault(cls, set()).add(attr_name) + union = self._make_union() + if union: + # There are more than 1 subclasses in the union, so set the referrer + # (which is currently being constructed) to use it + # note that we use annotations as the class has not been turned into + # a dataclass yet + cls.__annotations__[attr_name] = union + self._set_discriminator(cls, attr_name, getattr(cls, attr_name, None)) + + +_tagged_unions: dict[type, _TaggedUnion] = {} + + +def __init_subclass__(discriminator: str, cls: type): + # Add a discriminator field to the class so it can + # be identified when deserailizing, and make sure it is last in the list + cls.__annotations__ = { + **cls.__annotations__, + discriminator: Literal[cls.__name__], # type: ignore + } + cls.type = Field(cls.__name__, repr=False) # type: ignore + # Replace any bare annotation with a discriminated union of subclasses + # and register this class as one that refers to that union so it can be updated + for k, v in get_type_hints(cls).items(): + # This works for Expression[T] or Expression + tagged_union = _tagged_unions.get(get_origin(v) or v, None) + if tagged_union: + tagged_union.add_referrer(cls, k) + + +def __get_pydantic_core_schema__( + cls, source_type: Any, handler: GetCoreSchemaHandler, tagged_union: _TaggedUnion +): + # Rebuild any dataclass (including this one) that references this union + # Note that this has to be done after the creation of the dataclass so that + # previously created classes can refer to this newly created class + tagged_union.add_member(cls) + return handler(source_type) def if_instance_do(x: Any, cls: type, func: Callable): diff --git a/src/scanspec/plot.py b/src/scanspec/plot.py index 43311663..0ee4419f 100644 --- a/src/scanspec/plot.py +++ b/src/scanspec/plot.py @@ -8,7 +8,7 @@ from mpl_toolkits.mplot3d import Axes3D, proj3d from scipy import interpolate -from .core import Path +from .core import Path, uses_tagged_union from .regions import Circle, Ellipse, Polygon, Rectangle, Region, find_regions from .specs import DURATION, Spec @@ -86,6 +86,7 @@ def _plot_spline(axes, ranges, arrays: list[np.ndarray], index_colours: dict[int yield unscaled_splines +@uses_tagged_union def plot_spec(spec: Spec[Any], title: str | None = None): """Plot a spec, drawing the path taken through the scan. diff --git a/src/scanspec/regions.py b/src/scanspec/regions.py index 201a8a4b..0c9b0b1f 100644 --- a/src/scanspec/regions.py +++ b/src/scanspec/regions.py @@ -1,7 +1,8 @@ from __future__ import annotations -from collections.abc import Iterator -from typing import Generic +from collections.abc import Iterator, Mapping +from dataclasses import asdict, is_dataclass +from typing import Any, Generic import numpy as np from pydantic import BaseModel, Field @@ -11,6 +12,7 @@ AxesPoints, Axis, StrictConfig, + deserialize_as, discriminated_union_of_subclasses, if_instance_do, ) @@ -64,6 +66,15 @@ def __sub__(self, other) -> DifferenceOf[Axis]: def __xor__(self, other) -> 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 asdict(self) # type: ignore + + @staticmethod + def deserialize(obj): + """Deserialize the Region from a dictionary.""" + return deserialize_as(Region, obj) + def get_mask(region: Region[Axis], points: AxesPoints[Axis]) -> np.ndarray: """Return a mask of the points inside the region. @@ -254,10 +265,10 @@ class Polygon(Region[Axis]): x_axis: Axis = Field(description="The name matching the x axis of the spec") y_axis: Axis = Field(description="The name matching the y axis of the spec") x_verts: list[float] = Field( - description="The Nx1 x coordinates of the polygons vertices", min_len=3 + description="The Nx1 x coordinates of the polygons vertices", min_length=3 ) y_verts: list[float] = Field( - description="The Nx1 y coordinates of the polygons vertices", min_len=3 + description="The Nx1 y coordinates of the polygons vertices", min_length=3 ) def axis_sets(self) -> list[set[Axis]]: @@ -298,7 +309,7 @@ class Circle(Region[Axis]): y_axis: Axis = Field(description="The name matching the y axis of the spec") x_middle: float = Field(description="The central x point of the circle") y_middle: float = Field(description="The central y point of the circle") - radius: float = Field(description="Radius of the circle", exc_min=0) + radius: float = Field(description="Radius of the circle", gt=0) def axis_sets(self) -> list[set[Axis]]: return [{self.x_axis, self.y_axis}] @@ -328,10 +339,10 @@ class Ellipse(Region[Axis]): x_middle: float = Field(description="The central x point of the ellipse") y_middle: float = Field(description="The central y point of the ellipse") x_radius: float = Field( - description="The radius along the x axis of the ellipse", exc_min=0 + description="The radius along the x axis of the ellipse", gt=0 ) y_radius: float = Field( - description="The radius along the y axis of the ellipse", exc_min=0 + description="The radius along the y axis of the ellipse", gt=0 ) angle: float = Field(description="The angle of the ellipse (degrees)", default=0.0) @@ -354,8 +365,10 @@ def mask(self, points: AxesPoints[Axis]) -> np.ndarray: def find_regions(obj) -> Iterator[Region[Axis]]: """Recursively yield Regions from obj and its children.""" - if hasattr(obj, "__pydantic_model__") and issubclass( - obj.__pydantic_model__, BaseModel + if ( + hasattr(obj, "__pydantic_model__") + and issubclass(obj.__pydantic_model__, BaseModel) + or is_dataclass(obj) ): if isinstance(obj, Region): yield obj diff --git a/src/scanspec/service.py b/src/scanspec/service.py index 939bfd8a..e52ef5d9 100644 --- a/src/scanspec/service.py +++ b/src/scanspec/service.py @@ -11,11 +11,11 @@ from pydantic import Field from pydantic.dataclasses import dataclass -from scanspec.core import AxesPoints, Frames, Path +from scanspec.core import AxesPoints, Frames, Path, uses_tagged_union from .specs import Line, Spec -app = FastAPI() +app = FastAPI(version="0.1.1") # # Data Model @@ -27,6 +27,7 @@ @dataclass +@uses_tagged_union class ValidResponse: """Response model for spec validation.""" @@ -43,6 +44,7 @@ class PointsFormat(str, Enum): @dataclass +@uses_tagged_union class PointsRequest: """A request for generated scan points.""" @@ -123,6 +125,7 @@ class SmallestStepResponse: @app.post("/valid", response_model=ValidResponse) +@uses_tagged_union def valid( spec: Spec = Body(..., examples=[_EXAMPLE_SPEC]), ) -> ValidResponse | JSONResponse: @@ -195,6 +198,7 @@ def bounds( @app.post("/gap", response_model=GapResponse) +@uses_tagged_union def gap( spec: Spec = Body( ..., @@ -220,6 +224,7 @@ def gap( @app.post("/smalleststep", response_model=SmallestStepResponse) +@uses_tagged_union def smallest_step( spec: Spec = Body(..., examples=[_EXAMPLE_SPEC]), ) -> SmallestStepResponse: diff --git a/src/scanspec/specs.py b/src/scanspec/specs.py index cbafb126..d51e5a6f 100644 --- a/src/scanspec/specs.py +++ b/src/scanspec/specs.py @@ -2,10 +2,13 @@ from collections.abc import Callable, Mapping from dataclasses import asdict -from typing import Any, Generic +from typing import ( + Any, + Generic, +) import numpy as np -from pydantic import Field, parse_obj_as +from pydantic import Field, validate_call from pydantic.dataclasses import dataclass from .core import ( @@ -15,6 +18,7 @@ Path, SnakedFrames, StrictConfig, + deserialize_as, discriminated_union_of_subclasses, gap_between_frames, if_instance_do, @@ -44,7 +48,7 @@ DURATION = "DURATION" -@discriminated_union_of_subclasses(config=StrictConfig) +@discriminated_union_of_subclasses class Spec(Generic[Axis]): """A serializable representation of the type and parameters of a scan. @@ -106,10 +110,10 @@ def serialize(self) -> Mapping[str, Any]: """Serialize the spec to a dictionary.""" return asdict(self) # type: ignore - @classmethod - def deserialize(cls, obj): + @staticmethod + def deserialize(obj): """Deserialize the spec from a dictionary.""" - return parse_obj_as(cls, obj) + return deserialize_as(Spec, obj) @dataclass(config=StrictConfig) @@ -160,7 +164,7 @@ class Repeat(Spec[Axis]): .. note:: There is no turnaround arrow at x=4 """ - num: int = Field(min=1, description="Number of frames to produce") + num: int = Field(ge=1, description="Number of frames to produce") gap: bool = Field( description="If False and the slowest of the stack of Frames is snaked " "then the end and start of consecutive iterations of Spec will have no gap", @@ -286,7 +290,7 @@ def calculate(self, bounds=True, nested=False) -> list[Frames[Axis]]: # The axis_set spans multiple Dimensions, squash them together # If the spec to be squashed is nested (inside the Mask or outside) # then check the path changes if requested - check_path_changes = (nested or si) and self.check_path_changes + check_path_changes = bool(nested or si) and self.check_path_changes squashed = squash_frames(frames[si : ei + 1], check_path_changes) frames = frames[:si] + [squashed] + frames[ei + 1 :] # Generate masks from the midpoints showing what's inside @@ -457,7 +461,7 @@ class Line(Spec[Axis]): axis: Axis = Field(description="An identifier for what to move") start: float = Field(description="Midpoint of the first point of the line") stop: float = Field(description="Midpoint of the last point of the line") - num: int = Field(min=1, description="Number of frames to produce") + num: int = Field(ge=1, description="Number of frames to produce") def axes(self) -> list: return [self.axis] @@ -485,7 +489,7 @@ def bounded( axis: Axis = 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(min=1, description="Number of frames to produce"), + num: int = Field(ge=1, description="Number of frames to produce"), ) -> Line[Axis]: """Specify a Line by extreme bounds instead of midpoints. @@ -506,6 +510,12 @@ def bounded( return cls(axis, start, stop, num) +""" +Defers wrapping function with validate_call until class is fully instantiated +""" +Line.bounded = validate_call(Line.bounded) # type:ignore + + @dataclass(config=StrictConfig) class Static(Spec[Axis]): """A static frame, repeated num times, with axis at value. @@ -521,13 +531,13 @@ class Static(Spec[Axis]): axis: Axis = Field(description="An identifier for what to move") value: float = Field(description="The value at each point") - num: int = Field(min=1, description="Number of frames to produce", default=1) + num: int = Field(ge=1, description="Number of frames to produce", default=1) @classmethod def duration( cls: type[Static], duration: float = Field(description="The duration of each static point"), - num: int = Field(min=1, description="Number of frames to produce", default=1), + num: int = Field(ge=1, description="Number of frames to produce", default=1), ) -> Static[str]: """A static spec with no motion, only a duration repeated "num" times. @@ -551,6 +561,9 @@ def calculate(self, bounds=True, nested=False) -> list[Frames[Axis]]: ) +Static.duration = validate_call(Static.duration) # type:ignore + + @dataclass(config=StrictConfig) class Spiral(Spec[Axis]): """Archimedean spiral of "x_axis" and "y_axis". @@ -573,7 +586,7 @@ class Spiral(Spec[Axis]): y_start: float = Field(description="y centre of the spiral") x_range: float = Field(description="x width of the spiral") y_range: float = Field(description="y width of the spiral") - num: int = Field(min=1, description="Number of frames to produce") + num: int = Field(ge=1, description="Number of frames to produce") rotate: float = Field( description="How much to rotate the angle of the spiral", default=0.0 ) @@ -636,6 +649,9 @@ def spaced( ) +Spiral.spaced = validate_call(Spiral.spaced) # type:ignore + + def fly(spec: Spec[Axis], duration: float) -> Spec[Axis]: """Flyscan, zipping with fixed duration for every frame. diff --git a/tests/test_cli.py b/tests/test_cli.py index 4268f017..37894ff0 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,3 +1,4 @@ +import json import pathlib import subprocess import sys @@ -238,7 +239,9 @@ def test_schema() -> None: result = runner.invoke(cli.schema) assert result.exit_code == 0 schema_path = pathlib.Path(__file__).resolve().parent.parent / "schema.json" - assert result.output == schema_path.read_text() + with open(schema_path) as file: + data = json.load(file) + assert data == json.loads(result.output) def test_cli_version(): diff --git a/tests/test_serialization.py b/tests/test_serialization.py index adb5729d..89c2e7d7 100644 --- a/tests/test_serialization.py +++ b/tests/test_serialization.py @@ -4,7 +4,7 @@ import pytest from pydantic import ValidationError -from scanspec.regions import Circle, Rectangle, UnionOf +from scanspec.regions import Circle, Rectangle, Region, UnionOf from scanspec.specs import Line, Mask, Spec, Spiral @@ -15,6 +15,20 @@ def test_line_serializes() -> None: assert Spec.deserialize(serialized) == ob +def test_circle_serializes() -> None: + ob = Circle("x", "y", x_middle=0, y_middle=1, radius=4) + serialized = { + "x_axis": "x", + "y_axis": "y", + "x_middle": 0.0, + "y_middle": 1.0, + "radius": 4.0, + "type": "Circle", + } + assert ob.serialize() == serialized + assert Region.deserialize(serialized) == ob + + def test_masked_circle_serializes() -> None: ob = Mask(Line("x", 0, 1, 4), Circle("x", "y", x_middle=0, y_middle=1, radius=4)) serialized = {