diff --git a/pyproject.toml b/pyproject.toml index 80ded4f2..38c13f8f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ requires-python = ">=3.10" # Plotting plotting = ["scipy", "matplotlib"] # REST service support -service = ["fastapi>0.100.0", "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 182d729c..2c61e06d 100644 --- a/schema.json +++ b/schema.json @@ -14,19 +14,10 @@ "content": { "application/json": { "schema": { - "oneOf": [ + "anyOf": [ { "$ref": "#/components/schemas/Concat-Input" }, - { - "$ref": "#/components/schemas/Line" - }, - { - "$ref": "#/components/schemas/Static" - }, - { - "$ref": "#/components/schemas/Spiral" - }, { "$ref": "#/components/schemas/Product-Input" }, @@ -44,6 +35,15 @@ }, { "$ref": "#/components/schemas/Squash-Input" + }, + { + "$ref": "#/components/schemas/Line" + }, + { + "$ref": "#/components/schemas/Static" + }, + { + "$ref": "#/components/schemas/Spiral" } ], "title": "Spec", @@ -65,22 +65,7 @@ }, "type": "Product" } - ], - "discriminator": { - "propertyName": "type", - "mapping": { - "Concat": "#/components/schemas/Concat-Input", - "Line": "#/components/schemas/Line", - "Static": "#/components/schemas/Static", - "Spiral": "#/components/schemas/Spiral", - "Product": "#/components/schemas/Product-Input", - "Repeat": "#/components/schemas/Repeat", - "Zip": "#/components/schemas/Zip-Input", - "Mask": "#/components/schemas/Mask-Input", - "Snake": "#/components/schemas/Snake-Input", - "Squash": "#/components/schemas/Squash-Input" - } - } + ] } } }, @@ -257,15 +242,6 @@ { "$ref": "#/components/schemas/Concat-Input" }, - { - "$ref": "#/components/schemas/Line" - }, - { - "$ref": "#/components/schemas/Static" - }, - { - "$ref": "#/components/schemas/Spiral" - }, { "$ref": "#/components/schemas/Product-Input" }, @@ -283,6 +259,15 @@ }, { "$ref": "#/components/schemas/Squash-Input" + }, + { + "$ref": "#/components/schemas/Line" + }, + { + "$ref": "#/components/schemas/Static" + }, + { + "$ref": "#/components/schemas/Spiral" } ], "title": "Spec", @@ -347,15 +332,6 @@ { "$ref": "#/components/schemas/Concat-Input" }, - { - "$ref": "#/components/schemas/Line" - }, - { - "$ref": "#/components/schemas/Static" - }, - { - "$ref": "#/components/schemas/Spiral" - }, { "$ref": "#/components/schemas/Product-Input" }, @@ -373,6 +349,15 @@ }, { "$ref": "#/components/schemas/Squash-Input" + }, + { + "$ref": "#/components/schemas/Line" + }, + { + "$ref": "#/components/schemas/Static" + }, + { + "$ref": "#/components/schemas/Spiral" } ], "title": "Spec", @@ -548,34 +533,34 @@ "left": { "oneOf": [ { - "$ref": "#/components/schemas/Range" + "$ref": "#/components/schemas/CombinationOf-Input" }, { - "$ref": "#/components/schemas/Rectangle" + "$ref": "#/components/schemas/UnionOf-Input" }, { - "$ref": "#/components/schemas/Polygon" + "$ref": "#/components/schemas/IntersectionOf-Input" }, { - "$ref": "#/components/schemas/Circle" + "$ref": "#/components/schemas/DifferenceOf-Input" }, { - "$ref": "#/components/schemas/Ellipse" + "$ref": "#/components/schemas/SymmetricDifferenceOf-Input" }, { - "$ref": "#/components/schemas/CombinationOf-Input" + "$ref": "#/components/schemas/Range" }, { - "$ref": "#/components/schemas/UnionOf" + "$ref": "#/components/schemas/Rectangle" }, { - "$ref": "#/components/schemas/IntersectionOf" + "$ref": "#/components/schemas/Polygon" }, { - "$ref": "#/components/schemas/DifferenceOf" + "$ref": "#/components/schemas/Circle" }, { - "$ref": "#/components/schemas/SymmetricDifferenceOf" + "$ref": "#/components/schemas/Ellipse" } ], "title": "Left", @@ -585,48 +570,48 @@ "mapping": { "Circle": "#/components/schemas/Circle", "CombinationOf": "#/components/schemas/CombinationOf-Input", - "DifferenceOf": "#/components/schemas/DifferenceOf", + "DifferenceOf": "#/components/schemas/DifferenceOf-Input", "Ellipse": "#/components/schemas/Ellipse", - "IntersectionOf": "#/components/schemas/IntersectionOf", + "IntersectionOf": "#/components/schemas/IntersectionOf-Input", "Polygon": "#/components/schemas/Polygon", "Range": "#/components/schemas/Range", "Rectangle": "#/components/schemas/Rectangle", - "SymmetricDifferenceOf": "#/components/schemas/SymmetricDifferenceOf", - "UnionOf": "#/components/schemas/UnionOf" + "SymmetricDifferenceOf": "#/components/schemas/SymmetricDifferenceOf-Input", + "UnionOf": "#/components/schemas/UnionOf-Input" } } }, "right": { "oneOf": [ { - "$ref": "#/components/schemas/Range" + "$ref": "#/components/schemas/CombinationOf-Input" }, { - "$ref": "#/components/schemas/Rectangle" + "$ref": "#/components/schemas/UnionOf-Input" }, { - "$ref": "#/components/schemas/Polygon" + "$ref": "#/components/schemas/IntersectionOf-Input" }, { - "$ref": "#/components/schemas/Circle" + "$ref": "#/components/schemas/DifferenceOf-Input" }, { - "$ref": "#/components/schemas/Ellipse" + "$ref": "#/components/schemas/SymmetricDifferenceOf-Input" }, { - "$ref": "#/components/schemas/CombinationOf-Input" + "$ref": "#/components/schemas/Range" }, { - "$ref": "#/components/schemas/UnionOf" + "$ref": "#/components/schemas/Rectangle" }, { - "$ref": "#/components/schemas/IntersectionOf" + "$ref": "#/components/schemas/Polygon" }, { - "$ref": "#/components/schemas/DifferenceOf" + "$ref": "#/components/schemas/Circle" }, { - "$ref": "#/components/schemas/SymmetricDifferenceOf" + "$ref": "#/components/schemas/Ellipse" } ], "title": "Right", @@ -636,14 +621,14 @@ "mapping": { "Circle": "#/components/schemas/Circle", "CombinationOf": "#/components/schemas/CombinationOf-Input", - "DifferenceOf": "#/components/schemas/DifferenceOf", + "DifferenceOf": "#/components/schemas/DifferenceOf-Input", "Ellipse": "#/components/schemas/Ellipse", - "IntersectionOf": "#/components/schemas/IntersectionOf", + "IntersectionOf": "#/components/schemas/IntersectionOf-Input", "Polygon": "#/components/schemas/Polygon", "Range": "#/components/schemas/Range", "Rectangle": "#/components/schemas/Rectangle", - "SymmetricDifferenceOf": "#/components/schemas/SymmetricDifferenceOf", - "UnionOf": "#/components/schemas/UnionOf" + "SymmetricDifferenceOf": "#/components/schemas/SymmetricDifferenceOf-Input", + "UnionOf": "#/components/schemas/UnionOf-Input" } } }, @@ -671,34 +656,34 @@ "left": { "oneOf": [ { - "$ref": "#/components/schemas/Range" + "$ref": "#/components/schemas/CombinationOf-Output" }, { - "$ref": "#/components/schemas/Rectangle" + "$ref": "#/components/schemas/UnionOf-Output" }, { - "$ref": "#/components/schemas/Polygon" + "$ref": "#/components/schemas/IntersectionOf-Output" }, { - "$ref": "#/components/schemas/Circle" + "$ref": "#/components/schemas/DifferenceOf-Output" }, { - "$ref": "#/components/schemas/Ellipse" + "$ref": "#/components/schemas/SymmetricDifferenceOf-Output" }, { - "$ref": "#/components/schemas/CombinationOf-Output" + "$ref": "#/components/schemas/Range" }, { - "$ref": "#/components/schemas/UnionOf" + "$ref": "#/components/schemas/Rectangle" }, { - "$ref": "#/components/schemas/IntersectionOf" + "$ref": "#/components/schemas/Polygon" }, { - "$ref": "#/components/schemas/DifferenceOf" + "$ref": "#/components/schemas/Circle" }, { - "$ref": "#/components/schemas/SymmetricDifferenceOf" + "$ref": "#/components/schemas/Ellipse" } ], "title": "Left", @@ -708,48 +693,48 @@ "mapping": { "Circle": "#/components/schemas/Circle", "CombinationOf": "#/components/schemas/CombinationOf-Output", - "DifferenceOf": "#/components/schemas/DifferenceOf", + "DifferenceOf": "#/components/schemas/DifferenceOf-Output", "Ellipse": "#/components/schemas/Ellipse", - "IntersectionOf": "#/components/schemas/IntersectionOf", + "IntersectionOf": "#/components/schemas/IntersectionOf-Output", "Polygon": "#/components/schemas/Polygon", "Range": "#/components/schemas/Range", "Rectangle": "#/components/schemas/Rectangle", - "SymmetricDifferenceOf": "#/components/schemas/SymmetricDifferenceOf", - "UnionOf": "#/components/schemas/UnionOf" + "SymmetricDifferenceOf": "#/components/schemas/SymmetricDifferenceOf-Output", + "UnionOf": "#/components/schemas/UnionOf-Output" } } }, "right": { "oneOf": [ { - "$ref": "#/components/schemas/Range" + "$ref": "#/components/schemas/CombinationOf-Output" }, { - "$ref": "#/components/schemas/Rectangle" + "$ref": "#/components/schemas/UnionOf-Output" }, { - "$ref": "#/components/schemas/Polygon" + "$ref": "#/components/schemas/IntersectionOf-Output" }, { - "$ref": "#/components/schemas/Circle" + "$ref": "#/components/schemas/DifferenceOf-Output" }, { - "$ref": "#/components/schemas/Ellipse" + "$ref": "#/components/schemas/SymmetricDifferenceOf-Output" }, { - "$ref": "#/components/schemas/CombinationOf-Output" + "$ref": "#/components/schemas/Range" }, { - "$ref": "#/components/schemas/UnionOf" + "$ref": "#/components/schemas/Rectangle" }, { - "$ref": "#/components/schemas/IntersectionOf" + "$ref": "#/components/schemas/Polygon" }, { - "$ref": "#/components/schemas/DifferenceOf" + "$ref": "#/components/schemas/Circle" }, { - "$ref": "#/components/schemas/SymmetricDifferenceOf" + "$ref": "#/components/schemas/Ellipse" } ], "title": "Right", @@ -759,14 +744,14 @@ "mapping": { "Circle": "#/components/schemas/Circle", "CombinationOf": "#/components/schemas/CombinationOf-Output", - "DifferenceOf": "#/components/schemas/DifferenceOf", + "DifferenceOf": "#/components/schemas/DifferenceOf-Output", "Ellipse": "#/components/schemas/Ellipse", - "IntersectionOf": "#/components/schemas/IntersectionOf", + "IntersectionOf": "#/components/schemas/IntersectionOf-Output", "Polygon": "#/components/schemas/Polygon", "Range": "#/components/schemas/Range", "Rectangle": "#/components/schemas/Rectangle", - "SymmetricDifferenceOf": "#/components/schemas/SymmetricDifferenceOf", - "UnionOf": "#/components/schemas/UnionOf" + "SymmetricDifferenceOf": "#/components/schemas/SymmetricDifferenceOf-Output", + "UnionOf": "#/components/schemas/UnionOf-Output" } } }, @@ -796,15 +781,6 @@ { "$ref": "#/components/schemas/Concat-Input" }, - { - "$ref": "#/components/schemas/Line" - }, - { - "$ref": "#/components/schemas/Static" - }, - { - "$ref": "#/components/schemas/Spiral" - }, { "$ref": "#/components/schemas/Product-Input" }, @@ -822,6 +798,15 @@ }, { "$ref": "#/components/schemas/Squash-Input" + }, + { + "$ref": "#/components/schemas/Line" + }, + { + "$ref": "#/components/schemas/Static" + }, + { + "$ref": "#/components/schemas/Spiral" } ], "title": "Left", @@ -847,15 +832,6 @@ { "$ref": "#/components/schemas/Concat-Input" }, - { - "$ref": "#/components/schemas/Line" - }, - { - "$ref": "#/components/schemas/Static" - }, - { - "$ref": "#/components/schemas/Spiral" - }, { "$ref": "#/components/schemas/Product-Input" }, @@ -873,6 +849,15 @@ }, { "$ref": "#/components/schemas/Squash-Input" + }, + { + "$ref": "#/components/schemas/Line" + }, + { + "$ref": "#/components/schemas/Static" + }, + { + "$ref": "#/components/schemas/Spiral" } ], "title": "Right", @@ -931,15 +916,6 @@ { "$ref": "#/components/schemas/Concat-Output" }, - { - "$ref": "#/components/schemas/Line" - }, - { - "$ref": "#/components/schemas/Static" - }, - { - "$ref": "#/components/schemas/Spiral" - }, { "$ref": "#/components/schemas/Product-Output" }, @@ -957,6 +933,15 @@ }, { "$ref": "#/components/schemas/Squash-Output" + }, + { + "$ref": "#/components/schemas/Line" + }, + { + "$ref": "#/components/schemas/Static" + }, + { + "$ref": "#/components/schemas/Spiral" } ], "title": "Left", @@ -982,15 +967,6 @@ { "$ref": "#/components/schemas/Concat-Output" }, - { - "$ref": "#/components/schemas/Line" - }, - { - "$ref": "#/components/schemas/Static" - }, - { - "$ref": "#/components/schemas/Spiral" - }, { "$ref": "#/components/schemas/Product-Output" }, @@ -1008,6 +984,15 @@ }, { "$ref": "#/components/schemas/Squash-Output" + }, + { + "$ref": "#/components/schemas/Line" + }, + { + "$ref": "#/components/schemas/Static" + }, + { + "$ref": "#/components/schemas/Spiral" } ], "title": "Right", @@ -1059,10 +1044,25 @@ "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": { + "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" }, @@ -1085,15 +1085,35 @@ "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" + "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" }, @@ -1116,10 +1136,15 @@ "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" + "Rectangle": "#/components/schemas/Rectangle", + "SymmetricDifferenceOf": "#/components/schemas/SymmetricDifferenceOf-Input", + "UnionOf": "#/components/schemas/UnionOf-Input" } } }, @@ -1142,17 +1167,140 @@ "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": { + "DifferenceOf-Output": { "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" + "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" + } + } }, - "x_middle": { + "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" @@ -1234,10 +1382,148 @@ "type": "object", "title": "HTTPValidationError" }, - "IntersectionOf": { + "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" }, @@ -1260,15 +1546,35 @@ "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" + "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" }, @@ -1291,10 +1597,15 @@ "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" + "Rectangle": "#/components/schemas/Rectangle", + "SymmetricDifferenceOf": "#/components/schemas/SymmetricDifferenceOf-Output", + "UnionOf": "#/components/schemas/UnionOf-Output" } } }, @@ -1367,15 +1678,6 @@ { "$ref": "#/components/schemas/Concat-Input" }, - { - "$ref": "#/components/schemas/Line" - }, - { - "$ref": "#/components/schemas/Static" - }, - { - "$ref": "#/components/schemas/Spiral" - }, { "$ref": "#/components/schemas/Product-Input" }, @@ -1393,6 +1695,15 @@ }, { "$ref": "#/components/schemas/Squash-Input" + }, + { + "$ref": "#/components/schemas/Line" + }, + { + "$ref": "#/components/schemas/Static" + }, + { + "$ref": "#/components/schemas/Spiral" } ], "title": "Spec", @@ -1416,34 +1727,34 @@ "region": { "oneOf": [ { - "$ref": "#/components/schemas/Range" + "$ref": "#/components/schemas/CombinationOf-Input" }, { - "$ref": "#/components/schemas/Rectangle" + "$ref": "#/components/schemas/UnionOf-Input" }, { - "$ref": "#/components/schemas/Polygon" + "$ref": "#/components/schemas/IntersectionOf-Input" }, { - "$ref": "#/components/schemas/Circle" + "$ref": "#/components/schemas/DifferenceOf-Input" }, { - "$ref": "#/components/schemas/Ellipse" + "$ref": "#/components/schemas/SymmetricDifferenceOf-Input" }, { - "$ref": "#/components/schemas/CombinationOf-Input" + "$ref": "#/components/schemas/Range" }, { - "$ref": "#/components/schemas/UnionOf" + "$ref": "#/components/schemas/Rectangle" }, { - "$ref": "#/components/schemas/IntersectionOf" + "$ref": "#/components/schemas/Polygon" }, { - "$ref": "#/components/schemas/DifferenceOf" + "$ref": "#/components/schemas/Circle" }, { - "$ref": "#/components/schemas/SymmetricDifferenceOf" + "$ref": "#/components/schemas/Ellipse" } ], "title": "Region", @@ -1453,14 +1764,14 @@ "mapping": { "Circle": "#/components/schemas/Circle", "CombinationOf": "#/components/schemas/CombinationOf-Input", - "DifferenceOf": "#/components/schemas/DifferenceOf", + "DifferenceOf": "#/components/schemas/DifferenceOf-Input", "Ellipse": "#/components/schemas/Ellipse", - "IntersectionOf": "#/components/schemas/IntersectionOf", + "IntersectionOf": "#/components/schemas/IntersectionOf-Input", "Polygon": "#/components/schemas/Polygon", "Range": "#/components/schemas/Range", "Rectangle": "#/components/schemas/Rectangle", - "SymmetricDifferenceOf": "#/components/schemas/SymmetricDifferenceOf", - "UnionOf": "#/components/schemas/UnionOf" + "SymmetricDifferenceOf": "#/components/schemas/SymmetricDifferenceOf-Input", + "UnionOf": "#/components/schemas/UnionOf-Input" } } }, @@ -1496,15 +1807,6 @@ { "$ref": "#/components/schemas/Concat-Output" }, - { - "$ref": "#/components/schemas/Line" - }, - { - "$ref": "#/components/schemas/Static" - }, - { - "$ref": "#/components/schemas/Spiral" - }, { "$ref": "#/components/schemas/Product-Output" }, @@ -1522,6 +1824,15 @@ }, { "$ref": "#/components/schemas/Squash-Output" + }, + { + "$ref": "#/components/schemas/Line" + }, + { + "$ref": "#/components/schemas/Static" + }, + { + "$ref": "#/components/schemas/Spiral" } ], "title": "Spec", @@ -1545,34 +1856,34 @@ "region": { "oneOf": [ { - "$ref": "#/components/schemas/Range" + "$ref": "#/components/schemas/CombinationOf-Output" }, { - "$ref": "#/components/schemas/Rectangle" + "$ref": "#/components/schemas/UnionOf-Output" }, { - "$ref": "#/components/schemas/Polygon" + "$ref": "#/components/schemas/IntersectionOf-Output" }, { - "$ref": "#/components/schemas/Circle" + "$ref": "#/components/schemas/DifferenceOf-Output" }, { - "$ref": "#/components/schemas/Ellipse" + "$ref": "#/components/schemas/SymmetricDifferenceOf-Output" }, { - "$ref": "#/components/schemas/CombinationOf-Output" + "$ref": "#/components/schemas/Range" }, { - "$ref": "#/components/schemas/UnionOf" + "$ref": "#/components/schemas/Rectangle" }, { - "$ref": "#/components/schemas/IntersectionOf" + "$ref": "#/components/schemas/Polygon" }, { - "$ref": "#/components/schemas/DifferenceOf" + "$ref": "#/components/schemas/Circle" }, { - "$ref": "#/components/schemas/SymmetricDifferenceOf" + "$ref": "#/components/schemas/Ellipse" } ], "title": "Region", @@ -1582,14 +1893,14 @@ "mapping": { "Circle": "#/components/schemas/Circle", "CombinationOf": "#/components/schemas/CombinationOf-Output", - "DifferenceOf": "#/components/schemas/DifferenceOf", + "DifferenceOf": "#/components/schemas/DifferenceOf-Output", "Ellipse": "#/components/schemas/Ellipse", - "IntersectionOf": "#/components/schemas/IntersectionOf", + "IntersectionOf": "#/components/schemas/IntersectionOf-Output", "Polygon": "#/components/schemas/Polygon", "Range": "#/components/schemas/Range", "Rectangle": "#/components/schemas/Rectangle", - "SymmetricDifferenceOf": "#/components/schemas/SymmetricDifferenceOf", - "UnionOf": "#/components/schemas/UnionOf" + "SymmetricDifferenceOf": "#/components/schemas/SymmetricDifferenceOf-Output", + "UnionOf": "#/components/schemas/UnionOf-Output" } } }, @@ -1684,15 +1995,6 @@ { "$ref": "#/components/schemas/Concat-Input" }, - { - "$ref": "#/components/schemas/Line" - }, - { - "$ref": "#/components/schemas/Static" - }, - { - "$ref": "#/components/schemas/Spiral" - }, { "$ref": "#/components/schemas/Product-Input" }, @@ -1710,6 +2012,15 @@ }, { "$ref": "#/components/schemas/Squash-Input" + }, + { + "$ref": "#/components/schemas/Line" + }, + { + "$ref": "#/components/schemas/Static" + }, + { + "$ref": "#/components/schemas/Spiral" } ], "title": "Spec", @@ -1775,18 +2086,18 @@ "type": "number" }, "type": "array", + "minItems": 3, "title": "X Verts", - "description": "The Nx1 x coordinates of the polygons vertices", - "min_len": 3 + "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", - "min_len": 3 + "description": "The Nx1 y coordinates of the polygons vertices" }, "type": { "type": "string", @@ -1816,15 +2127,6 @@ { "$ref": "#/components/schemas/Concat-Input" }, - { - "$ref": "#/components/schemas/Line" - }, - { - "$ref": "#/components/schemas/Static" - }, - { - "$ref": "#/components/schemas/Spiral" - }, { "$ref": "#/components/schemas/Product-Input" }, @@ -1842,6 +2144,15 @@ }, { "$ref": "#/components/schemas/Squash-Input" + }, + { + "$ref": "#/components/schemas/Line" + }, + { + "$ref": "#/components/schemas/Static" + }, + { + "$ref": "#/components/schemas/Spiral" } ], "title": "Outer", @@ -1867,15 +2178,6 @@ { "$ref": "#/components/schemas/Concat-Input" }, - { - "$ref": "#/components/schemas/Line" - }, - { - "$ref": "#/components/schemas/Static" - }, - { - "$ref": "#/components/schemas/Spiral" - }, { "$ref": "#/components/schemas/Product-Input" }, @@ -1893,6 +2195,15 @@ }, { "$ref": "#/components/schemas/Squash-Input" + }, + { + "$ref": "#/components/schemas/Line" + }, + { + "$ref": "#/components/schemas/Static" + }, + { + "$ref": "#/components/schemas/Spiral" } ], "title": "Inner", @@ -1939,15 +2250,6 @@ { "$ref": "#/components/schemas/Concat-Output" }, - { - "$ref": "#/components/schemas/Line" - }, - { - "$ref": "#/components/schemas/Static" - }, - { - "$ref": "#/components/schemas/Spiral" - }, { "$ref": "#/components/schemas/Product-Output" }, @@ -1965,6 +2267,15 @@ }, { "$ref": "#/components/schemas/Squash-Output" + }, + { + "$ref": "#/components/schemas/Line" + }, + { + "$ref": "#/components/schemas/Static" + }, + { + "$ref": "#/components/schemas/Spiral" } ], "title": "Outer", @@ -1990,15 +2301,6 @@ { "$ref": "#/components/schemas/Concat-Output" }, - { - "$ref": "#/components/schemas/Line" - }, - { - "$ref": "#/components/schemas/Static" - }, - { - "$ref": "#/components/schemas/Spiral" - }, { "$ref": "#/components/schemas/Product-Output" }, @@ -2016,6 +2318,15 @@ }, { "$ref": "#/components/schemas/Squash-Output" + }, + { + "$ref": "#/components/schemas/Line" + }, + { + "$ref": "#/components/schemas/Static" + }, + { + "$ref": "#/components/schemas/Spiral" } ], "title": "Inner", @@ -2213,15 +2524,6 @@ { "$ref": "#/components/schemas/Concat-Input" }, - { - "$ref": "#/components/schemas/Line" - }, - { - "$ref": "#/components/schemas/Static" - }, - { - "$ref": "#/components/schemas/Spiral" - }, { "$ref": "#/components/schemas/Product-Input" }, @@ -2239,6 +2541,15 @@ }, { "$ref": "#/components/schemas/Squash-Input" + }, + { + "$ref": "#/components/schemas/Line" + }, + { + "$ref": "#/components/schemas/Static" + }, + { + "$ref": "#/components/schemas/Spiral" } ], "title": "Spec", @@ -2284,15 +2595,6 @@ { "$ref": "#/components/schemas/Concat-Output" }, - { - "$ref": "#/components/schemas/Line" - }, - { - "$ref": "#/components/schemas/Static" - }, - { - "$ref": "#/components/schemas/Spiral" - }, { "$ref": "#/components/schemas/Product-Output" }, @@ -2310,6 +2612,15 @@ }, { "$ref": "#/components/schemas/Squash-Output" + }, + { + "$ref": "#/components/schemas/Line" + }, + { + "$ref": "#/components/schemas/Static" + }, + { + "$ref": "#/components/schemas/Spiral" } ], "title": "Spec", @@ -2421,15 +2732,6 @@ { "$ref": "#/components/schemas/Concat-Input" }, - { - "$ref": "#/components/schemas/Line" - }, - { - "$ref": "#/components/schemas/Static" - }, - { - "$ref": "#/components/schemas/Spiral" - }, { "$ref": "#/components/schemas/Product-Input" }, @@ -2447,6 +2749,15 @@ }, { "$ref": "#/components/schemas/Squash-Input" + }, + { + "$ref": "#/components/schemas/Line" + }, + { + "$ref": "#/components/schemas/Static" + }, + { + "$ref": "#/components/schemas/Spiral" } ], "title": "Spec", @@ -2498,6 +2809,24 @@ { "$ref": "#/components/schemas/Concat-Output" }, + { + "$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/Squash-Output" + }, { "$ref": "#/components/schemas/Line" }, @@ -2506,109 +2835,352 @@ }, { "$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/Product-Output" + "$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/Repeat" + "$ref": "#/components/schemas/Range" }, { - "$ref": "#/components/schemas/Zip-Output" + "$ref": "#/components/schemas/Rectangle" }, { - "$ref": "#/components/schemas/Mask-Output" + "$ref": "#/components/schemas/Polygon" }, { - "$ref": "#/components/schemas/Snake-Output" + "$ref": "#/components/schemas/Circle" }, { - "$ref": "#/components/schemas/Squash-Output" + "$ref": "#/components/schemas/Ellipse" } ], - "title": "Spec", - "description": "The Spec to squash the dimensions of", + "title": "Right", + "description": "The right-hand Region to combine", "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" + "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": [ - "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" + "SymmetricDifferenceOf" ], - "const": "Static", + "const": "SymmetricDifferenceOf", "title": "Type", - "default": "Static" + "default": "SymmetricDifferenceOf" } }, "additionalProperties": false, "type": "object", "required": [ - "axis", - "value" + "left", + "right" ], - "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))" + "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": { + "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" }, @@ -2631,15 +3203,35 @@ "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" + "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" }, @@ -2662,21 +3254,26 @@ "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" + "Rectangle": "#/components/schemas/Rectangle", + "SymmetricDifferenceOf": "#/components/schemas/SymmetricDifferenceOf-Input", + "UnionOf": "#/components/schemas/UnionOf-Input" } } }, "type": { "type": "string", "enum": [ - "SymmetricDifferenceOf" + "UnionOf" ], - "const": "SymmetricDifferenceOf", + "const": "UnionOf", "title": "Type", - "default": "SymmetricDifferenceOf" + "default": "UnionOf" } }, "additionalProperties": false, @@ -2685,13 +3282,28 @@ "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])" + "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": { + "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" }, @@ -2714,15 +3326,35 @@ "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" + "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" }, @@ -2745,10 +3377,15 @@ "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" + "Rectangle": "#/components/schemas/Rectangle", + "SymmetricDifferenceOf": "#/components/schemas/SymmetricDifferenceOf-Output", + "UnionOf": "#/components/schemas/UnionOf-Output" } } }, @@ -2778,15 +3415,6 @@ { "$ref": "#/components/schemas/Concat-Output" }, - { - "$ref": "#/components/schemas/Line" - }, - { - "$ref": "#/components/schemas/Static" - }, - { - "$ref": "#/components/schemas/Spiral" - }, { "$ref": "#/components/schemas/Product-Output" }, @@ -2804,6 +3432,15 @@ }, { "$ref": "#/components/schemas/Squash-Output" + }, + { + "$ref": "#/components/schemas/Line" + }, + { + "$ref": "#/components/schemas/Static" + }, + { + "$ref": "#/components/schemas/Spiral" } ], "title": "Input Spec", @@ -2829,15 +3466,6 @@ { "$ref": "#/components/schemas/Concat-Output" }, - { - "$ref": "#/components/schemas/Line" - }, - { - "$ref": "#/components/schemas/Static" - }, - { - "$ref": "#/components/schemas/Spiral" - }, { "$ref": "#/components/schemas/Product-Output" }, @@ -2855,6 +3483,15 @@ }, { "$ref": "#/components/schemas/Squash-Output" + }, + { + "$ref": "#/components/schemas/Line" + }, + { + "$ref": "#/components/schemas/Static" + }, + { + "$ref": "#/components/schemas/Spiral" } ], "title": "Valid Spec", @@ -2924,15 +3561,6 @@ { "$ref": "#/components/schemas/Concat-Input" }, - { - "$ref": "#/components/schemas/Line" - }, - { - "$ref": "#/components/schemas/Static" - }, - { - "$ref": "#/components/schemas/Spiral" - }, { "$ref": "#/components/schemas/Product-Input" }, @@ -2950,6 +3578,15 @@ }, { "$ref": "#/components/schemas/Squash-Input" + }, + { + "$ref": "#/components/schemas/Line" + }, + { + "$ref": "#/components/schemas/Static" + }, + { + "$ref": "#/components/schemas/Spiral" } ], "title": "Left", @@ -2975,15 +3612,6 @@ { "$ref": "#/components/schemas/Concat-Input" }, - { - "$ref": "#/components/schemas/Line" - }, - { - "$ref": "#/components/schemas/Static" - }, - { - "$ref": "#/components/schemas/Spiral" - }, { "$ref": "#/components/schemas/Product-Input" }, @@ -3001,6 +3629,15 @@ }, { "$ref": "#/components/schemas/Squash-Input" + }, + { + "$ref": "#/components/schemas/Line" + }, + { + "$ref": "#/components/schemas/Static" + }, + { + "$ref": "#/components/schemas/Spiral" } ], "title": "Right", @@ -3047,15 +3684,6 @@ { "$ref": "#/components/schemas/Concat-Output" }, - { - "$ref": "#/components/schemas/Line" - }, - { - "$ref": "#/components/schemas/Static" - }, - { - "$ref": "#/components/schemas/Spiral" - }, { "$ref": "#/components/schemas/Product-Output" }, @@ -3073,6 +3701,15 @@ }, { "$ref": "#/components/schemas/Squash-Output" + }, + { + "$ref": "#/components/schemas/Line" + }, + { + "$ref": "#/components/schemas/Static" + }, + { + "$ref": "#/components/schemas/Spiral" } ], "title": "Left", @@ -3098,15 +3735,6 @@ { "$ref": "#/components/schemas/Concat-Output" }, - { - "$ref": "#/components/schemas/Line" - }, - { - "$ref": "#/components/schemas/Static" - }, - { - "$ref": "#/components/schemas/Spiral" - }, { "$ref": "#/components/schemas/Product-Output" }, @@ -3124,6 +3752,15 @@ }, { "$ref": "#/components/schemas/Squash-Output" + }, + { + "$ref": "#/components/schemas/Line" + }, + { + "$ref": "#/components/schemas/Static" + }, + { + "$ref": "#/components/schemas/Spiral" } ], "title": "Right", diff --git a/src/scanspec/core.py b/src/scanspec/core.py index 9d46a136..f912867a 100644 --- a/src/scanspec/core.py +++ b/src/scanspec/core.py @@ -9,6 +9,7 @@ Generic, Literal, TypeVar, + Union, get_origin, get_type_hints, ) @@ -38,7 +39,7 @@ ] -StrictConfig: ConfigDict = ConfigDict(extra="forbid") +StrictConfig: ConfigDict = {"extra": "forbid"} def discriminated_union_of_subclasses( @@ -115,7 +116,7 @@ def calculate(self) -> int: """ tagged_union = _TaggedUnion(cls, discriminator) _tagged_unions[cls] = tagged_union - cls.__init_subclass__ = classmethod(__init_subclass__) + cls.__init_subclass__ = classmethod(partial(__init_subclass__, discriminator)) cls.__get_pydantic_core_schema__ = classmethod( partial(__get_pydantic_core_schema__, tagged_union=tagged_union) ) @@ -126,7 +127,7 @@ def calculate(self) -> int: def deserialize_as(cls, obj): - return TypeAdapter(_tagged_unions[cls]._make_union()).validate_python(obj) + return _tagged_unions[cls].type_adapter.validate_python(obj) def uses_tagged_union(cls_or_func: T) -> T: @@ -148,11 +149,8 @@ def __init__(self, base_class: type, discriminator: str): self._discriminator = discriminator def _make_union(self): - # Make a union of members - # https://docs.pydantic.dev/2.8/concepts/unions/#discriminated-unions-with-str-discriminators - if len(self._members) > 1: - # Unions are only valid with more than 1 member - return tuple(self._members) # type: ignore + 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 @@ -201,12 +199,12 @@ def add_referrer(self, cls: type | Callable, attr_name: str): _tagged_unions: dict[type, _TaggedUnion] = {} -def __init_subclass__(cls: type): +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__, - "type": Literal[cls.__name__], # type: ignore + 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 diff --git a/src/scanspec/regions.py b/src/scanspec/regions.py index 3b355516..c1ef8e22 100644 --- a/src/scanspec/regions.py +++ b/src/scanspec/regions.py @@ -12,6 +12,7 @@ AxesPoints, Axis, StrictConfig, + deserialize_as, discriminated_union_of_subclasses, if_instance_do, ) @@ -34,7 +35,6 @@ @discriminated_union_of_subclasses -@dataclass(config=StrictConfig) class Region(Generic[Axis]): """Abstract baseclass for a Region that can `Mask` a `Spec`. @@ -66,6 +66,11 @@ def __sub__(self, other) -> DifferenceOf[Axis]: def __xor__(self, other) -> SymmetricDifferenceOf[Axis]: return if_instance_do(other, Region, lambda o: SymmetricDifferenceOf(self, o)) + @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. @@ -96,6 +101,87 @@ def _merge_axis_sets(axis_sets: list[set[Axis]]) -> Iterator[set[Axis]]: yield axis_set +@dataclass(config=StrictConfig) +class CombinationOf(Region[Axis]): + """Abstract baseclass for a combination of two regions, left and right.""" + + left: Region[Axis] = Field(description="The left-hand Region to combine") + right: Region[Axis] = Field(description="The right-hand Region to combine") + + def axis_sets(self) -> list[set[Axis]]: + axis_sets = list( + _merge_axis_sets(self.left.axis_sets() + self.right.axis_sets()) + ) + return axis_sets + + +# Naming so we don't clash with typing.Union +@dataclass(config=StrictConfig) +class UnionOf(CombinationOf[Axis]): + """A point is in UnionOf(a, b) if in either a or b. + + Typically created with the ``|`` operator + + >>> r = Range("x", 0.5, 2.5) | Range("x", 1.5, 3.5) + >>> r.mask({"x": np.array([0, 1, 2, 3, 4])}) + array([False, True, True, True, False]) + """ + + def mask(self, points: AxesPoints[Axis]) -> np.ndarray: + mask = get_mask(self.left, points) | get_mask(self.right, points) + return mask + + +@dataclass(config=StrictConfig) +class IntersectionOf(CombinationOf[Axis]): + """A point is in IntersectionOf(a, b) if in both a and b. + + Typically created with the ``&`` operator. + + >>> r = Range("x", 0.5, 2.5) & Range("x", 1.5, 3.5) + >>> r.mask({"x": np.array([0, 1, 2, 3, 4])}) + array([False, False, True, False, False]) + """ + + def mask(self, points: AxesPoints[Axis]) -> np.ndarray: + mask = get_mask(self.left, points) & get_mask(self.right, points) + return mask + + +@dataclass(config=StrictConfig) +class DifferenceOf(CombinationOf[Axis]): + """A point is in DifferenceOf(a, b) if in a and not in b. + + Typically created with the ``-`` operator. + + >>> r = Range("x", 0.5, 2.5) - Range("x", 1.5, 3.5) + >>> r.mask({"x": np.array([0, 1, 2, 3, 4])}) + array([False, True, False, False, False]) + """ + + def mask(self, points: AxesPoints[Axis]) -> np.ndarray: + left_mask = get_mask(self.left, points) + # Return the xor restricted to the left region + mask = left_mask ^ get_mask(self.right, points) & left_mask + return mask + + +@dataclass(config=StrictConfig) +class SymmetricDifferenceOf(CombinationOf[Axis]): + """A point is in SymmetricDifferenceOf(a, b) if in either a or b, but not both. + + Typically created with the ``^`` operator. + + >>> r = Range("x", 0.5, 2.5) ^ Range("x", 1.5, 3.5) + >>> r.mask({"x": np.array([0, 1, 2, 3, 4])}) + array([False, True, False, True, False]) + """ + + def mask(self, points: AxesPoints[Axis]) -> np.ndarray: + mask = get_mask(self.left, points) ^ get_mask(self.right, points) + return mask + + @dataclass(config=StrictConfig) class Range(Region[Axis]): """Mask contains points of axis >= min and <= max. @@ -273,87 +359,6 @@ def mask(self, points: AxesPoints[Axis]) -> np.ndarray: return mask -@dataclass(config=StrictConfig) -class CombinationOf(Region[Axis]): - """Abstract baseclass for a combination of two regions, left and right.""" - - left: Region[Axis] = Field(description="The left-hand Region to combine") - right: Region[Axis] = Field(description="The right-hand Region to combine") - - def axis_sets(self) -> list[set[Axis]]: - axis_sets = list( - _merge_axis_sets(self.left.axis_sets() + self.right.axis_sets()) - ) - return axis_sets - - -# Naming so we don't clash with typing.Union -@dataclass(config=StrictConfig) -class UnionOf(CombinationOf[Axis]): - """A point is in UnionOf(a, b) if in either a or b. - - Typically created with the ``|`` operator - - >>> r = Range("x", 0.5, 2.5) | Range("x", 1.5, 3.5) - >>> r.mask({"x": np.array([0, 1, 2, 3, 4])}) - array([False, True, True, True, False]) - """ - - def mask(self, points: AxesPoints[Axis]) -> np.ndarray: - mask = get_mask(self.left, points) | get_mask(self.right, points) - return mask - - -@dataclass(config=StrictConfig) -class IntersectionOf(CombinationOf[Axis]): - """A point is in IntersectionOf(a, b) if in both a and b. - - Typically created with the ``&`` operator. - - >>> r = Range("x", 0.5, 2.5) & Range("x", 1.5, 3.5) - >>> r.mask({"x": np.array([0, 1, 2, 3, 4])}) - array([False, False, True, False, False]) - """ - - def mask(self, points: AxesPoints[Axis]) -> np.ndarray: - mask = get_mask(self.left, points) & get_mask(self.right, points) - return mask - - -@dataclass(config=StrictConfig) -class DifferenceOf(CombinationOf[Axis]): - """A point is in DifferenceOf(a, b) if in a and not in b. - - Typically created with the ``-`` operator. - - >>> r = Range("x", 0.5, 2.5) - Range("x", 1.5, 3.5) - >>> r.mask({"x": np.array([0, 1, 2, 3, 4])}) - array([False, True, False, False, False]) - """ - - def mask(self, points: AxesPoints[Axis]) -> np.ndarray: - left_mask = get_mask(self.left, points) - # Return the xor restricted to the left region - mask = left_mask ^ get_mask(self.right, points) & left_mask - return mask - - -@dataclass(config=StrictConfig) -class SymmetricDifferenceOf(CombinationOf[Axis]): - """A point is in SymmetricDifferenceOf(a, b) if in either a or b, but not both. - - Typically created with the ``^`` operator. - - >>> r = Range("x", 0.5, 2.5) ^ Range("x", 1.5, 3.5) - >>> r.mask({"x": np.array([0, 1, 2, 3, 4])}) - array([False, True, False, True, False]) - """ - - def mask(self, points: AxesPoints[Axis]) -> np.ndarray: - mask = get_mask(self.left, points) ^ get_mask(self.right, points) - return mask - - def find_regions(obj) -> Iterator[Region[Axis]]: """Recursively yield Regions from obj and its children.""" if ( diff --git a/src/scanspec/specs.py b/src/scanspec/specs.py index 42b8a74f..b8fa353f 100644 --- a/src/scanspec/specs.py +++ b/src/scanspec/specs.py @@ -48,7 +48,6 @@ DURATION = "DURATION" -@dataclass(config=StrictConfig) @discriminated_union_of_subclasses class Spec(Generic[Axis]): """A serializable representation of the type and parameters of a scan. @@ -111,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 deserialize_as(cls, obj) + return deserialize_as(Spec, obj) @dataclass(config=StrictConfig) @@ -164,208 +163,6 @@ def calculate(self, bounds=True, nested=False) -> list[Frames[Axis]]: return [dim] -@dataclass(config=StrictConfig) -class Line(Spec[Axis]): - """Linearly spaced frames with start and stop as first and last midpoints. - - .. example_spec:: - - from scanspec.specs import Line - - spec = Line("x", 1, 2, 5) - """ - - axis: Axis = Field(description="An identifier for what to move") - start: float = Field(description="Midpoint of the first point of the line") - stop: float = Field(description="Midpoint of the last point of the line") - num: int = Field(ge=1, description="Number of frames to produce") - - def axes(self) -> list: - return [self.axis] - - def _line_from_indexes(self, indexes: np.ndarray) -> dict[Axis, np.ndarray]: - if self.num == 1: - # Only one point, stop-start gives length of one point - step = self.stop - self.start - else: - # Multiple points, stop-start gives length of num-1 points - step = (self.stop - self.start) / (self.num - 1) - # self.start is the first centre point, but we need the lower bound - # of the first point as this is where the index array starts - first = self.start - step / 2 - return {self.axis: indexes * step + first} - - def calculate(self, bounds=True, nested=False) -> list[Frames[Axis]]: - return _dimensions_from_indexes( - self._line_from_indexes, self.axes(), self.num, bounds - ) - - @classmethod - def bounded( - cls, - axis: Axis = Field(description="An identifier for what to move"), - lower: float = Field(description="Lower bound of the first point of the line"), - upper: float = Field(description="Upper bound of the last point of the line"), - num: int = Field(ge=1, description="Number of frames to produce"), - ) -> Line[Axis]: - """Specify a Line by extreme bounds instead of midpoints. - - .. example_spec:: - - from scanspec.specs import Line - - spec = Line.bounded("x", 1, 2, 5) - """ - half_step = (upper - lower) / num / 2 - start = lower + half_step - if num == 1: - # One point, stop will only be used for step size - stop = upper + half_step - else: - # Many points, stop will be produced - stop = upper - half_step - return cls(axis, start, stop, num) - - -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. - - Can be used to set axis=value at every point in a scan. - - .. example_spec:: - - from scanspec.specs import Line, Static - - spec = Line("y", 1, 2, 3).zip(Static("x", 3)) - """ - - axis: Axis = Field(description="An identifier for what to move") - value: float = Field(description="The value at each point") - num: int = Field(ge=1, description="Number of frames to produce", default=1) - - @classmethod - def duration( - cls: type[Static], - duration: float = Field(description="The duration of each static point"), - num: int = Field(ge=1, description="Number of frames to produce", default=1), - ) -> Static[str]: - """A static spec with no motion, only a duration repeated "num" times. - - .. example_spec:: - - from scanspec.specs import Line, Static - - spec = Line("y", 1, 2, 3).zip(Static.duration(0.1)) - """ - return cls(DURATION, duration, num) - - def axes(self) -> list: - return [self.axis] - - def _repeats_from_indexes(self, indexes: np.ndarray) -> dict[Axis, np.ndarray]: - return {self.axis: np.full(len(indexes), self.value)} - - def calculate(self, bounds=True, nested=False) -> list[Frames[Axis]]: - return _dimensions_from_indexes( - self._repeats_from_indexes, self.axes(), self.num, bounds - ) - - -Static.duration = validate_call(Static.duration) # type:ignore - - -@dataclass(config=StrictConfig) -class Spiral(Spec[Axis]): - """Archimedean spiral of "x_axis" and "y_axis". - - Starts at centre point ("x_start", "y_start") with angle "rotate". Produces - "num" points in a spiral spanning width of "x_range" and height of "y_range" - - .. example_spec:: - - from scanspec.specs import Spiral - - spec = Spiral("x", "y", 1, 5, 10, 50, 30) - """ - - # TODO: Make use of typing.Annotated upon fix of - # https://github.com/pydantic/pydantic/issues/3496 - x_axis: Axis = Field(description="An identifier for what to move for x") - y_axis: Axis = Field(description="An identifier for what to move for y") - x_start: float = Field(description="x centre of the spiral") - y_start: float = Field(description="y centre of the spiral") - x_range: float = Field(description="x width of the spiral") - y_range: float = Field(description="y width of the spiral") - num: int = Field(ge=1, description="Number of frames to produce") - rotate: float = Field( - description="How much to rotate the angle of the spiral", default=0.0 - ) - - def axes(self) -> list[Axis]: - # TODO: reversed from __init__ args, a good idea? - return [self.y_axis, self.x_axis] - - def _spiral_from_indexes(self, indexes: np.ndarray) -> dict[Axis, np.ndarray]: - # simplest spiral equation: r = phi - # we want point spacing across area to be the same as between rings - # so: sqrt(area / num) = ring_spacing - # so: sqrt(pi * phi^2 / num) = 2 * pi - # so: phi = sqrt(4 * pi * num) - phi = np.sqrt(4 * np.pi * indexes) - # indexes are 0..num inclusive, and diameter is 2x biggest phi - diameter = 2 * np.sqrt(4 * np.pi * self.num) - # scale so that the spiral is strictly smaller than the range - x_scale = self.x_range / diameter - y_scale = self.y_range / diameter - return { - self.y_axis: self.y_start + y_scale * phi * np.cos(phi + self.rotate), - self.x_axis: self.x_start + x_scale * phi * np.sin(phi + self.rotate), - } - - def calculate(self, bounds=True, nested=False) -> list[Frames[Axis]]: - return _dimensions_from_indexes( - self._spiral_from_indexes, self.axes(), self.num, bounds - ) - - @classmethod - def spaced( - cls, - x_axis: Axis = Field(description="An identifier for what to move for x"), - y_axis: Axis = Field(description="An identifier for what to move for y"), - x_start: float = Field(description="x centre of the spiral"), - y_start: float = Field(description="y centre of the spiral"), - radius: float = Field(description="radius of the spiral"), - dr: float = Field(description="difference between each ring"), - rotate: float = Field( - description="How much to rotate the angle of the spiral", default=0.0 - ), - ) -> Spiral[Axis]: - """Specify a Spiral equally spaced in "x_axis" and "y_axis". - - .. example_spec:: - - from scanspec.specs import Spiral - - spec = Spiral.spaced("x", "y", 0, 0, 10, 3) - """ - # phi = sqrt(4 * pi * num) - # and: n_rings = phi / (2 * pi) - # so: n_rings * 2 * pi = sqrt(4 * pi * num) - # so: num = n_rings^2 * pi - n_rings = radius / dr - num = int(n_rings**2 * np.pi) - return cls( - x_axis, y_axis, x_start, y_start, radius * 2, radius * 2, num, rotate - ) - - -Spiral.spaced = validate_call(Spiral.spaced) # type:ignore - - @dataclass(config=StrictConfig) class Product(Spec[Axis]): """Outer product of two Specs, nesting inner within outer. @@ -650,6 +447,208 @@ def _dimensions_from_indexes( return [dimension] +@dataclass(config=StrictConfig) +class Line(Spec[Axis]): + """Linearly spaced frames with start and stop as first and last midpoints. + + .. example_spec:: + + from scanspec.specs import Line + + spec = Line("x", 1, 2, 5) + """ + + axis: Axis = Field(description="An identifier for what to move") + start: float = Field(description="Midpoint of the first point of the line") + stop: float = Field(description="Midpoint of the last point of the line") + num: int = Field(ge=1, description="Number of frames to produce") + + def axes(self) -> list: + return [self.axis] + + def _line_from_indexes(self, indexes: np.ndarray) -> dict[Axis, np.ndarray]: + if self.num == 1: + # Only one point, stop-start gives length of one point + step = self.stop - self.start + else: + # Multiple points, stop-start gives length of num-1 points + step = (self.stop - self.start) / (self.num - 1) + # self.start is the first centre point, but we need the lower bound + # of the first point as this is where the index array starts + first = self.start - step / 2 + return {self.axis: indexes * step + first} + + def calculate(self, bounds=True, nested=False) -> list[Frames[Axis]]: + return _dimensions_from_indexes( + self._line_from_indexes, self.axes(), self.num, bounds + ) + + @classmethod + def bounded( + cls, + axis: Axis = Field(description="An identifier for what to move"), + lower: float = Field(description="Lower bound of the first point of the line"), + upper: float = Field(description="Upper bound of the last point of the line"), + num: int = Field(ge=1, description="Number of frames to produce"), + ) -> Line[Axis]: + """Specify a Line by extreme bounds instead of midpoints. + + .. example_spec:: + + from scanspec.specs import Line + + spec = Line.bounded("x", 1, 2, 5) + """ + half_step = (upper - lower) / num / 2 + start = lower + half_step + if num == 1: + # One point, stop will only be used for step size + stop = upper + half_step + else: + # Many points, stop will be produced + stop = upper - half_step + return cls(axis, start, stop, num) + + +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. + + Can be used to set axis=value at every point in a scan. + + .. example_spec:: + + from scanspec.specs import Line, Static + + spec = Line("y", 1, 2, 3).zip(Static("x", 3)) + """ + + axis: Axis = Field(description="An identifier for what to move") + value: float = Field(description="The value at each point") + num: int = Field(ge=1, description="Number of frames to produce", default=1) + + @classmethod + def duration( + cls: type[Static], + duration: float = Field(description="The duration of each static point"), + num: int = Field(ge=1, description="Number of frames to produce", default=1), + ) -> Static[str]: + """A static spec with no motion, only a duration repeated "num" times. + + .. example_spec:: + + from scanspec.specs import Line, Static + + spec = Line("y", 1, 2, 3).zip(Static.duration(0.1)) + """ + return cls(DURATION, duration, num) + + def axes(self) -> list: + return [self.axis] + + def _repeats_from_indexes(self, indexes: np.ndarray) -> dict[Axis, np.ndarray]: + return {self.axis: np.full(len(indexes), self.value)} + + def calculate(self, bounds=True, nested=False) -> list[Frames[Axis]]: + return _dimensions_from_indexes( + self._repeats_from_indexes, self.axes(), self.num, bounds + ) + + +Static.duration = validate_call(Static.duration) # type:ignore + + +@dataclass(config=StrictConfig) +class Spiral(Spec[Axis]): + """Archimedean spiral of "x_axis" and "y_axis". + + Starts at centre point ("x_start", "y_start") with angle "rotate". Produces + "num" points in a spiral spanning width of "x_range" and height of "y_range" + + .. example_spec:: + + from scanspec.specs import Spiral + + spec = Spiral("x", "y", 1, 5, 10, 50, 30) + """ + + # TODO: Make use of typing.Annotated upon fix of + # https://github.com/pydantic/pydantic/issues/3496 + x_axis: Axis = Field(description="An identifier for what to move for x") + y_axis: Axis = Field(description="An identifier for what to move for y") + x_start: float = Field(description="x centre of the spiral") + y_start: float = Field(description="y centre of the spiral") + x_range: float = Field(description="x width of the spiral") + y_range: float = Field(description="y width of the spiral") + num: int = Field(ge=1, description="Number of frames to produce") + rotate: float = Field( + description="How much to rotate the angle of the spiral", default=0.0 + ) + + def axes(self) -> list[Axis]: + # TODO: reversed from __init__ args, a good idea? + return [self.y_axis, self.x_axis] + + def _spiral_from_indexes(self, indexes: np.ndarray) -> dict[Axis, np.ndarray]: + # simplest spiral equation: r = phi + # we want point spacing across area to be the same as between rings + # so: sqrt(area / num) = ring_spacing + # so: sqrt(pi * phi^2 / num) = 2 * pi + # so: phi = sqrt(4 * pi * num) + phi = np.sqrt(4 * np.pi * indexes) + # indexes are 0..num inclusive, and diameter is 2x biggest phi + diameter = 2 * np.sqrt(4 * np.pi * self.num) + # scale so that the spiral is strictly smaller than the range + x_scale = self.x_range / diameter + y_scale = self.y_range / diameter + return { + self.y_axis: self.y_start + y_scale * phi * np.cos(phi + self.rotate), + self.x_axis: self.x_start + x_scale * phi * np.sin(phi + self.rotate), + } + + def calculate(self, bounds=True, nested=False) -> list[Frames[Axis]]: + return _dimensions_from_indexes( + self._spiral_from_indexes, self.axes(), self.num, bounds + ) + + @classmethod + def spaced( + cls, + x_axis: Axis = Field(description="An identifier for what to move for x"), + y_axis: Axis = Field(description="An identifier for what to move for y"), + x_start: float = Field(description="x centre of the spiral"), + y_start: float = Field(description="y centre of the spiral"), + radius: float = Field(description="radius of the spiral"), + dr: float = Field(description="difference between each ring"), + rotate: float = Field( + description="How much to rotate the angle of the spiral", default=0.0 + ), + ) -> Spiral[Axis]: + """Specify a Spiral equally spaced in "x_axis" and "y_axis". + + .. example_spec:: + + from scanspec.specs import Spiral + + spec = Spiral.spaced("x", "y", 0, 0, 10, 3) + """ + # phi = sqrt(4 * pi * num) + # and: n_rings = phi / (2 * pi) + # so: n_rings * 2 * pi = sqrt(4 * pi * num) + # so: num = n_rings^2 * pi + n_rings = radius / dr + num = int(n_rings**2 * np.pi) + return cls( + x_axis, y_axis, x_start, y_start, radius * 2, radius * 2, num, rotate + ) + + +Spiral.spaced = validate_call(Spiral.spaced) # type:ignore + + def fly(spec: Spec[Axis], duration: float) -> Spec[Axis]: """Flyscan, zipping with fixed duration for every frame.