diff --git a/main/.buildinfo b/main/.buildinfo
new file mode 100644
index 00000000..8a569ce1
--- /dev/null
+++ b/main/.buildinfo
@@ -0,0 +1,4 @@
+# Sphinx build info version 1
+# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
+config: 383afb9c63caf10cc1381549f29c53b5
+tags: 645f666f9bcd5a90fca523b33c5a78b7
diff --git a/main/.doctrees/environment.pickle b/main/.doctrees/environment.pickle
new file mode 100644
index 00000000..c04b369f
Binary files /dev/null and b/main/.doctrees/environment.pickle differ
diff --git a/main/.doctrees/explanations.doctree b/main/.doctrees/explanations.doctree
new file mode 100644
index 00000000..164d0e07
Binary files /dev/null and b/main/.doctrees/explanations.doctree differ
diff --git a/main/.doctrees/explanations/technical-terms.doctree b/main/.doctrees/explanations/technical-terms.doctree
new file mode 100644
index 00000000..fcaef7cb
Binary files /dev/null and b/main/.doctrees/explanations/technical-terms.doctree differ
diff --git a/main/.doctrees/explanations/why-squash-can-change-path.doctree b/main/.doctrees/explanations/why-squash-can-change-path.doctree
new file mode 100644
index 00000000..bc48bb2e
Binary files /dev/null and b/main/.doctrees/explanations/why-squash-can-change-path.doctree differ
diff --git a/main/.doctrees/explanations/why-stack-frames.doctree b/main/.doctrees/explanations/why-stack-frames.doctree
new file mode 100644
index 00000000..040d1b2f
Binary files /dev/null and b/main/.doctrees/explanations/why-stack-frames.doctree differ
diff --git a/main/.doctrees/genindex.doctree b/main/.doctrees/genindex.doctree
new file mode 100644
index 00000000..102a27b2
Binary files /dev/null and b/main/.doctrees/genindex.doctree differ
diff --git a/main/.doctrees/how-to.doctree b/main/.doctrees/how-to.doctree
new file mode 100644
index 00000000..4d117f60
Binary files /dev/null and b/main/.doctrees/how-to.doctree differ
diff --git a/main/.doctrees/how-to/iterate-a-spec.doctree b/main/.doctrees/how-to/iterate-a-spec.doctree
new file mode 100644
index 00000000..82f6a1c6
Binary files /dev/null and b/main/.doctrees/how-to/iterate-a-spec.doctree differ
diff --git a/main/.doctrees/how-to/serialize-a-spec.doctree b/main/.doctrees/how-to/serialize-a-spec.doctree
new file mode 100644
index 00000000..976dca0d
Binary files /dev/null and b/main/.doctrees/how-to/serialize-a-spec.doctree differ
diff --git a/main/.doctrees/index.doctree b/main/.doctrees/index.doctree
new file mode 100644
index 00000000..605947ed
Binary files /dev/null and b/main/.doctrees/index.doctree differ
diff --git a/main/.doctrees/reference.doctree b/main/.doctrees/reference.doctree
new file mode 100644
index 00000000..f8d8470d
Binary files /dev/null and b/main/.doctrees/reference.doctree differ
diff --git a/main/.doctrees/reference/api.doctree b/main/.doctrees/reference/api.doctree
new file mode 100644
index 00000000..69fb9ed7
Binary files /dev/null and b/main/.doctrees/reference/api.doctree differ
diff --git a/main/.doctrees/reference/contributing.doctree b/main/.doctrees/reference/contributing.doctree
new file mode 100644
index 00000000..6f99d8d8
Binary files /dev/null and b/main/.doctrees/reference/contributing.doctree differ
diff --git a/main/.doctrees/reference/rest_api.doctree b/main/.doctrees/reference/rest_api.doctree
new file mode 100644
index 00000000..5d32cab6
Binary files /dev/null and b/main/.doctrees/reference/rest_api.doctree differ
diff --git a/main/.doctrees/tutorials.doctree b/main/.doctrees/tutorials.doctree
new file mode 100644
index 00000000..a4886bf1
Binary files /dev/null and b/main/.doctrees/tutorials.doctree differ
diff --git a/main/.doctrees/tutorials/creating-a-spec.doctree b/main/.doctrees/tutorials/creating-a-spec.doctree
new file mode 100644
index 00000000..4c0e2bf9
Binary files /dev/null and b/main/.doctrees/tutorials/creating-a-spec.doctree differ
diff --git a/main/.doctrees/tutorials/installation.doctree b/main/.doctrees/tutorials/installation.doctree
new file mode 100644
index 00000000..34c91b83
Binary files /dev/null and b/main/.doctrees/tutorials/installation.doctree differ
diff --git a/main/.doctrees/tutorials/rest-service.doctree b/main/.doctrees/tutorials/rest-service.doctree
new file mode 100644
index 00000000..4b640b06
Binary files /dev/null and b/main/.doctrees/tutorials/rest-service.doctree differ
diff --git a/main/_downloads/03d02dd7fcbfa209b8e8fc653cd48fe9/why-squash-can-change-path-3.hires.png b/main/_downloads/03d02dd7fcbfa209b8e8fc653cd48fe9/why-squash-can-change-path-3.hires.png
new file mode 100644
index 00000000..4b52560f
Binary files /dev/null and b/main/_downloads/03d02dd7fcbfa209b8e8fc653cd48fe9/why-squash-can-change-path-3.hires.png differ
diff --git a/main/_downloads/03df8facf6b141a80c01c64943a8d53d/why-squash-can-change-path-1.py b/main/_downloads/03df8facf6b141a80c01c64943a8d53d/why-squash-can-change-path-1.py
new file mode 100644
index 00000000..3212fde7
--- /dev/null
+++ b/main/_downloads/03df8facf6b141a80c01c64943a8d53d/why-squash-can-change-path-1.py
@@ -0,0 +1,5 @@
+from scanspec.specs import Line
+from scanspec.plot import plot_spec
+
+spec = Line("z", 0, 1, 3) * ~Line("y", 0, 1, 3) * Line("x", 0, 1, 3)
+plot_spec(spec)
\ No newline at end of file
diff --git a/main/_downloads/057159ccedf0735e14b556c60a1dda9b/api-15.hires.png b/main/_downloads/057159ccedf0735e14b556c60a1dda9b/api-15.hires.png
new file mode 100644
index 00000000..dffd6596
Binary files /dev/null and b/main/_downloads/057159ccedf0735e14b556c60a1dda9b/api-15.hires.png differ
diff --git a/main/_downloads/090ae78c4b6455d5980148fff5aa44c2/creating-a-spec-3.hires.png b/main/_downloads/090ae78c4b6455d5980148fff5aa44c2/creating-a-spec-3.hires.png
new file mode 100644
index 00000000..4c2352d9
Binary files /dev/null and b/main/_downloads/090ae78c4b6455d5980148fff5aa44c2/creating-a-spec-3.hires.png differ
diff --git a/main/_downloads/0b4799657e877d5b1b4cacd9b2acb170/why-squash-can-change-path-3.pdf b/main/_downloads/0b4799657e877d5b1b4cacd9b2acb170/why-squash-can-change-path-3.pdf
new file mode 100644
index 00000000..b24de864
Binary files /dev/null and b/main/_downloads/0b4799657e877d5b1b4cacd9b2acb170/why-squash-can-change-path-3.pdf differ
diff --git a/main/_downloads/1196c5a1d3f6e3ebfd587de4b9ad3ab8/api-17.py b/main/_downloads/1196c5a1d3f6e3ebfd587de4b9ad3ab8/api-17.py
new file mode 100644
index 00000000..9fd0362c
--- /dev/null
+++ b/main/_downloads/1196c5a1d3f6e3ebfd587de4b9ad3ab8/api-17.py
@@ -0,0 +1,9 @@
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.regions import Rectangle
+from scanspec.specs import Line
+
+grid = Line("y", 1, 3, 10) * ~Line("x", 0, 2, 10)
+spec = grid & Rectangle("x", "y", 0, 1.1, 1.5, 2.1, 30)
+plot_spec(spec)
\ No newline at end of file
diff --git a/main/_downloads/130a9cb2a2397d788bb832670c6b98be/api-10.hires.png b/main/_downloads/130a9cb2a2397d788bb832670c6b98be/api-10.hires.png
new file mode 100644
index 00000000..fe8ebcc8
Binary files /dev/null and b/main/_downloads/130a9cb2a2397d788bb832670c6b98be/api-10.hires.png differ
diff --git a/main/_downloads/1328d20d2f1f1197b57441e6e3c5744c/api-20.py b/main/_downloads/1328d20d2f1f1197b57441e6e3c5744c/api-20.py
new file mode 100644
index 00000000..a337b2ed
--- /dev/null
+++ b/main/_downloads/1328d20d2f1f1197b57441e6e3c5744c/api-20.py
@@ -0,0 +1,9 @@
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.regions import Ellipse
+from scanspec.specs import Line
+
+grid = Line("y", 3, 8, 10) * ~Line("x", 1 ,8, 10)
+spec = grid & Ellipse("x", "y", 5, 5, 2, 3, 75)
+plot_spec(spec)
\ No newline at end of file
diff --git a/main/_downloads/13b77470026a004a78b9fe0d8381851c/creating-a-spec-1.py b/main/_downloads/13b77470026a004a78b9fe0d8381851c/creating-a-spec-1.py
new file mode 100644
index 00000000..d40cfb88
--- /dev/null
+++ b/main/_downloads/13b77470026a004a78b9fe0d8381851c/creating-a-spec-1.py
@@ -0,0 +1,7 @@
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.specs import Line
+
+spec = Line("x", 1, 2, 5)
+plot_spec(spec)
\ No newline at end of file
diff --git a/main/_downloads/140a97216923402012aba5e48fea542e/why-squash-can-change-path-4.py b/main/_downloads/140a97216923402012aba5e48fea542e/why-squash-can-change-path-4.py
new file mode 100644
index 00000000..e4881c6e
--- /dev/null
+++ b/main/_downloads/140a97216923402012aba5e48fea542e/why-squash-can-change-path-4.py
@@ -0,0 +1,7 @@
+from scanspec.specs import Line, Squash
+from scanspec.plot import plot_spec
+
+spec = Line("z", 0, 1, 3) * Squash(
+ Line("y", 0, 1, 3) * ~Line("x", 0, 1, 3), check_path_changes=False
+)
+plot_spec(spec)
\ No newline at end of file
diff --git a/main/_downloads/18e0995c191e17b5282d28a1d408b17a/api-6.png b/main/_downloads/18e0995c191e17b5282d28a1d408b17a/api-6.png
new file mode 100644
index 00000000..e7eb3e03
Binary files /dev/null and b/main/_downloads/18e0995c191e17b5282d28a1d408b17a/api-6.png differ
diff --git a/main/_downloads/1f2bf7a0ab5a81274b679654b2cfceaa/creating-a-spec-4.png b/main/_downloads/1f2bf7a0ab5a81274b679654b2cfceaa/creating-a-spec-4.png
new file mode 100644
index 00000000..8e161c10
Binary files /dev/null and b/main/_downloads/1f2bf7a0ab5a81274b679654b2cfceaa/creating-a-spec-4.png differ
diff --git a/main/_downloads/230186b3633a9f4b80c5e0a62d257a5a/api-19.py b/main/_downloads/230186b3633a9f4b80c5e0a62d257a5a/api-19.py
new file mode 100644
index 00000000..aff5125b
--- /dev/null
+++ b/main/_downloads/230186b3633a9f4b80c5e0a62d257a5a/api-19.py
@@ -0,0 +1,9 @@
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.regions import Circle
+from scanspec.specs import Line
+
+grid = Line("y", 1, 3, 10) * ~Line("x", 0, 2, 10)
+spec = grid & Circle("x", "y", 1, 2, 0.9)
+plot_spec(spec)
\ No newline at end of file
diff --git a/main/_downloads/25365eba811f6b456fe24829840ed193/why-squash-can-change-path-4.png b/main/_downloads/25365eba811f6b456fe24829840ed193/why-squash-can-change-path-4.png
new file mode 100644
index 00000000..fb7645a0
Binary files /dev/null and b/main/_downloads/25365eba811f6b456fe24829840ed193/why-squash-can-change-path-4.png differ
diff --git a/main/_downloads/259d09caa9d289e5e3237369eb66aaf5/api-14.pdf b/main/_downloads/259d09caa9d289e5e3237369eb66aaf5/api-14.pdf
new file mode 100644
index 00000000..f9c73b5b
Binary files /dev/null and b/main/_downloads/259d09caa9d289e5e3237369eb66aaf5/api-14.pdf differ
diff --git a/main/_downloads/26cbb08b79302005a689c4e8dd87625d/api-5.pdf b/main/_downloads/26cbb08b79302005a689c4e8dd87625d/api-5.pdf
new file mode 100644
index 00000000..e4ba229a
Binary files /dev/null and b/main/_downloads/26cbb08b79302005a689c4e8dd87625d/api-5.pdf differ
diff --git a/main/_downloads/2978570c668d252b9da1fe49a06ec7ac/api-4.py b/main/_downloads/2978570c668d252b9da1fe49a06ec7ac/api-4.py
new file mode 100644
index 00000000..79f5f57d
--- /dev/null
+++ b/main/_downloads/2978570c668d252b9da1fe49a06ec7ac/api-4.py
@@ -0,0 +1,7 @@
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.specs import Line
+
+spec = Line("z", 1, 2, 3) * Line("y", 3, 4, 5).zip(Line("x", 4, 5, 5))
+plot_spec(spec)
\ No newline at end of file
diff --git a/main/_downloads/2cfa1a7efa170f3f496b6daff9bb2c49/creating-a-spec-6.hires.png b/main/_downloads/2cfa1a7efa170f3f496b6daff9bb2c49/creating-a-spec-6.hires.png
new file mode 100644
index 00000000..85815f29
Binary files /dev/null and b/main/_downloads/2cfa1a7efa170f3f496b6daff9bb2c49/creating-a-spec-6.hires.png differ
diff --git a/main/_downloads/32c9e69b107f5cd11ed75edfef7a521c/creating-a-spec-4.pdf b/main/_downloads/32c9e69b107f5cd11ed75edfef7a521c/creating-a-spec-4.pdf
new file mode 100644
index 00000000..dcd451a1
Binary files /dev/null and b/main/_downloads/32c9e69b107f5cd11ed75edfef7a521c/creating-a-spec-4.pdf differ
diff --git a/main/_downloads/36534493a6d19b6411e1895d931db289/creating-a-spec-1.hires.png b/main/_downloads/36534493a6d19b6411e1895d931db289/creating-a-spec-1.hires.png
new file mode 100644
index 00000000..60e7aef8
Binary files /dev/null and b/main/_downloads/36534493a6d19b6411e1895d931db289/creating-a-spec-1.hires.png differ
diff --git a/main/_downloads/37b8bccecbfd8cb6524cb6c081269149/api-9.py b/main/_downloads/37b8bccecbfd8cb6524cb6c081269149/api-9.py
new file mode 100644
index 00000000..d40cfb88
--- /dev/null
+++ b/main/_downloads/37b8bccecbfd8cb6524cb6c081269149/api-9.py
@@ -0,0 +1,7 @@
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.specs import Line
+
+spec = Line("x", 1, 2, 5)
+plot_spec(spec)
\ No newline at end of file
diff --git a/main/_downloads/38dd90e16fe49468a45d14588c6fac60/api-21.py b/main/_downloads/38dd90e16fe49468a45d14588c6fac60/api-21.py
new file mode 100644
index 00000000..87c0f949
--- /dev/null
+++ b/main/_downloads/38dd90e16fe49468a45d14588c6fac60/api-21.py
@@ -0,0 +1,9 @@
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.specs import Line
+from scanspec.regions import Circle
+
+cube = Line("z", 1, 3, 3) * Line("y", 1, 3, 10) * ~Line("x", 0, 2, 10)
+spec = cube & Circle("x", "y", 1, 2, 0.9)
+plot_spec(spec)
\ No newline at end of file
diff --git a/main/_downloads/39c6a274efd0966c49a486fadb2619df/api-17.pdf b/main/_downloads/39c6a274efd0966c49a486fadb2619df/api-17.pdf
new file mode 100644
index 00000000..b471e81e
Binary files /dev/null and b/main/_downloads/39c6a274efd0966c49a486fadb2619df/api-17.pdf differ
diff --git a/main/_downloads/3a1ec0a7c53d4117736e76290f8d5a07/api-21.hires.png b/main/_downloads/3a1ec0a7c53d4117736e76290f8d5a07/api-21.hires.png
new file mode 100644
index 00000000..7e96c75b
Binary files /dev/null and b/main/_downloads/3a1ec0a7c53d4117736e76290f8d5a07/api-21.hires.png differ
diff --git a/main/_downloads/3bd32d2e3c3a3b9ab90bef32dc7012ec/why-squash-can-change-path-2.png b/main/_downloads/3bd32d2e3c3a3b9ab90bef32dc7012ec/why-squash-can-change-path-2.png
new file mode 100644
index 00000000..8e0e6654
Binary files /dev/null and b/main/_downloads/3bd32d2e3c3a3b9ab90bef32dc7012ec/why-squash-can-change-path-2.png differ
diff --git a/main/_downloads/3dc26b59ff0d17f57c01d672ace5f20c/api-12.hires.png b/main/_downloads/3dc26b59ff0d17f57c01d672ace5f20c/api-12.hires.png
new file mode 100644
index 00000000..7c083252
Binary files /dev/null and b/main/_downloads/3dc26b59ff0d17f57c01d672ace5f20c/api-12.hires.png differ
diff --git a/main/_downloads/3f697368b6d069641068f1e1077fb6d0/api-10.pdf b/main/_downloads/3f697368b6d069641068f1e1077fb6d0/api-10.pdf
new file mode 100644
index 00000000..63826e04
Binary files /dev/null and b/main/_downloads/3f697368b6d069641068f1e1077fb6d0/api-10.pdf differ
diff --git a/main/_downloads/43c491207b6c41467113b7cb385139ba/api-13.pdf b/main/_downloads/43c491207b6c41467113b7cb385139ba/api-13.pdf
new file mode 100644
index 00000000..641b4777
Binary files /dev/null and b/main/_downloads/43c491207b6c41467113b7cb385139ba/api-13.pdf differ
diff --git a/main/_downloads/43d9474bab264cd0429cb1665f9955a8/api-15.pdf b/main/_downloads/43d9474bab264cd0429cb1665f9955a8/api-15.pdf
new file mode 100644
index 00000000..97bbff14
Binary files /dev/null and b/main/_downloads/43d9474bab264cd0429cb1665f9955a8/api-15.pdf differ
diff --git a/main/_downloads/46002abc3e44ab78d2ec586ac55ecbd5/api-4.pdf b/main/_downloads/46002abc3e44ab78d2ec586ac55ecbd5/api-4.pdf
new file mode 100644
index 00000000..a7dd9580
Binary files /dev/null and b/main/_downloads/46002abc3e44ab78d2ec586ac55ecbd5/api-4.pdf differ
diff --git a/main/_downloads/460d5ecae88292ff9f7461d1ce40d981/api-2.pdf b/main/_downloads/460d5ecae88292ff9f7461d1ce40d981/api-2.pdf
new file mode 100644
index 00000000..fa9cc33c
Binary files /dev/null and b/main/_downloads/460d5ecae88292ff9f7461d1ce40d981/api-2.pdf differ
diff --git a/main/_downloads/478bf00be18d7d4ce67afbd9964ddfd7/api-11.pdf b/main/_downloads/478bf00be18d7d4ce67afbd9964ddfd7/api-11.pdf
new file mode 100644
index 00000000..12eaf646
Binary files /dev/null and b/main/_downloads/478bf00be18d7d4ce67afbd9964ddfd7/api-11.pdf differ
diff --git a/main/_downloads/4be0e6fc9c722722574a702ec0235820/api-8.png b/main/_downloads/4be0e6fc9c722722574a702ec0235820/api-8.png
new file mode 100644
index 00000000..6423e51f
Binary files /dev/null and b/main/_downloads/4be0e6fc9c722722574a702ec0235820/api-8.png differ
diff --git a/main/_downloads/4bf4512437c120e94b6630677b316278/api-20.hires.png b/main/_downloads/4bf4512437c120e94b6630677b316278/api-20.hires.png
new file mode 100644
index 00000000..6cc0e647
Binary files /dev/null and b/main/_downloads/4bf4512437c120e94b6630677b316278/api-20.hires.png differ
diff --git a/main/_downloads/577932ffad65c458a1e7d7b06e2d619e/api-2.png b/main/_downloads/577932ffad65c458a1e7d7b06e2d619e/api-2.png
new file mode 100644
index 00000000..6cd4c9cc
Binary files /dev/null and b/main/_downloads/577932ffad65c458a1e7d7b06e2d619e/api-2.png differ
diff --git a/main/_downloads/5842467d595365eaa9c6e8a5e843a7f8/api-6.py b/main/_downloads/5842467d595365eaa9c6e8a5e843a7f8/api-6.py
new file mode 100644
index 00000000..f10a8074
--- /dev/null
+++ b/main/_downloads/5842467d595365eaa9c6e8a5e843a7f8/api-6.py
@@ -0,0 +1,7 @@
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.specs import Line
+
+spec = Line("y", 1, 3, 3) * ~Line("x", 3, 5, 5)
+plot_spec(spec)
\ No newline at end of file
diff --git a/main/_downloads/58e6bbb067ceb0cec82f5b4d950773d4/api-14.py b/main/_downloads/58e6bbb067ceb0cec82f5b4d950773d4/api-14.py
new file mode 100644
index 00000000..99605daf
--- /dev/null
+++ b/main/_downloads/58e6bbb067ceb0cec82f5b4d950773d4/api-14.py
@@ -0,0 +1,7 @@
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.specs import Spiral
+
+spec = Spiral.spaced("x", "y", 0, 0, 10, 3)
+plot_spec(spec)
\ No newline at end of file
diff --git a/main/_downloads/59ccb090bb2d3360ace4709fff9b5b3b/creating-a-spec-1.pdf b/main/_downloads/59ccb090bb2d3360ace4709fff9b5b3b/creating-a-spec-1.pdf
new file mode 100644
index 00000000..4657d683
Binary files /dev/null and b/main/_downloads/59ccb090bb2d3360ace4709fff9b5b3b/creating-a-spec-1.pdf differ
diff --git a/main/_downloads/5babad101a1fb402fa8708a1d700b2bd/api-12.pdf b/main/_downloads/5babad101a1fb402fa8708a1d700b2bd/api-12.pdf
new file mode 100644
index 00000000..3e2c1fc2
Binary files /dev/null and b/main/_downloads/5babad101a1fb402fa8708a1d700b2bd/api-12.pdf differ
diff --git a/main/_downloads/5c0624426b92b4d678699185e497f06b/creating-a-spec-6.pdf b/main/_downloads/5c0624426b92b4d678699185e497f06b/creating-a-spec-6.pdf
new file mode 100644
index 00000000..04d13f1c
Binary files /dev/null and b/main/_downloads/5c0624426b92b4d678699185e497f06b/creating-a-spec-6.pdf differ
diff --git a/main/_downloads/5e807c21e8499d45788e8cf4f1af03ea/api-14.png b/main/_downloads/5e807c21e8499d45788e8cf4f1af03ea/api-14.png
new file mode 100644
index 00000000..95833a43
Binary files /dev/null and b/main/_downloads/5e807c21e8499d45788e8cf4f1af03ea/api-14.png differ
diff --git a/main/_downloads/603e53497bfdb6e0d5f199b9f65883a6/creating-a-spec-1.png b/main/_downloads/603e53497bfdb6e0d5f199b9f65883a6/creating-a-spec-1.png
new file mode 100644
index 00000000..dd5773a0
Binary files /dev/null and b/main/_downloads/603e53497bfdb6e0d5f199b9f65883a6/creating-a-spec-1.png differ
diff --git a/main/_downloads/65c31687b904cef516424334e81d81e7/creating-a-spec-4.hires.png b/main/_downloads/65c31687b904cef516424334e81d81e7/creating-a-spec-4.hires.png
new file mode 100644
index 00000000..3315580e
Binary files /dev/null and b/main/_downloads/65c31687b904cef516424334e81d81e7/creating-a-spec-4.hires.png differ
diff --git a/main/_downloads/6799e6124974d7c56748316454a1d774/why-squash-can-change-path-3.png b/main/_downloads/6799e6124974d7c56748316454a1d774/why-squash-can-change-path-3.png
new file mode 100644
index 00000000..0db1b61c
Binary files /dev/null and b/main/_downloads/6799e6124974d7c56748316454a1d774/why-squash-can-change-path-3.png differ
diff --git a/main/_downloads/67ff8ce1c0b850eb1fb787c2ca6c05e0/api-15.png b/main/_downloads/67ff8ce1c0b850eb1fb787c2ca6c05e0/api-15.png
new file mode 100644
index 00000000..010d8e4a
Binary files /dev/null and b/main/_downloads/67ff8ce1c0b850eb1fb787c2ca6c05e0/api-15.png differ
diff --git a/main/_downloads/69b56bfc978abef83acd04290b4cbb62/api-7.py b/main/_downloads/69b56bfc978abef83acd04290b4cbb62/api-7.py
new file mode 100644
index 00000000..b1474a68
--- /dev/null
+++ b/main/_downloads/69b56bfc978abef83acd04290b4cbb62/api-7.py
@@ -0,0 +1,7 @@
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.specs import Line
+
+spec = Line("x", 1, 3, 3).concat(Line("x", 4, 5, 5))
+plot_spec(spec)
\ No newline at end of file
diff --git a/main/_downloads/6ac1affbbd25c62f767d0d736dcb9ac6/api-3.png b/main/_downloads/6ac1affbbd25c62f767d0d736dcb9ac6/api-3.png
new file mode 100644
index 00000000..e075a818
Binary files /dev/null and b/main/_downloads/6ac1affbbd25c62f767d0d736dcb9ac6/api-3.png differ
diff --git a/main/_downloads/6b2bfdefec22ee159c8ced5f7866a2cb/why-squash-can-change-path-4.pdf b/main/_downloads/6b2bfdefec22ee159c8ced5f7866a2cb/why-squash-can-change-path-4.pdf
new file mode 100644
index 00000000..298fae82
Binary files /dev/null and b/main/_downloads/6b2bfdefec22ee159c8ced5f7866a2cb/why-squash-can-change-path-4.pdf differ
diff --git a/main/_downloads/6c63be80402adee14ecfafb58cf23b0b/why-squash-can-change-path-4.hires.png b/main/_downloads/6c63be80402adee14ecfafb58cf23b0b/why-squash-can-change-path-4.hires.png
new file mode 100644
index 00000000..1ca4c5b2
Binary files /dev/null and b/main/_downloads/6c63be80402adee14ecfafb58cf23b0b/why-squash-can-change-path-4.hires.png differ
diff --git a/main/_downloads/6d4a97d0302014228a54f3420775b31e/creating-a-spec-5.png b/main/_downloads/6d4a97d0302014228a54f3420775b31e/creating-a-spec-5.png
new file mode 100644
index 00000000..fd59bddf
Binary files /dev/null and b/main/_downloads/6d4a97d0302014228a54f3420775b31e/creating-a-spec-5.png differ
diff --git a/main/_downloads/70cc095cb48d5ac8f7238a0a9117e378/api-19.pdf b/main/_downloads/70cc095cb48d5ac8f7238a0a9117e378/api-19.pdf
new file mode 100644
index 00000000..d9a1e470
Binary files /dev/null and b/main/_downloads/70cc095cb48d5ac8f7238a0a9117e378/api-19.pdf differ
diff --git a/main/_downloads/75ada40b0231b57cb1455511a2734d93/api-13.py b/main/_downloads/75ada40b0231b57cb1455511a2734d93/api-13.py
new file mode 100644
index 00000000..41d808fc
--- /dev/null
+++ b/main/_downloads/75ada40b0231b57cb1455511a2734d93/api-13.py
@@ -0,0 +1,7 @@
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.specs import Spiral
+
+spec = Spiral("x", "y", 1, 5, 10, 50, 30)
+plot_spec(spec)
\ No newline at end of file
diff --git a/main/_downloads/76d94101d031d187c5c0b8699a646d02/creating-a-spec-2.py b/main/_downloads/76d94101d031d187c5c0b8699a646d02/creating-a-spec-2.py
new file mode 100644
index 00000000..8294329b
--- /dev/null
+++ b/main/_downloads/76d94101d031d187c5c0b8699a646d02/creating-a-spec-2.py
@@ -0,0 +1,7 @@
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.specs import Line
+
+spec = Line("y", 3, 4, 5).zip(Line("x", 1, 2, 5))
+plot_spec(spec)
\ No newline at end of file
diff --git a/main/_downloads/78838f9d67c7edd54f15ce6bb14f6565/api-19.png b/main/_downloads/78838f9d67c7edd54f15ce6bb14f6565/api-19.png
new file mode 100644
index 00000000..c92ffa1b
Binary files /dev/null and b/main/_downloads/78838f9d67c7edd54f15ce6bb14f6565/api-19.png differ
diff --git a/main/_downloads/79becd8c0b77145922e83bcf9587b0df/api-9.pdf b/main/_downloads/79becd8c0b77145922e83bcf9587b0df/api-9.pdf
new file mode 100644
index 00000000..35ae628c
Binary files /dev/null and b/main/_downloads/79becd8c0b77145922e83bcf9587b0df/api-9.pdf differ
diff --git a/main/_downloads/7b36ae011c8fde871f2c725f4835b273/api-16.py b/main/_downloads/7b36ae011c8fde871f2c725f4835b273/api-16.py
new file mode 100644
index 00000000..e08f52ed
--- /dev/null
+++ b/main/_downloads/7b36ae011c8fde871f2c725f4835b273/api-16.py
@@ -0,0 +1,7 @@
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.specs import Line, step
+
+spec = step(Line("x", 1, 2, 3), 0.1)
+plot_spec(spec)
\ No newline at end of file
diff --git a/main/_downloads/7be99933a51115eeac94ea28dcc94a10/api-3.py b/main/_downloads/7be99933a51115eeac94ea28dcc94a10/api-3.py
new file mode 100644
index 00000000..00a898c7
--- /dev/null
+++ b/main/_downloads/7be99933a51115eeac94ea28dcc94a10/api-3.py
@@ -0,0 +1,7 @@
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.specs import Line, Repeat
+
+spec = Repeat(2, gap=False) * ~Line.bounded("x", 3, 4, 1)
+plot_spec(spec)
\ No newline at end of file
diff --git a/main/_downloads/7d2392d8c7ebe4d498d3806f3c33bbff/api-5.png b/main/_downloads/7d2392d8c7ebe4d498d3806f3c33bbff/api-5.png
new file mode 100644
index 00000000..bbb0a167
Binary files /dev/null and b/main/_downloads/7d2392d8c7ebe4d498d3806f3c33bbff/api-5.png differ
diff --git a/main/_downloads/7e03bd6318152a5ef2fc9f8f777a09fb/api-17.hires.png b/main/_downloads/7e03bd6318152a5ef2fc9f8f777a09fb/api-17.hires.png
new file mode 100644
index 00000000..b2affc3c
Binary files /dev/null and b/main/_downloads/7e03bd6318152a5ef2fc9f8f777a09fb/api-17.hires.png differ
diff --git a/main/_downloads/7f5d6f98d851a6f87ee17b4e1a2c477d/api-3.pdf b/main/_downloads/7f5d6f98d851a6f87ee17b4e1a2c477d/api-3.pdf
new file mode 100644
index 00000000..d463fe36
Binary files /dev/null and b/main/_downloads/7f5d6f98d851a6f87ee17b4e1a2c477d/api-3.pdf differ
diff --git a/main/_downloads/8000c8c6a016c74b895a2751917f06a8/api-11.hires.png b/main/_downloads/8000c8c6a016c74b895a2751917f06a8/api-11.hires.png
new file mode 100644
index 00000000..e91ff7ff
Binary files /dev/null and b/main/_downloads/8000c8c6a016c74b895a2751917f06a8/api-11.hires.png differ
diff --git a/main/_downloads/83bb1051f32a4e7b81ce75d2b6b73920/api-14.hires.png b/main/_downloads/83bb1051f32a4e7b81ce75d2b6b73920/api-14.hires.png
new file mode 100644
index 00000000..65f84a17
Binary files /dev/null and b/main/_downloads/83bb1051f32a4e7b81ce75d2b6b73920/api-14.hires.png differ
diff --git a/main/_downloads/84adf8120af3f73ccaffd95e693a1656/creating-a-spec-3.py b/main/_downloads/84adf8120af3f73ccaffd95e693a1656/creating-a-spec-3.py
new file mode 100644
index 00000000..1141c08b
--- /dev/null
+++ b/main/_downloads/84adf8120af3f73ccaffd95e693a1656/creating-a-spec-3.py
@@ -0,0 +1,7 @@
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.specs import Line
+
+spec = Line("y", 3, 4, 3) * Line("x", 1, 2, 5)
+plot_spec(spec)
\ No newline at end of file
diff --git a/main/_downloads/85f0d6120e17258c589906635c74454f/api-19.hires.png b/main/_downloads/85f0d6120e17258c589906635c74454f/api-19.hires.png
new file mode 100644
index 00000000..813dc60a
Binary files /dev/null and b/main/_downloads/85f0d6120e17258c589906635c74454f/api-19.hires.png differ
diff --git a/main/_downloads/86ad7000c8ad3d6b09b0ea0d99447521/api-15.py b/main/_downloads/86ad7000c8ad3d6b09b0ea0d99447521/api-15.py
new file mode 100644
index 00000000..34e91b66
--- /dev/null
+++ b/main/_downloads/86ad7000c8ad3d6b09b0ea0d99447521/api-15.py
@@ -0,0 +1,7 @@
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.specs import Line, fly
+
+spec = fly(Line("x", 1, 2, 3), 0.1)
+plot_spec(spec)
\ No newline at end of file
diff --git a/main/_downloads/883c460d6ca060f91e4934965c4e1fa1/api-13.hires.png b/main/_downloads/883c460d6ca060f91e4934965c4e1fa1/api-13.hires.png
new file mode 100644
index 00000000..545bca23
Binary files /dev/null and b/main/_downloads/883c460d6ca060f91e4934965c4e1fa1/api-13.hires.png differ
diff --git a/main/_downloads/896b7b6ce45fead8f85a9c99fda18032/api-10.py b/main/_downloads/896b7b6ce45fead8f85a9c99fda18032/api-10.py
new file mode 100644
index 00000000..bff58d33
--- /dev/null
+++ b/main/_downloads/896b7b6ce45fead8f85a9c99fda18032/api-10.py
@@ -0,0 +1,7 @@
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.specs import Line
+
+spec = Line.bounded("x", 1, 2, 5)
+plot_spec(spec)
\ No newline at end of file
diff --git a/main/_downloads/8b5e9b621cb85b9186abb508b0644353/api-18.png b/main/_downloads/8b5e9b621cb85b9186abb508b0644353/api-18.png
new file mode 100644
index 00000000..39cc28ed
Binary files /dev/null and b/main/_downloads/8b5e9b621cb85b9186abb508b0644353/api-18.png differ
diff --git a/main/_downloads/8d1f05c70d13d9eb2d237e906361b08f/api-4.hires.png b/main/_downloads/8d1f05c70d13d9eb2d237e906361b08f/api-4.hires.png
new file mode 100644
index 00000000..e2caf8e4
Binary files /dev/null and b/main/_downloads/8d1f05c70d13d9eb2d237e906361b08f/api-4.hires.png differ
diff --git a/main/_downloads/90481c9e5c2b3588d1b16f603ccb3a86/creating-a-spec-3.pdf b/main/_downloads/90481c9e5c2b3588d1b16f603ccb3a86/creating-a-spec-3.pdf
new file mode 100644
index 00000000..200fd691
Binary files /dev/null and b/main/_downloads/90481c9e5c2b3588d1b16f603ccb3a86/creating-a-spec-3.pdf differ
diff --git a/main/_downloads/904f5336e0d9ec9ed66ef034443f6f65/api-18.pdf b/main/_downloads/904f5336e0d9ec9ed66ef034443f6f65/api-18.pdf
new file mode 100644
index 00000000..78b90e9d
Binary files /dev/null and b/main/_downloads/904f5336e0d9ec9ed66ef034443f6f65/api-18.pdf differ
diff --git a/main/_downloads/920016fcda583eee66f9e3f3c37d90ca/api-1.pdf b/main/_downloads/920016fcda583eee66f9e3f3c37d90ca/api-1.pdf
new file mode 100644
index 00000000..06bea5df
Binary files /dev/null and b/main/_downloads/920016fcda583eee66f9e3f3c37d90ca/api-1.pdf differ
diff --git a/main/_downloads/9919f2f7caee56bb7f5ed0bbe278e0d3/api-7.png b/main/_downloads/9919f2f7caee56bb7f5ed0bbe278e0d3/api-7.png
new file mode 100644
index 00000000..6309f851
Binary files /dev/null and b/main/_downloads/9919f2f7caee56bb7f5ed0bbe278e0d3/api-7.png differ
diff --git a/main/_downloads/9a6730e0936263bd94ca9edd3a3abedb/api-21.pdf b/main/_downloads/9a6730e0936263bd94ca9edd3a3abedb/api-21.pdf
new file mode 100644
index 00000000..61d4fe0d
Binary files /dev/null and b/main/_downloads/9a6730e0936263bd94ca9edd3a3abedb/api-21.pdf differ
diff --git a/main/_downloads/9b040cc998e60252e63241cf421ff896/api-20.pdf b/main/_downloads/9b040cc998e60252e63241cf421ff896/api-20.pdf
new file mode 100644
index 00000000..52bd5ca6
Binary files /dev/null and b/main/_downloads/9b040cc998e60252e63241cf421ff896/api-20.pdf differ
diff --git a/main/_downloads/9b910feaa68e0b54f476927a47285377/api-2.hires.png b/main/_downloads/9b910feaa68e0b54f476927a47285377/api-2.hires.png
new file mode 100644
index 00000000..fd809205
Binary files /dev/null and b/main/_downloads/9b910feaa68e0b54f476927a47285377/api-2.hires.png differ
diff --git a/main/_downloads/9d36571ca3788038dbc94df5de05c3b5/why-squash-can-change-path-1.hires.png b/main/_downloads/9d36571ca3788038dbc94df5de05c3b5/why-squash-can-change-path-1.hires.png
new file mode 100644
index 00000000..c81e0a0c
Binary files /dev/null and b/main/_downloads/9d36571ca3788038dbc94df5de05c3b5/why-squash-can-change-path-1.hires.png differ
diff --git a/main/_downloads/a40a2da1920d27bcf335a4a10024addd/api-8.pdf b/main/_downloads/a40a2da1920d27bcf335a4a10024addd/api-8.pdf
new file mode 100644
index 00000000..b2940eee
Binary files /dev/null and b/main/_downloads/a40a2da1920d27bcf335a4a10024addd/api-8.pdf differ
diff --git a/main/_downloads/a717543d1b9fa515d50f9913c55a1965/api-12.py b/main/_downloads/a717543d1b9fa515d50f9913c55a1965/api-12.py
new file mode 100644
index 00000000..726271d9
--- /dev/null
+++ b/main/_downloads/a717543d1b9fa515d50f9913c55a1965/api-12.py
@@ -0,0 +1,7 @@
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.specs import Line, Static
+
+spec = Line("y", 1, 2, 3).zip(Static.duration(0.1))
+plot_spec(spec)
\ No newline at end of file
diff --git a/main/_downloads/ab723404048ec7c2b49037801c43777a/api-13.png b/main/_downloads/ab723404048ec7c2b49037801c43777a/api-13.png
new file mode 100644
index 00000000..75a31412
Binary files /dev/null and b/main/_downloads/ab723404048ec7c2b49037801c43777a/api-13.png differ
diff --git a/main/_downloads/af2c138d163058422058018877fb711d/api-7.pdf b/main/_downloads/af2c138d163058422058018877fb711d/api-7.pdf
new file mode 100644
index 00000000..16fe3731
Binary files /dev/null and b/main/_downloads/af2c138d163058422058018877fb711d/api-7.pdf differ
diff --git a/main/_downloads/b3f54cd984003c3ac9f1c2960a2517f3/creating-a-spec-4.py b/main/_downloads/b3f54cd984003c3ac9f1c2960a2517f3/creating-a-spec-4.py
new file mode 100644
index 00000000..1c263ea1
--- /dev/null
+++ b/main/_downloads/b3f54cd984003c3ac9f1c2960a2517f3/creating-a-spec-4.py
@@ -0,0 +1,7 @@
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.specs import Line
+
+spec = Line("y", 3, 4, 3) * ~Line("x", 1, 2, 5)
+plot_spec(spec)
\ No newline at end of file
diff --git a/main/_downloads/b45fd71d937091034bd5ae9a05ef55aa/api-20.png b/main/_downloads/b45fd71d937091034bd5ae9a05ef55aa/api-20.png
new file mode 100644
index 00000000..3f0e826d
Binary files /dev/null and b/main/_downloads/b45fd71d937091034bd5ae9a05ef55aa/api-20.png differ
diff --git a/main/_downloads/b61fec1d185d5d29d1735e1c5f45786d/api-1.py b/main/_downloads/b61fec1d185d5d29d1735e1c5f45786d/api-1.py
new file mode 100644
index 00000000..afdf3cbb
--- /dev/null
+++ b/main/_downloads/b61fec1d185d5d29d1735e1c5f45786d/api-1.py
@@ -0,0 +1,7 @@
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.specs import Line
+
+spec = Line("y", 1, 2, 3) * Line("x", 3, 4, 12)
+plot_spec(spec)
\ No newline at end of file
diff --git a/main/_downloads/b8c099a01c4b47dc4d1cd8976793725f/creating-a-spec-2.png b/main/_downloads/b8c099a01c4b47dc4d1cd8976793725f/creating-a-spec-2.png
new file mode 100644
index 00000000..e33f512b
Binary files /dev/null and b/main/_downloads/b8c099a01c4b47dc4d1cd8976793725f/creating-a-spec-2.png differ
diff --git a/main/_downloads/bace78a87ef701e7b08f72f39db31591/api-16.pdf b/main/_downloads/bace78a87ef701e7b08f72f39db31591/api-16.pdf
new file mode 100644
index 00000000..d2ed1803
Binary files /dev/null and b/main/_downloads/bace78a87ef701e7b08f72f39db31591/api-16.pdf differ
diff --git a/main/_downloads/bb85d706a002300ce5226b62a14ce5ff/api-9.hires.png b/main/_downloads/bb85d706a002300ce5226b62a14ce5ff/api-9.hires.png
new file mode 100644
index 00000000..60e7aef8
Binary files /dev/null and b/main/_downloads/bb85d706a002300ce5226b62a14ce5ff/api-9.hires.png differ
diff --git a/main/_downloads/bb8f2c460ed8d9d18885fd5afb672bb7/creating-a-spec-3.png b/main/_downloads/bb8f2c460ed8d9d18885fd5afb672bb7/creating-a-spec-3.png
new file mode 100644
index 00000000..54bbf893
Binary files /dev/null and b/main/_downloads/bb8f2c460ed8d9d18885fd5afb672bb7/creating-a-spec-3.png differ
diff --git a/main/_downloads/be0266aac130d905756ec129d729f864/why-squash-can-change-path-2.py b/main/_downloads/be0266aac130d905756ec129d729f864/why-squash-can-change-path-2.py
new file mode 100644
index 00000000..3da62c24
--- /dev/null
+++ b/main/_downloads/be0266aac130d905756ec129d729f864/why-squash-can-change-path-2.py
@@ -0,0 +1,7 @@
+from scanspec.specs import Line, Squash
+from scanspec.plot import plot_spec
+
+spec = Line("z", 0, 1, 3) * Squash(
+ ~Line("y", 0, 1, 3) * Line("x", 0, 1, 3), check_path_changes=False
+)
+plot_spec(spec)
\ No newline at end of file
diff --git a/main/_downloads/be0898ca638fe5e575dcef190808b2ea/api-21.png b/main/_downloads/be0898ca638fe5e575dcef190808b2ea/api-21.png
new file mode 100644
index 00000000..a31ba707
Binary files /dev/null and b/main/_downloads/be0898ca638fe5e575dcef190808b2ea/api-21.png differ
diff --git a/main/_downloads/be484758b050d137d624dd5eb901eb46/api-5.hires.png b/main/_downloads/be484758b050d137d624dd5eb901eb46/api-5.hires.png
new file mode 100644
index 00000000..6b46e4d6
Binary files /dev/null and b/main/_downloads/be484758b050d137d624dd5eb901eb46/api-5.hires.png differ
diff --git a/main/_downloads/be854064316e2e0362d1e7cac6859d21/why-squash-can-change-path-2.hires.png b/main/_downloads/be854064316e2e0362d1e7cac6859d21/why-squash-can-change-path-2.hires.png
new file mode 100644
index 00000000..c8ad1420
Binary files /dev/null and b/main/_downloads/be854064316e2e0362d1e7cac6859d21/why-squash-can-change-path-2.hires.png differ
diff --git a/main/_downloads/c1cf2d085f7ede85d58df873f9e4588b/api-2.py b/main/_downloads/c1cf2d085f7ede85d58df873f9e4588b/api-2.py
new file mode 100644
index 00000000..4a2e2b75
--- /dev/null
+++ b/main/_downloads/c1cf2d085f7ede85d58df873f9e4588b/api-2.py
@@ -0,0 +1,7 @@
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.specs import Line
+
+spec = 2 * ~Line.bounded("x", 3, 4, 1)
+plot_spec(spec)
\ No newline at end of file
diff --git a/main/_downloads/c2faec58e8a7872b8c73e00e780c4f8b/creating-a-spec-2.pdf b/main/_downloads/c2faec58e8a7872b8c73e00e780c4f8b/creating-a-spec-2.pdf
new file mode 100644
index 00000000..248783cf
Binary files /dev/null and b/main/_downloads/c2faec58e8a7872b8c73e00e780c4f8b/creating-a-spec-2.pdf differ
diff --git a/main/_downloads/c520cb1d7710fa3437e6b1e23ac6234b/api-16.hires.png b/main/_downloads/c520cb1d7710fa3437e6b1e23ac6234b/api-16.hires.png
new file mode 100644
index 00000000..cfda7f6a
Binary files /dev/null and b/main/_downloads/c520cb1d7710fa3437e6b1e23ac6234b/api-16.hires.png differ
diff --git a/main/_downloads/ca3cebb2d7d418d7c2435416c49b69a0/api-11.py b/main/_downloads/ca3cebb2d7d418d7c2435416c49b69a0/api-11.py
new file mode 100644
index 00000000..2132f391
--- /dev/null
+++ b/main/_downloads/ca3cebb2d7d418d7c2435416c49b69a0/api-11.py
@@ -0,0 +1,7 @@
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.specs import Line, Static
+
+spec = Line("y", 1, 2, 3).zip(Static("x", 3))
+plot_spec(spec)
\ No newline at end of file
diff --git a/main/_downloads/cafe10e32e48a4bdd0001eac84beaf9a/api-18.py b/main/_downloads/cafe10e32e48a4bdd0001eac84beaf9a/api-18.py
new file mode 100644
index 00000000..845343f9
--- /dev/null
+++ b/main/_downloads/cafe10e32e48a4bdd0001eac84beaf9a/api-18.py
@@ -0,0 +1,9 @@
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.regions import Polygon
+from scanspec.specs import Line
+
+grid = Line("y", 3, 8, 10) * ~Line("x", 1 ,8, 10)
+spec = grid & Polygon("x", "y", [1.0, 6.0, 8.0, 2.0], [4.0, 10.0, 6.0, 1.0])
+plot_spec(spec)
\ No newline at end of file
diff --git a/main/_downloads/cc141685a493616a8f29e461c61b1b0b/api-6.pdf b/main/_downloads/cc141685a493616a8f29e461c61b1b0b/api-6.pdf
new file mode 100644
index 00000000..67c244b8
Binary files /dev/null and b/main/_downloads/cc141685a493616a8f29e461c61b1b0b/api-6.pdf differ
diff --git a/main/_downloads/cf755cae62efcddb79d35f345aaba8ba/creating-a-spec-6.py b/main/_downloads/cf755cae62efcddb79d35f345aaba8ba/creating-a-spec-6.py
new file mode 100644
index 00000000..48213384
--- /dev/null
+++ b/main/_downloads/cf755cae62efcddb79d35f345aaba8ba/creating-a-spec-6.py
@@ -0,0 +1,8 @@
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.specs import Line
+from scanspec.regions import Circle
+
+spec = Line("y", 3, 4, 3) * ~Line("x", 1, 2, 5) & Circle("x", "y", 1.5, 3.5, 0.6) - Circle("x", "y", 1.4, 3.5, 0.2)
+plot_spec(spec)
\ No newline at end of file
diff --git a/main/_downloads/d022f15746f7fd9411859574d137f187/api-17.png b/main/_downloads/d022f15746f7fd9411859574d137f187/api-17.png
new file mode 100644
index 00000000..fc2e1085
Binary files /dev/null and b/main/_downloads/d022f15746f7fd9411859574d137f187/api-17.png differ
diff --git a/main/_downloads/d03a8e736b7010f35754bcd1962e6322/api-16.png b/main/_downloads/d03a8e736b7010f35754bcd1962e6322/api-16.png
new file mode 100644
index 00000000..20c3e3d5
Binary files /dev/null and b/main/_downloads/d03a8e736b7010f35754bcd1962e6322/api-16.png differ
diff --git a/main/_downloads/d169d8ec24761bcab348c4e74c47d7cd/api-7.hires.png b/main/_downloads/d169d8ec24761bcab348c4e74c47d7cd/api-7.hires.png
new file mode 100644
index 00000000..a46c6a2a
Binary files /dev/null and b/main/_downloads/d169d8ec24761bcab348c4e74c47d7cd/api-7.hires.png differ
diff --git a/main/_downloads/d31ee89c9fe4d6d0e70214e6a0e8a4df/creating-a-spec-5.py b/main/_downloads/d31ee89c9fe4d6d0e70214e6a0e8a4df/creating-a-spec-5.py
new file mode 100644
index 00000000..d66bcd9a
--- /dev/null
+++ b/main/_downloads/d31ee89c9fe4d6d0e70214e6a0e8a4df/creating-a-spec-5.py
@@ -0,0 +1,8 @@
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.specs import Line
+from scanspec.regions import Circle
+
+spec = Line("y", 3, 4, 3) * ~Line("x", 1, 2, 5) & Circle("x", "y", 1.5, 3.5, 0.6)
+plot_spec(spec)
\ No newline at end of file
diff --git a/main/_downloads/d565b1741d4d24df3efab01f0f22b1f9/api-11.png b/main/_downloads/d565b1741d4d24df3efab01f0f22b1f9/api-11.png
new file mode 100644
index 00000000..04a1413a
Binary files /dev/null and b/main/_downloads/d565b1741d4d24df3efab01f0f22b1f9/api-11.png differ
diff --git a/main/_downloads/d5d933689d67f07042600b892a1fa576/creating-a-spec-6.png b/main/_downloads/d5d933689d67f07042600b892a1fa576/creating-a-spec-6.png
new file mode 100644
index 00000000..1682d72b
Binary files /dev/null and b/main/_downloads/d5d933689d67f07042600b892a1fa576/creating-a-spec-6.png differ
diff --git a/main/_downloads/d63649a5a6f78b5a752e39a4af7c1464/api-1.png b/main/_downloads/d63649a5a6f78b5a752e39a4af7c1464/api-1.png
new file mode 100644
index 00000000..e9e24a1e
Binary files /dev/null and b/main/_downloads/d63649a5a6f78b5a752e39a4af7c1464/api-1.png differ
diff --git a/main/_downloads/d710b479db32b9052157f9dc9f9d4f05/api-3.hires.png b/main/_downloads/d710b479db32b9052157f9dc9f9d4f05/api-3.hires.png
new file mode 100644
index 00000000..6947f592
Binary files /dev/null and b/main/_downloads/d710b479db32b9052157f9dc9f9d4f05/api-3.hires.png differ
diff --git a/main/_downloads/d9359aa24479d0d7ffa6257851863adf/api-8.py b/main/_downloads/d9359aa24479d0d7ffa6257851863adf/api-8.py
new file mode 100644
index 00000000..1cc7c4e1
--- /dev/null
+++ b/main/_downloads/d9359aa24479d0d7ffa6257851863adf/api-8.py
@@ -0,0 +1,7 @@
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.specs import Line, Squash
+
+spec = Squash(Line("y", 1, 2, 3) * Line("x", 0, 1, 4))
+plot_spec(spec)
\ No newline at end of file
diff --git a/main/_downloads/de6ea31b8c00d27a29758d057fc4b397/api-12.png b/main/_downloads/de6ea31b8c00d27a29758d057fc4b397/api-12.png
new file mode 100644
index 00000000..48446dc7
Binary files /dev/null and b/main/_downloads/de6ea31b8c00d27a29758d057fc4b397/api-12.png differ
diff --git a/main/_downloads/dfd125f8fe26e983a096e8d2be420600/creating-a-spec-2.hires.png b/main/_downloads/dfd125f8fe26e983a096e8d2be420600/creating-a-spec-2.hires.png
new file mode 100644
index 00000000..ac04be9b
Binary files /dev/null and b/main/_downloads/dfd125f8fe26e983a096e8d2be420600/creating-a-spec-2.hires.png differ
diff --git a/main/_downloads/e39ca05263713d71f3f88356c2144068/api-8.hires.png b/main/_downloads/e39ca05263713d71f3f88356c2144068/api-8.hires.png
new file mode 100644
index 00000000..ea6b33dd
Binary files /dev/null and b/main/_downloads/e39ca05263713d71f3f88356c2144068/api-8.hires.png differ
diff --git a/main/_downloads/e5428a1bf8c516ed6cecc74fbbe0de8f/why-squash-can-change-path-3.py b/main/_downloads/e5428a1bf8c516ed6cecc74fbbe0de8f/why-squash-can-change-path-3.py
new file mode 100644
index 00000000..5b61317d
--- /dev/null
+++ b/main/_downloads/e5428a1bf8c516ed6cecc74fbbe0de8f/why-squash-can-change-path-3.py
@@ -0,0 +1,5 @@
+from scanspec.specs import Line
+from scanspec.plot import plot_spec
+
+spec = Line("z", 0, 1, 3) * Line("y", 0, 1, 3) * ~Line("x", 0, 1, 3)
+plot_spec(spec)
\ No newline at end of file
diff --git a/main/_downloads/e65a64116288e7b0b97b63ffe2ab4bbb/why-squash-can-change-path-1.png b/main/_downloads/e65a64116288e7b0b97b63ffe2ab4bbb/why-squash-can-change-path-1.png
new file mode 100644
index 00000000..de0e6578
Binary files /dev/null and b/main/_downloads/e65a64116288e7b0b97b63ffe2ab4bbb/why-squash-can-change-path-1.png differ
diff --git a/main/_downloads/e8b59d332e2b76ad4a589b1ce152994c/api-5.py b/main/_downloads/e8b59d332e2b76ad4a589b1ce152994c/api-5.py
new file mode 100644
index 00000000..68dd4100
--- /dev/null
+++ b/main/_downloads/e8b59d332e2b76ad4a589b1ce152994c/api-5.py
@@ -0,0 +1,8 @@
+# Example Spec
+
+from scanspec.plot import plot_spec
+from scanspec.regions import Circle
+from scanspec.specs import Line
+
+spec = Line("y", 1, 3, 3) * Line("x", 3, 5, 5) & Circle("x", "y", 4, 2, 1.2)
+plot_spec(spec)
\ No newline at end of file
diff --git a/main/_downloads/e97e6c4ea088783de917c4b5859e1595/why-squash-can-change-path-1.pdf b/main/_downloads/e97e6c4ea088783de917c4b5859e1595/why-squash-can-change-path-1.pdf
new file mode 100644
index 00000000..d615b76e
Binary files /dev/null and b/main/_downloads/e97e6c4ea088783de917c4b5859e1595/why-squash-can-change-path-1.pdf differ
diff --git a/main/_downloads/eb327b11365631cc9b00d79fb4c39112/api-10.png b/main/_downloads/eb327b11365631cc9b00d79fb4c39112/api-10.png
new file mode 100644
index 00000000..fa4c14ba
Binary files /dev/null and b/main/_downloads/eb327b11365631cc9b00d79fb4c39112/api-10.png differ
diff --git a/main/_downloads/ec8d232d0953cdaac6529cb67293b0e8/creating-a-spec-5.pdf b/main/_downloads/ec8d232d0953cdaac6529cb67293b0e8/creating-a-spec-5.pdf
new file mode 100644
index 00000000..6d381a0a
Binary files /dev/null and b/main/_downloads/ec8d232d0953cdaac6529cb67293b0e8/creating-a-spec-5.pdf differ
diff --git a/main/_downloads/edf4182424d7c4118287d2ea50bef6bb/why-squash-can-change-path-2.pdf b/main/_downloads/edf4182424d7c4118287d2ea50bef6bb/why-squash-can-change-path-2.pdf
new file mode 100644
index 00000000..aba09680
Binary files /dev/null and b/main/_downloads/edf4182424d7c4118287d2ea50bef6bb/why-squash-can-change-path-2.pdf differ
diff --git a/main/_downloads/f14e86302c9aa0c1ba37850ec22c308e/api-18.hires.png b/main/_downloads/f14e86302c9aa0c1ba37850ec22c308e/api-18.hires.png
new file mode 100644
index 00000000..248823e9
Binary files /dev/null and b/main/_downloads/f14e86302c9aa0c1ba37850ec22c308e/api-18.hires.png differ
diff --git a/main/_downloads/f169874319858b7c51d001cb0d90893f/api-4.png b/main/_downloads/f169874319858b7c51d001cb0d90893f/api-4.png
new file mode 100644
index 00000000..a09eaa89
Binary files /dev/null and b/main/_downloads/f169874319858b7c51d001cb0d90893f/api-4.png differ
diff --git a/main/_downloads/f25ee9ccc9cf43c3c3addcdab46ff433/creating-a-spec-5.hires.png b/main/_downloads/f25ee9ccc9cf43c3c3addcdab46ff433/creating-a-spec-5.hires.png
new file mode 100644
index 00000000..ff6d14a1
Binary files /dev/null and b/main/_downloads/f25ee9ccc9cf43c3c3addcdab46ff433/creating-a-spec-5.hires.png differ
diff --git a/main/_downloads/f6a8ef27bfb1b6c7859588df9191a864/api-6.hires.png b/main/_downloads/f6a8ef27bfb1b6c7859588df9191a864/api-6.hires.png
new file mode 100644
index 00000000..7b35ab21
Binary files /dev/null and b/main/_downloads/f6a8ef27bfb1b6c7859588df9191a864/api-6.hires.png differ
diff --git a/main/_downloads/f9bd088472d91e6b992cd9d75b57f556/api-9.png b/main/_downloads/f9bd088472d91e6b992cd9d75b57f556/api-9.png
new file mode 100644
index 00000000..dd5773a0
Binary files /dev/null and b/main/_downloads/f9bd088472d91e6b992cd9d75b57f556/api-9.png differ
diff --git a/main/_downloads/ff9fe3a1c02bde9717bcf50e7f8dd25b/api-1.hires.png b/main/_downloads/ff9fe3a1c02bde9717bcf50e7f8dd25b/api-1.hires.png
new file mode 100644
index 00000000..a93b5bc3
Binary files /dev/null and b/main/_downloads/ff9fe3a1c02bde9717bcf50e7f8dd25b/api-1.hires.png differ
diff --git a/main/_images/api-1.png b/main/_images/api-1.png
new file mode 100644
index 00000000..e9e24a1e
Binary files /dev/null and b/main/_images/api-1.png differ
diff --git a/main/_images/api-10.png b/main/_images/api-10.png
new file mode 100644
index 00000000..fa4c14ba
Binary files /dev/null and b/main/_images/api-10.png differ
diff --git a/main/_images/api-11.png b/main/_images/api-11.png
new file mode 100644
index 00000000..04a1413a
Binary files /dev/null and b/main/_images/api-11.png differ
diff --git a/main/_images/api-12.png b/main/_images/api-12.png
new file mode 100644
index 00000000..48446dc7
Binary files /dev/null and b/main/_images/api-12.png differ
diff --git a/main/_images/api-13.png b/main/_images/api-13.png
new file mode 100644
index 00000000..75a31412
Binary files /dev/null and b/main/_images/api-13.png differ
diff --git a/main/_images/api-14.png b/main/_images/api-14.png
new file mode 100644
index 00000000..95833a43
Binary files /dev/null and b/main/_images/api-14.png differ
diff --git a/main/_images/api-15.png b/main/_images/api-15.png
new file mode 100644
index 00000000..010d8e4a
Binary files /dev/null and b/main/_images/api-15.png differ
diff --git a/main/_images/api-16.png b/main/_images/api-16.png
new file mode 100644
index 00000000..20c3e3d5
Binary files /dev/null and b/main/_images/api-16.png differ
diff --git a/main/_images/api-17.png b/main/_images/api-17.png
new file mode 100644
index 00000000..fc2e1085
Binary files /dev/null and b/main/_images/api-17.png differ
diff --git a/main/_images/api-18.png b/main/_images/api-18.png
new file mode 100644
index 00000000..39cc28ed
Binary files /dev/null and b/main/_images/api-18.png differ
diff --git a/main/_images/api-19.png b/main/_images/api-19.png
new file mode 100644
index 00000000..c92ffa1b
Binary files /dev/null and b/main/_images/api-19.png differ
diff --git a/main/_images/api-2.png b/main/_images/api-2.png
new file mode 100644
index 00000000..6cd4c9cc
Binary files /dev/null and b/main/_images/api-2.png differ
diff --git a/main/_images/api-20.png b/main/_images/api-20.png
new file mode 100644
index 00000000..3f0e826d
Binary files /dev/null and b/main/_images/api-20.png differ
diff --git a/main/_images/api-21.png b/main/_images/api-21.png
new file mode 100644
index 00000000..a31ba707
Binary files /dev/null and b/main/_images/api-21.png differ
diff --git a/main/_images/api-3.png b/main/_images/api-3.png
new file mode 100644
index 00000000..e075a818
Binary files /dev/null and b/main/_images/api-3.png differ
diff --git a/main/_images/api-4.png b/main/_images/api-4.png
new file mode 100644
index 00000000..a09eaa89
Binary files /dev/null and b/main/_images/api-4.png differ
diff --git a/main/_images/api-5.png b/main/_images/api-5.png
new file mode 100644
index 00000000..bbb0a167
Binary files /dev/null and b/main/_images/api-5.png differ
diff --git a/main/_images/api-6.png b/main/_images/api-6.png
new file mode 100644
index 00000000..e7eb3e03
Binary files /dev/null and b/main/_images/api-6.png differ
diff --git a/main/_images/api-7.png b/main/_images/api-7.png
new file mode 100644
index 00000000..6309f851
Binary files /dev/null and b/main/_images/api-7.png differ
diff --git a/main/_images/api-8.png b/main/_images/api-8.png
new file mode 100644
index 00000000..6423e51f
Binary files /dev/null and b/main/_images/api-8.png differ
diff --git a/main/_images/api-9.png b/main/_images/api-9.png
new file mode 100644
index 00000000..dd5773a0
Binary files /dev/null and b/main/_images/api-9.png differ
diff --git a/main/_images/creating-a-spec-1.png b/main/_images/creating-a-spec-1.png
new file mode 100644
index 00000000..dd5773a0
Binary files /dev/null and b/main/_images/creating-a-spec-1.png differ
diff --git a/main/_images/creating-a-spec-2.png b/main/_images/creating-a-spec-2.png
new file mode 100644
index 00000000..e33f512b
Binary files /dev/null and b/main/_images/creating-a-spec-2.png differ
diff --git a/main/_images/creating-a-spec-3.png b/main/_images/creating-a-spec-3.png
new file mode 100644
index 00000000..54bbf893
Binary files /dev/null and b/main/_images/creating-a-spec-3.png differ
diff --git a/main/_images/creating-a-spec-4.png b/main/_images/creating-a-spec-4.png
new file mode 100644
index 00000000..8e161c10
Binary files /dev/null and b/main/_images/creating-a-spec-4.png differ
diff --git a/main/_images/creating-a-spec-5.png b/main/_images/creating-a-spec-5.png
new file mode 100644
index 00000000..fd59bddf
Binary files /dev/null and b/main/_images/creating-a-spec-5.png differ
diff --git a/main/_images/creating-a-spec-6.png b/main/_images/creating-a-spec-6.png
new file mode 100644
index 00000000..1682d72b
Binary files /dev/null and b/main/_images/creating-a-spec-6.png differ
diff --git a/main/_images/definitions.png b/main/_images/definitions.png
new file mode 100644
index 00000000..8fc7b9ae
Binary files /dev/null and b/main/_images/definitions.png differ
diff --git a/main/_images/inheritance-7560b70421c624944f0f13c4b7fad08c42206964.svg b/main/_images/inheritance-7560b70421c624944f0f13c4b7fad08c42206964.svg
new file mode 100644
index 00000000..3d4ab6fd
--- /dev/null
+++ b/main/_images/inheritance-7560b70421c624944f0f13c4b7fad08c42206964.svg
@@ -0,0 +1,164 @@
+
+from __future__ import annotations
+
+from dataclasses import field
+from typing import (
+ Any,
+ Callable,
+ Dict,
+ Generic,
+ Iterable,
+ Iterator,
+ List,
+ Optional,
+ Sequence,
+ Type,
+ TypeVar,
+ Union,
+)
+
+import numpy as np
+from pydantic import BaseConfig, Extra, Field, ValidationError, create_model
+from pydantic.error_wrappers import ErrorWrapper
+from typing_extensions import Literal
+
+__all__ = [
+ "if_instance_do",
+ "Axis",
+ "AxesPoints",
+ "Frames",
+ "SnakedFrames",
+ "gap_between_frames",
+ "squash_frames",
+ "Path",
+ "Midpoints",
+ "discriminated_union_of_subclasses",
+ "StrictConfig",
+]
+
+
+
+[docs]
+class StrictConfig(BaseConfig):
+ """Pydantic configuration for scanspecs and regions."""
+
+ extra: Extra = Extra.forbid
+
+
+
+
+[docs]
+def discriminated_union_of_subclasses(
+ super_cls: Optional[Type] = None,
+ *,
+ discriminator: str = "type",
+ config: Optional[Type[BaseConfig]] = None,
+) -> Union[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": <type name>, params for
+ <type name>...}.
+ Add validation methods to super_cls so it can be parsed by pydantic.parse_obj_as.
+
+ Example::
+
+ @discriminated_union_of_subclasses
+ class Expression(ABC):
+ @abstractmethod
+ def calculate(self) -> int:
+ ...
+
+
+ @dataclass
+ class Add(Expression):
+ left: Expression
+ right: Expression
+
+ def calculate(self) -> int:
+ return self.left.calculate() + self.right.calculate()
+
+
+ @dataclass
+ class Subtract(Expression):
+ left: Expression
+ right: Expression
+
+ def calculate(self) -> int:
+ return self.left.calculate() - self.right.calculate()
+
+
+ @dataclass
+ class IntLiteral(Expression):
+ value: int
+
+ def calculate(self) -> int:
+ return self.value
+
+
+ my_sum = Add(IntLiteral(5), Subtract(IntLiteral(10), IntLiteral(2)))
+ assert my_sum.calculate() == 13
+
+ assert my_sum == parse_obj_as(
+ Expression,
+ {
+ "type": "Add",
+ "left": {"type": "IntLiteral", "value": 5},
+ "right": {
+ "type": "Subtract",
+ "left": {"type": "IntLiteral", "value": 10},
+ "right": {"type": "IntLiteral", "value": 2},
+ },
+ },
+ )
+
+ Args:
+ super_cls: The superclass of the union, Expression in the above example
+ discriminator: The discriminator that will be inserted into the
+ serialized documents for type determination. Defaults to "type".
+ config: A pydantic config class to be inserted into all
+ subclasses. Defaults to None.
+
+ Returns:
+ Union[Type, Callable[[Type], Type]]: A decorator that adds the necessary
+ functionality to a class.
+ """
+
+ 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: Optional[Type[BaseConfig]] = None,
+) -> Union[Type, Callable[[Type], Type]]:
+
+ super_cls._ref_classes = set()
+ super_cls._model = None
+
+ 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
+ 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
+
+
+
+[docs]
+def if_instance_do(x: Any, cls: Type, func: Callable):
+ """If x is of type cls then return func(x), otherwise return NotImplemented.
+
+ Used as a helper when implementing operator overloading.
+ """
+ if isinstance(x, cls):
+ return func(x)
+ else:
+ return NotImplemented
+
+
+
+#: A type variable for an `axis_` that can be specified for a scan
+Axis = TypeVar("Axis")
+
+#: Map of axes to float ndarray of points
+#: E.g. {xmotor: array([0, 1, 2]), ymotor: array([2, 2, 2])}
+AxesPoints = Dict[Axis, np.ndarray]
+
+
+
+[docs]
+class Frames(Generic[Axis]):
+ """Represents a series of scan frames along a number of axes.
+
+ During a scan each axis will traverse lower-midpoint-upper for each frame.
+
+ Args:
+ midpoints: The midpoints of scan frames for each axis
+ lower: Lower bounds of scan frames if different from midpoints
+ upper: Upper bounds of scan frames if different from midpoints
+ gap: If supplied, define if there is a gap between frame and previous
+ otherwise it is calculated by looking at lower and upper bounds
+
+ Typically used in two ways:
+
+ - A list of Frames objects returned from `Spec.calculate` represents a scan
+ as a linear stack of frames. Interpreted as nested from slowest moving to
+ fastest moving, so each faster Frames object will iterate once per
+ position of the slower Frames object. It is passed to a `Path` for
+ calculation of the actual scan path.
+ - A single Frames object returned from `Path.consume` represents a chunk of
+ frames forming part of a scan path, for interpretation by the code
+ that will actually perform the scan.
+
+ See Also:
+ `technical-terms`
+ """
+
+ def __init__(
+ self,
+ midpoints: AxesPoints[Axis],
+ lower: Optional[AxesPoints[Axis]] = None,
+ upper: Optional[AxesPoints[Axis]] = None,
+ gap: Optional[np.ndarray] = None,
+ ):
+ #: The midpoints of scan frames for each axis
+ self.midpoints = midpoints
+ #: The lower bounds of each scan frame in each axis for fly-scanning
+ self.lower = lower or midpoints
+ #: The upper bounds of each scan frame in each axis for fly-scanning
+ self.upper = upper or midpoints
+ if gap is not None:
+ #: Whether there is a gap between this frame and the previous. First
+ #: element is whether there is a gap between the last frame and the first
+ self.gap = gap
+ else:
+ # Need to calculate gap as not passed one
+ # We have a gap if upper[i] != lower[i+1] for any axes
+ axes_gap = [
+ np.roll(u, 1) != l
+ for u, l in zip(self.upper.values(), self.lower.values())
+ ]
+ self.gap = np.logical_or.reduce(axes_gap)
+ # Check all axes and ordering are the same
+ assert list(self.midpoints) == list(self.lower) == list(self.upper), (
+ f"Mismatching axes "
+ f"{list(self.midpoints)} != {list(self.lower)} != {list(self.upper)}"
+ )
+ # Check all lengths are the same
+ lengths = set(
+ len(arr)
+ for d in (self.midpoints, self.lower, self.upper)
+ for arr in d.values()
+ )
+ lengths.add(len(self.gap))
+ assert len(lengths) <= 1, f"Mismatching lengths {list(lengths)}"
+
+
+[docs]
+ def axes(self) -> List[Axis]:
+ """The axes which will move during the scan.
+
+ These will be present in `midpoints`, `lower` and `upper`.
+ """
+ return list(self.midpoints.keys())
+
+
+ def __len__(self) -> int:
+ """The number of frames in this section of the scan."""
+ # All axespoints arrays are same length, pick the first one
+ return len(self.gap)
+
+
+[docs]
+ def extract(self, indices: np.ndarray, calculate_gap=True) -> Frames[Axis]:
+ """Return a new Frames object restricted to the indices provided.
+
+ Args:
+ indices: The indices of the frames to extract, modulo scan length
+ calculate_gap: If True then recalculate the gap from upper and lower
+
+ >>> frames = Frames({"x": np.array([1, 2, 3])})
+ >>> frames.extract(np.array([1, 0, 1])).midpoints
+ {'x': array([2, 1, 2])}
+ """
+ dim_indices = indices % len(self)
+
+ def extract_dict(ds: Iterable[AxesPoints[Axis]]) -> AxesPoints[Axis]:
+ for d in ds:
+ return {k: v[dim_indices] for k, v in d.items()}
+ return {}
+
+ def extract_gap(gaps: Iterable[np.ndarray]) -> Optional[np.ndarray]:
+ for gap in gaps:
+ if not calculate_gap:
+ return gap[dim_indices]
+ return None
+
+ return _merge_frames(self, dict_merge=extract_dict, gap_merge=extract_gap)
+
+
+
+[docs]
+ def concat(self, other: Frames[Axis], gap: bool = False) -> Frames[Axis]:
+ """Return a new Frames object concatenating self and other.
+
+ Requires both Frames objects to have the same axes, but not necessarily in
+ the same order. The order is inherited from self, so other may be reordered.
+
+ Args:
+ other: The Frames to concatenate to self
+ gap: Whether to force a gap between the two Frames objects
+
+ >>> frames = Frames({"x": np.array([1, 2, 3]), "y": np.array([6, 5, 4])})
+ >>> frames2 = Frames({"y": np.array([3, 2, 1]), "x": np.array([4, 5, 6])})
+ >>> frames.concat(frames2).midpoints
+ {'x': array([1, 2, 3, 4, 5, 6]), 'y': array([6, 5, 4, 3, 2, 1])}
+ """
+ assert set(self.axes()) == set(
+ other.axes()
+ ), f"axes {self.axes()} != {other.axes()}"
+
+ def concat_dict(ds: Sequence[AxesPoints[Axis]]) -> AxesPoints[Axis]:
+ # Concat each array in midpoints, lower, upper. E.g.
+ # lower[ax] = np.concatenate(self.lower[ax], other.lower[ax])
+ return {a: np.concatenate([d[a] for d in ds]) for a in self.axes()}
+
+ def concat_gap(gaps: Sequence[np.ndarray]) -> np.ndarray:
+ g = np.concatenate(gaps)
+ # Calc the first frame
+ g[0] = gap_between_frames(other, self)
+ # And the join frame
+ g[len(self)] = gap or gap_between_frames(self, other)
+ return g
+
+ return _merge_frames(self, other, dict_merge=concat_dict, gap_merge=concat_gap)
+
+
+
+[docs]
+ def zip(self, other: Frames[Axis]) -> Frames[Axis]:
+ """Return a new Frames object merging self and other.
+
+ Require both Frames objects to not share axes.
+
+ >>> fx = Frames({"x": np.array([1, 2, 3])})
+ >>> fy = Frames({"y": np.array([5, 6, 7])})
+ >>> fx.zip(fy).midpoints
+ {'x': array([1, 2, 3]), 'y': array([5, 6, 7])}
+ """
+ overlapping = list(set(self.axes()).intersection(other.axes()))
+ assert not overlapping, f"Zipping would overwrite axes {overlapping}"
+
+ def zip_dict(ds: Sequence[AxesPoints[Axis]]) -> AxesPoints[Axis]:
+ # Merge dicts for midpoints, lower, upper. E.g.
+ # lower[ax] = {**self.lower[ax], **other.lower[ax]}
+ return dict(kv for d in ds for kv in d.items())
+
+ def zip_gap(gaps: Sequence[np.ndarray]) -> np.ndarray:
+ # Gap if either frames has a gap. E.g.
+ # gap[i] = self.gap[i] | other.gap[i]
+ return np.logical_or.reduce(gaps)
+
+ return _merge_frames(self, other, dict_merge=zip_dict, gap_merge=zip_gap)
+
+
+
+
+def _merge_frames(
+ *stack: Frames[Axis],
+ dict_merge=Callable[[Sequence[AxesPoints[Axis]]], AxesPoints[Axis]],
+ gap_merge=Callable[[Sequence[np.ndarray]], Optional[np.ndarray]],
+) -> Frames[Axis]:
+ types = set(type(fs) for fs in stack)
+ assert len(types) == 1, f"Mismatching types for {stack}"
+ cls = types.pop()
+
+ # If any lower or upper are different, apply to those
+ kwargs = {}
+ for a in ("lower", "upper"):
+ if any(fs.midpoints is not getattr(fs, a) for fs in stack):
+ kwargs[a] = dict_merge([getattr(fs, a) for fs in stack])
+
+ # Apply to midpoints, force calculation of gap
+ return cls(
+ midpoints=dict_merge([fs.midpoints for fs in stack]),
+ gap=gap_merge([fs.gap for fs in stack]),
+ **kwargs,
+ )
+
+
+
+[docs]
+class SnakedFrames(Frames[Axis]):
+ """Like a `Frames` object, but each alternate repetition will run in reverse."""
+
+ def __init__(
+ self,
+ midpoints: AxesPoints[Axis],
+ lower: Optional[AxesPoints[Axis]] = None,
+ upper: Optional[AxesPoints[Axis]] = None,
+ gap: Optional[np.ndarray] = None,
+ ):
+ super().__init__(midpoints, lower=lower, upper=upper, gap=gap)
+ # Override first element of gap to be True, as subsequent runs
+ # of snake scans are always joined end -> start
+ self.gap[0] = False
+
+
+[docs]
+ @classmethod
+ def from_frames(cls, frames: Frames[Axis]) -> SnakedFrames[Axis]:
+ """Create a snaked version of a `Frames` object."""
+ return cls(frames.midpoints, frames.lower, frames.upper, frames.gap)
+
+
+
+[docs]
+ def extract(self, indices: np.ndarray, calculate_gap=True) -> Frames[Axis]:
+ """Return a new Frames object restricted to the indices provided.
+
+ Args:
+ indices: The indices of the frames to extract, can extend past len(self)
+ calculate_gap: If True then recalculate the gap from upper and lower
+
+ >>> frames = SnakedFrames({"x": np.array([1, 2, 3])})
+ >>> frames.extract(np.array([0, 1, 2, 3, 4, 5])).midpoints
+ {'x': array([1, 2, 3, 3, 2, 1])}
+ """
+ # Calculate the indices
+ # E.g for len = 4
+ # indices: 0123456789
+ # backwards: 0000111100
+ # snake_indices: 0123321001
+ # gap_indices: 0123032101
+ length = len(self)
+ backwards = (indices // length) % 2
+ snake_indices = np.where(backwards, (length - 1) - indices, indices) % length
+ cls: Type[Frames[Any]]
+ if not calculate_gap:
+ cls = Frames
+ gap = self.gap[np.where(backwards, length - indices, indices) % length]
+ else:
+ cls = type(self)
+ gap = None
+
+ # If lower or upper are different, apply to those
+ kwargs = {}
+ if self.midpoints is not self.lower:
+ # If going backwards select from the opposite bound
+ kwargs["lower"] = {
+ k: np.where(backwards, self.upper[k][snake_indices], v[snake_indices])
+ for k, v in self.lower.items()
+ }
+ if self.midpoints is not self.upper:
+ kwargs["upper"] = {
+ k: np.where(backwards, self.lower[k][snake_indices], v[snake_indices])
+ for k, v in self.upper.items()
+ }
+
+ # Apply to midpoints
+ return cls(
+ {k: v[snake_indices] for k, v in self.midpoints.items()}, gap=gap, **kwargs
+ )
+
+
+
+
+
+[docs]
+def gap_between_frames(frames1: Frames[Axis], frames2: Frames[Axis]) -> bool:
+ """Is there a gap between end of frames1 and start of frames2."""
+ return any(frames1.upper[a][-1] != frames2.lower[a][0] for a in frames1.axes())
+
+
+
+
+[docs]
+def squash_frames(stack: List[Frames[Axis]], check_path_changes=True) -> Frames[Axis]:
+ """Squash a stack of nested Frames into a single one.
+
+ Args:
+ stack: The Frames stack to squash, from slowest to fastest moving
+ check_path_changes: If True then check that nesting the output
+ Frames object within others will provide the same path
+ as nesting the input Frames stack within others
+
+ See Also:
+ `why-squash-can-change-path`
+
+ >>> fx = SnakedFrames({"x": np.array([1, 2])})
+ >>> fy = Frames({"y": np.array([3, 4])})
+ >>> squash_frames([fy, fx]).midpoints
+ {'y': array([3, 3, 4, 4]), 'x': array([1, 2, 2, 1])}
+ """
+ path = Path(stack)
+ # Consuming a Path through these Frames performs the squash
+ squashed = path.consume()
+ # Check that the squash is the same as the original
+ if stack and isinstance(stack[0], SnakedFrames):
+ squashed = SnakedFrames.from_frames(squashed)
+ # The top level is snaking, so this Frames object will run backwards
+ # This means any non-snaking axes will run backwards, which is
+ # surprising, so don't allow it
+ if check_path_changes:
+ non_snaking = [
+ k for d in stack for k in d.axes() if not isinstance(d, SnakedFrames)
+ ]
+ if non_snaking:
+ raise ValueError(
+ f"Cannot squash non-snaking Frames inside a SnakingFrames "
+ f"otherwise {non_snaking} would run backwards"
+ )
+ elif check_path_changes:
+ # The top level is not snaking, so make sure there is an even
+ # number of iterations of any snaking axis within it so it
+ # doesn't jump when this frames object is iterated a second time
+ for i, frames in enumerate(stack):
+ # A SnakedFrames within a non-snaking top level must repeat
+ # an even number of times
+ if isinstance(frames, SnakedFrames) and np.prod(path.lengths[:i]) % 2:
+ raise ValueError(
+ f"Cannot squash SnakingFrames inside a non-snaking Frames "
+ f"when they do not repeat an even number of times "
+ f"otherwise {frames.axes()} would jump in position"
+ )
+ return squashed
+
+
+
+
+[docs]
+class Path(Generic[Axis]):
+ """A consumable route through a stack of Frames, representing a scan path.
+
+ Args:
+ stack: The Frames stack describing the scan, from slowest to fastest
+ moving
+ start: The index of where in the Path to start
+ num: The number of scan frames to produce after start. None means up to
+ the end
+
+ See Also:
+ `iterate-a-spec`
+ """
+
+ def __init__(
+ self, stack: List[Frames[Axis]], start: int = 0, num: Optional[int] = None
+ ):
+ #: The Frames stack describing the scan, from slowest to fastest moving
+ self.stack = stack
+ #: Index that is next to be consumed
+ self.index = start
+ #: The lengths of all the stack
+ self.lengths = np.array([len(f) for f in stack])
+ #: Index of the end frame, one more than the last index that will be
+ #: produced
+ self.end_index = np.prod(self.lengths)
+ if num is not None and start + num < self.end_index:
+ self.end_index = start + num
+
+
+[docs]
+ def consume(self, num: Optional[int] = None) -> Frames[Axis]:
+ """Consume at most num frames from the Path and return as a Frames object.
+
+ >>> fx = SnakedFrames({"x": np.array([1, 2])})
+ >>> fy = Frames({"y": np.array([3, 4])})
+ >>> path = Path([fy, fx])
+ >>> path.consume(3).midpoints
+ {'y': array([3, 3, 4]), 'x': array([1, 2, 2])}
+ >>> path.consume(3).midpoints
+ {'y': array([4]), 'x': array([1])}
+ >>> path.consume(3).midpoints
+ {'y': array([], dtype=int64), 'x': array([], dtype=int64)}
+ """
+ if num is None:
+ end_index = self.end_index
+ else:
+ end_index = min(self.index + num, self.end_index)
+ indices = np.arange(self.index, end_index)
+ self.index = end_index
+ stack: Frames[Axis] = Frames(
+ {}, {}, {}, np.zeros(indices.shape, dtype=np.bool_)
+ )
+ # Example numbers below from a 2x3x4 ZxYxX scan
+ for i, frames in enumerate(self.stack):
+ # Number of times each frame will repeat: Z:12, Y:4, X:1
+ repeats = np.prod(self.lengths[i + 1 :])
+ # Scan indices mapped to indices within Frames object:
+ # Z:000000000000111111111111
+ # Y:000011112222000011112222
+ # X:012301230123012301230123
+ if repeats > 1:
+ dim_indices = indices // repeats
+ else:
+ dim_indices = indices
+ # Create the sliced frames
+ sliced = frames.extract(dim_indices, calculate_gap=False)
+ if repeats > 1:
+ # Whether this frames contributes to the gap bit
+ # Z:000000000000100000000000
+ # Y:000010001000100010001000
+ # X:111111111111111111111111
+ in_gap = (indices % repeats) == 0
+ # If in_gap, then keep the relevant gap bit
+ sliced.gap &= in_gap
+ # Zip it with the output Frames object
+ stack = stack.zip(sliced)
+ return stack
+
+
+ def __len__(self) -> int:
+ """Number of frames left in a scan, reduces when `consume` is called."""
+ return self.end_index - self.index
+
+
+
+
+[docs]
+class Midpoints(Generic[Axis]):
+ """Convenience iterable that produces the scan midpoints for each axis.
+
+ For better performance, consume from a `Path` instead.
+
+ Args:
+ stack: The stack of Frames describing the scan, from slowest to fastest
+ moving
+
+ See Also:
+ `iterate-a-spec`
+
+ >>> fx = SnakedFrames({"x": np.array([1, 2])})
+ >>> fy = Frames({"y": np.array([3, 4])})
+ >>> mp = Midpoints([fy, fx])
+ >>> for p in mp: print(p)
+ {'y': 3, 'x': 1}
+ {'y': 3, 'x': 2}
+ {'y': 4, 'x': 2}
+ {'y': 4, 'x': 1}
+ """
+
+ def __init__(self, stack: List[Frames[Axis]]):
+ #: The stack of Frames describing the scan, from slowest to fastest moving
+ self.stack = stack
+
+ @property
+ def axes(self) -> List[Axis]:
+ """The axes that will be present in each points dictionary."""
+ axes = []
+ for frames in self.stack:
+ axes += frames.axes()
+ return axes
+
+ def __len__(self) -> int:
+ """The number of dictionaries that will be produced if iterated over."""
+ return int(np.prod([len(frames) for frames in self.stack]))
+
+ def __iter__(self) -> Iterator[Dict[Axis, float]]:
+ """Yield {axis: midpoint} for each frame in the scan."""
+ path = Path(self.stack)
+ while len(path):
+ frames = path.consume(1)
+ yield {a: frames.midpoints[a][0] for a in frames.axes()}
+
+
+from itertools import cycle
+from typing import Any, Dict, Iterator, List
+
+import numpy as np
+from matplotlib import colors, patches
+from matplotlib import pyplot as plt
+from mpl_toolkits.mplot3d import Axes3D, proj3d
+from scipy import interpolate
+
+from .core import Path
+from .regions import Circle, Ellipse, Polygon, Rectangle, Region, find_regions
+from .specs import DURATION, Spec
+
+__all__ = ["plot_spec"]
+
+
+def _plot_arrays(axes, arrays: List[np.ndarray], **kwargs):
+ if len(arrays) > 2:
+ axes.plot3D(arrays[2], arrays[1], arrays[0], **kwargs)
+ elif len(arrays) == 2:
+ axes.plot(arrays[1], arrays[0], **kwargs)
+ else:
+ axes.plot(arrays[0], np.zeros(len(arrays[0])), **kwargs)
+
+
+# https://stackoverflow.com/a/11156353
+class _Arrow3D(patches.FancyArrowPatch):
+ def __init__(self, xs, ys, zs, *args, **kwargs):
+ super().__init__((0, 0), (0, 0), *args, **kwargs)
+ self._verts3d = xs, ys, zs
+
+ # Added here because of https://github.com/matplotlib/matplotlib/issues/21688
+ def do_3d_projection(self, renderer=None):
+ xs3d, ys3d, zs3d = self._verts3d
+ xs, ys, zs = proj3d.proj_transform(xs3d, ys3d, zs3d, self.axes.M)
+ self.set_positions((xs[0], ys[0]), (xs[1], ys[1]))
+
+ return np.min(zs)
+
+
+def _plot_arrow(axes, arrays: List[np.ndarray]):
+ if len(arrays) == 1:
+ arrays = [np.array([0, 0])] + arrays
+ if len(arrays) == 2:
+ head = [a[-1] for a in reversed(arrays)]
+ tail = [a[-1] - (a[-1] - a[-2]) * 0.1 for a in reversed(arrays)]
+ axes.annotate(
+ "", head[:2], tail[:2], arrowprops=dict(color="lightgrey", arrowstyle="-|>")
+ )
+ elif len(arrays) == 3:
+ arrows = [a[-2:] for a in reversed(arrays)]
+ a = _Arrow3D(
+ *arrows[:3], mutation_scale=10, arrowstyle="-|>", color="lightgrey"
+ )
+ axes.add_artist(a)
+
+
+def _plot_spline(axes, ranges, arrays: List[np.ndarray], index_colours: Dict[int, str]):
+ scaled_arrays = [a / r for a, r in zip(arrays, ranges)]
+ # Define curves parametrically
+ t = np.zeros(len(arrays[0]))
+ t[1:] = np.sqrt(sum((arr[1:] - arr[:-1]) ** 2 for arr in scaled_arrays))
+ t = np.cumsum(t)
+ if t[-1] > 0:
+ # Can't make a spline that starts and ends in the same place, so add a small
+ # delta
+ for s, r in zip(scaled_arrays, ranges):
+ if s[0] == s[-1]:
+ s += np.linspace(0, r * 1e-7, len(s))
+ # There are no duplicated points, plot a spline
+ t /= t[-1]
+ # Scale the arrays so splines don't favour larger scaled axes
+ tck, _ = interpolate.splprep(scaled_arrays, k=2, s=0)
+ starts = sorted(list(index_colours))
+ stops = starts[1:] + [len(arrays[0]) - 1]
+ for start, stop in zip(starts, stops):
+ tnew = np.linspace(t[start], t[stop], num=1001)
+ spline = interpolate.splev(tnew, tck)
+ # Scale the splines back to the original scaling
+ unscaled_splines = [a * r for a, r in zip(spline, ranges)]
+ _plot_arrays(axes, unscaled_splines, color=index_colours[start])
+ yield unscaled_splines
+
+
+
+[docs]
+def plot_spec(spec: Spec[Any]):
+ """Plot a spec, drawing the path taken through the scan.
+
+ Uses a different colour for each frame, grey for the turnarounds, and
+ marks the midpoints with a filled circle if there are less than 200 of
+ them. If the scan is 2D then 2D regions are shown in black.
+
+ .. example_spec::
+
+ from scanspec.specs import Line
+ from scanspec.regions import Circle
+
+ cube = Line("z", 1, 3, 3) * Line("y", 1, 3, 10) * ~Line("x", 0, 2, 10)
+ spec = cube & Circle("x", "y", 1, 2, 0.9)
+ """
+ dims = spec.calculate()
+ dim = Path(dims).consume()
+ axes = [a for a in spec.axes() if a is not DURATION]
+ ndims = len(axes)
+
+ # Setup axes
+ if ndims > 2:
+ plt.figure(figsize=(6, 6))
+ plt_axes: Axes3D = plt.axes(projection="3d")
+ plt_axes.grid(False)
+ plt_axes.set_zlabel(axes[-3])
+ plt_axes.set_ylabel(axes[-2])
+ plt_axes.view_init(elev=15)
+ elif ndims == 2:
+ plt.figure(figsize=(6, 6))
+ plt_axes = plt.axes()
+ plt_axes.set_ylabel(axes[-2])
+ else:
+ plt.figure(figsize=(6, 2))
+ plt_axes = plt.axes()
+ plt_axes.yaxis.set_visible(False)
+ plt_axes.set_xlabel(axes[-1])
+
+ # Title with dimension sizes
+ plt.title(", ".join(f"Dim[{' '.join(d.axes())} len={len(d)}]" for d in dims))
+
+ # Plot any Regions
+ if ndims <= 2:
+ regions: Iterator[Region[Any]] = find_regions(spec)
+ for region in regions:
+ if isinstance(region, Rectangle):
+ xy = (region.x_min, region.y_min)
+ width = region.x_max - region.x_min
+ height = region.y_max - region.y_min
+ plt_axes.add_patch(
+ patches.Rectangle(xy, width, height, angle=region.angle, fill=False)
+ )
+ elif isinstance(region, Circle):
+ xy = (region.x_middle, region.y_middle)
+ plt_axes.add_patch(patches.Circle(xy, region.radius, fill=False))
+ elif isinstance(region, Ellipse):
+ xy = (region.x_middle, region.y_middle)
+ width = region.x_radius * 2
+ height = region.y_radius * 2
+ angle = region.angle
+ plt_axes.add_patch(
+ patches.Ellipse(xy, width, height, angle=angle, fill=False)
+ )
+ elif isinstance(region, Polygon):
+ # *xy_verts* is a numpy array with shape Nx2.
+ xy_verts = np.column_stack((region.x_verts, region.y_verts))
+ plt_axes.add_patch(patches.Polygon(xy_verts, fill=False))
+
+ # Plot the splines
+ tail: Any = {a: None for a in axes}
+ ranges = [max(np.max(v) - np.min(v), 0.0001) for k, v in dim.midpoints.items()]
+ seg_col = cycle(colors.TABLEAU_COLORS)
+ last_index = 0
+ splines = None
+ # The first element of gap is undefined (as there is no previous frame)
+ # so discard it
+ gap_indices = list(np.nonzero(dim.gap[1:])[0] + 1)
+ for index in gap_indices + [len(dim)]:
+ num_points = index - last_index
+ arrays = []
+ turnaround = []
+ for a in axes:
+ # Add the midpoints and the lower and upper bounds
+ arr = np.empty(num_points * 2 + 1)
+ arr[:-1:2] = dim.lower[a][last_index:index]
+ arr[1::2] = dim.midpoints[a][last_index:index]
+ arr[-1] = dim.upper[a][index - 1]
+ arrays.append(arr)
+ # Add the turnaround
+ if tail[a] is not None:
+ # Already had a tail, add lead in points
+ tail[a][2:] = np.linspace(-0.01, 0, 2) * (arr[1] - arr[0]) + arr[0]
+ turnaround.append(tail[a])
+ # Add tail off points
+ tail[a] = np.empty(4)
+ tail[a][:2] = np.linspace(0, 0.01, 2) * (arr[-1] - arr[-2]) + arr[-1]
+ last_index = index
+
+ arrow_arr = None
+ if turnaround:
+ # If we didn't move then plot a straight line from start to stop
+ if all(t[1] - t[0] == 0 for t in turnaround):
+ for t in turnaround:
+ t[1] += (t[2] - t[1]) / 4
+ if all(t[3] - t[2] == 0 for t in turnaround):
+ for t in turnaround:
+ t[2] -= (t[2] - t[1]) / 4
+ # Plot the turnaround
+ arrow_arr = list(
+ _plot_spline(plt_axes, ranges, turnaround, {0: "lightgrey"})
+ )[0]
+
+ # Plot the points
+ index_colours = {2 * i: next(seg_col) for i in range(num_points)}
+ splines = list(_plot_spline(plt_axes, ranges, arrays, index_colours))
+
+ if arrow_arr:
+ # Plot the arrow on the turnaround
+ _plot_arrow(plt_axes, arrow_arr)
+ elif splines:
+ # Plot the starting arrow in the direction of the first point
+ arrow_arr = [(2 * a[0] - a[1], a[0]) for a in splines[0]]
+ _plot_arrow(plt_axes, arrow_arr)
+ else:
+ # First point isn't moving, put a right caret marker
+ _plot_arrays(
+ plt_axes,
+ [np.array([dim.lower[a][0]]) for a in axes],
+ marker=5,
+ color="lightgrey",
+ )
+
+ # Plot the capture points
+ if len(dim) < 200:
+ arrays = [dim.midpoints[a] for a in axes]
+ _plot_arrays(plt_axes, arrays, linestyle="", marker=".", color="k")
+
+ # Plot the end
+ _plot_arrays(
+ plt_axes,
+ [np.array([dim.upper[a][-1]]) for a in axes],
+ marker="x",
+ color="lightgrey",
+ )
+
+ plt.show()
+
+
+from __future__ import annotations
+
+from typing import Generic, Iterator, List, Set
+
+import numpy as np
+from pydantic import BaseModel, Field
+from pydantic.dataclasses import dataclass
+
+from .core import (
+ AxesPoints,
+ Axis,
+ StrictConfig,
+ discriminated_union_of_subclasses,
+ if_instance_do,
+)
+
+__all__ = [
+ "Region",
+ "get_mask",
+ "CombinationOf",
+ "UnionOf",
+ "IntersectionOf",
+ "DifferenceOf",
+ "SymmetricDifferenceOf",
+ "Range",
+ "Rectangle",
+ "Polygon",
+ "Circle",
+ "Ellipse",
+ "find_regions",
+]
+
+
+
+[docs]
+@discriminated_union_of_subclasses
+class Region(Generic[Axis]):
+ """Abstract baseclass for a Region that can `Mask` a `Spec`.
+
+ Supports operators:
+
+ - ``|``: `UnionOf` two Regions, midpoints present in either
+ - ``&``: `IntersectionOf` two Regions, midpoints present in both
+ - ``-``: `DifferenceOf` two Regions, midpoints present in first not second
+ - ``^``: `SymmetricDifferenceOf` two Regions, midpoints present in one not both
+ """
+
+
+[docs]
+ def axis_sets(self) -> List[Set[Axis]]:
+ """Produce the non-overlapping sets of axes this region spans."""
+ raise NotImplementedError(self)
+
+
+
+[docs]
+ def mask(self, points: AxesPoints[Axis]) -> np.ndarray:
+ """Produce a mask of which points are in the region."""
+ raise NotImplementedError(self)
+
+
+ def __or__(self, other) -> UnionOf[Axis]:
+ return if_instance_do(other, Region, lambda o: UnionOf(self, o))
+
+ def __and__(self, other) -> IntersectionOf[Axis]:
+ return if_instance_do(other, Region, lambda o: IntersectionOf(self, o))
+
+ def __sub__(self, other) -> DifferenceOf[Axis]:
+ return if_instance_do(other, Region, lambda o: DifferenceOf(self, o))
+
+ def __xor__(self, other) -> SymmetricDifferenceOf[Axis]:
+ return if_instance_do(other, Region, lambda o: SymmetricDifferenceOf(self, o))
+
+
+
+
+[docs]
+def get_mask(region: Region[Axis], points: AxesPoints[Axis]) -> np.ndarray:
+ """Return a mask of the points inside the region.
+
+ If there is an overlap of axes of region and points return a
+ mask of the points in the region, otherwise return all ones
+ """
+ axes = set(points)
+ needs_mask = any(ks & axes for ks in region.axis_sets())
+ if needs_mask:
+ return region.mask(points)
+ else:
+ return np.ones(len(list(points.values())[0]))
+
+
+
+def _merge_axis_sets(axis_sets: List[Set[Axis]]) -> Iterator[Set[Axis]]:
+ # Take overlapping axis sets and merge any that overlap into each
+ # other
+ for ks in axis_sets: # ks = key_sets - left over from a previous naming standard
+ axis_set = ks.copy()
+ # Empty matching sets into this axis_set
+ for ks in axis_sets:
+ if ks & axis_set:
+ while ks:
+ axis_set.add(ks.pop())
+ # It might be emptied already, only yield if it isn't
+ if axis_set:
+ yield axis_set
+
+
+
+[docs]
+@dataclass(config=StrictConfig)
+class CombinationOf(Region[Axis]):
+ """Abstract baseclass for a combination of two regions, left and right."""
+
+ left: Region[Axis] = Field(description="The left-hand Region to combine")
+ right: Region[Axis] = Field(description="The right-hand Region to combine")
+
+ 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
+
+[docs]
+@dataclass(config=StrictConfig)
+class UnionOf(CombinationOf[Axis]):
+ """A point is in UnionOf(a, b) if in either a or b.
+
+ Typically created with the ``|`` operator
+
+ >>> r = Range("x", 0.5, 2.5) | Range("x", 1.5, 3.5)
+ >>> r.mask({"x": np.array([0, 1, 2, 3, 4])})
+ array([False, True, True, True, False])
+ """
+
+ def mask(self, points: AxesPoints[Axis]) -> np.ndarray:
+ mask = get_mask(self.left, points) | get_mask(self.right, points)
+ return mask
+
+
+
+
+[docs]
+@dataclass(config=StrictConfig)
+class IntersectionOf(CombinationOf[Axis]):
+ """A point is in IntersectionOf(a, b) if in both a and b.
+
+ Typically created with the ``&`` operator.
+
+ >>> r = Range("x", 0.5, 2.5) & Range("x", 1.5, 3.5)
+ >>> r.mask({"x": np.array([0, 1, 2, 3, 4])})
+ array([False, False, True, False, False])
+ """
+
+ def mask(self, points: AxesPoints[Axis]) -> np.ndarray:
+ mask = get_mask(self.left, points) & get_mask(self.right, points)
+ return mask
+
+
+
+
+[docs]
+@dataclass(config=StrictConfig)
+class DifferenceOf(CombinationOf[Axis]):
+ """A point is in DifferenceOf(a, b) if in a and not in b.
+
+ Typically created with the ``-`` operator.
+
+ >>> r = Range("x", 0.5, 2.5) - Range("x", 1.5, 3.5)
+ >>> r.mask({"x": np.array([0, 1, 2, 3, 4])})
+ array([False, True, False, False, False])
+ """
+
+ 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
+
+
+
+
+[docs]
+@dataclass(config=StrictConfig)
+class SymmetricDifferenceOf(CombinationOf[Axis]):
+ """A point is in SymmetricDifferenceOf(a, b) if in either a or b, but not both.
+
+ Typically created with the ``^`` operator.
+
+ >>> r = Range("x", 0.5, 2.5) ^ Range("x", 1.5, 3.5)
+ >>> r.mask({"x": np.array([0, 1, 2, 3, 4])})
+ array([False, True, False, True, False])
+ """
+
+ def mask(self, points: AxesPoints[Axis]) -> np.ndarray:
+ mask = get_mask(self.left, points) ^ get_mask(self.right, points)
+ return mask
+
+
+
+
+[docs]
+@dataclass(config=StrictConfig)
+class Range(Region[Axis]):
+ """Mask contains points of axis >= min and <= max.
+
+ >>> r = Range("x", 1, 2)
+ >>> r.mask({"x": np.array([0, 1, 2, 3, 4])})
+ array([False, True, True, False, False])
+ """
+
+ axis: Axis = Field(description="The name matching the axis to mask in spec")
+ min: float = Field(description="The minimum inclusive value in the region")
+ max: float = Field(description="The minimum inclusive value in the region")
+
+ def axis_sets(self) -> List[Set[Axis]]:
+ return [{self.axis}]
+
+ def mask(self, points: AxesPoints[Axis]) -> np.ndarray:
+ v = points[self.axis]
+ mask = np.bitwise_and(v >= self.min, v <= self.max)
+ return mask
+
+
+
+
+[docs]
+@dataclass(config=StrictConfig)
+class Rectangle(Region[Axis]):
+ """Mask contains points of axis within a rotated xy rectangle.
+
+ .. example_spec::
+
+ from scanspec.regions import Rectangle
+ from scanspec.specs import Line
+
+ grid = Line("y", 1, 3, 10) * ~Line("x", 0, 2, 10)
+ spec = grid & Rectangle("x", "y", 0, 1.1, 1.5, 2.1, 30)
+ """
+
+ x_axis: Axis = Field(description="The name matching the x axis of the spec")
+ y_axis: Axis = Field(description="The name matching the y axis of the spec")
+ x_min: float = Field(description="Minimum inclusive x value in the region")
+ y_min: float = Field(description="Minimum inclusive y value in the region")
+ x_max: float = Field(description="Maximum inclusive x value in the region")
+ y_max: float = Field(description="Maximum inclusive y value in the region")
+ angle: float = Field(
+ description="Clockwise rotation angle of the rectangle", default=0.0
+ )
+
+ def axis_sets(self) -> List[Set[Axis]]:
+ return [{self.x_axis, self.y_axis}]
+
+ def mask(self, points: AxesPoints[Axis]) -> np.ndarray:
+ x = points[self.x_axis] - self.x_min
+ y = points[self.y_axis] - self.y_min
+ if self.angle != 0:
+ # Rotate src points by -angle
+ phi = np.radians(-self.angle)
+ rx = x * np.cos(phi) - y * np.sin(phi)
+ ry = x * np.sin(phi) + y * np.cos(phi)
+ x = rx
+ y = ry
+ mask_x = np.bitwise_and(x >= 0, x <= (self.x_max - self.x_min))
+ mask_y = np.bitwise_and(y >= 0, y <= (self.y_max - self.y_min))
+ return mask_x & mask_y
+
+
+
+
+[docs]
+@dataclass(config=StrictConfig)
+class Polygon(Region[Axis]):
+ """Mask contains points of axis within a rotated xy polygon.
+
+ .. example_spec::
+
+ from scanspec.regions import Polygon
+ from scanspec.specs import Line
+
+ grid = Line("y", 3, 8, 10) * ~Line("x", 1 ,8, 10)
+ spec = grid & Polygon("x", "y", [1.0, 6.0, 8.0, 2.0], [4.0, 10.0, 6.0, 1.0])
+ """
+
+ x_axis: Axis = Field(description="The name matching the x axis of the spec")
+ y_axis: Axis = Field(description="The name matching the y axis of the spec")
+ x_verts: List[float] = Field(
+ description="The Nx1 x coordinates of the polygons vertices", min_len=3
+ )
+ y_verts: List[float] = Field(
+ description="The Nx1 y coordinates of the polygons vertices", min_len=3
+ )
+
+ def axis_sets(self) -> List[Set[Axis]]:
+ return [{self.x_axis, self.y_axis}]
+
+ def mask(self, points: AxesPoints[Axis]) -> np.ndarray:
+ x = points[self.x_axis]
+ y = points[self.y_axis]
+ v1x, v1y = self.x_verts[-1], self.y_verts[-1]
+ mask = np.full(len(x), False, dtype=np.int8)
+ for v2x, v2y in zip(self.x_verts, self.y_verts):
+ # skip horizontal edges
+ if v2y != v1y:
+ vmask = np.full(len(x), False, dtype=np.int8)
+ vmask |= (y < v2y) & (y >= v1y)
+ vmask |= (y < v1y) & (y >= v2y)
+ t = (y - v1y) / (v2y - v1y)
+ vmask &= x < v1x + t * (v2x - v1x)
+ mask ^= vmask
+ v1x, v1y = v2x, v2y
+ return mask
+
+
+
+
+[docs]
+@dataclass(config=StrictConfig)
+class Circle(Region[Axis]):
+ """Mask contains points of axis within an xy circle of given radius.
+
+ .. example_spec::
+
+ from scanspec.regions import Circle
+ from scanspec.specs import Line
+
+ grid = Line("y", 1, 3, 10) * ~Line("x", 0, 2, 10)
+ spec = grid & Circle("x", "y", 1, 2, 0.9)
+ """
+
+ x_axis: Axis = Field(description="The name matching the x axis of the spec")
+ y_axis: Axis = Field(description="The name matching the y axis of the spec")
+ x_middle: float = Field(description="The central x point of the circle")
+ y_middle: float = Field(description="The central y point of the circle")
+ radius: float = Field(description="Radius of the circle", exc_min=0)
+
+ def axis_sets(self) -> List[Set[Axis]]:
+ return [{self.x_axis, self.y_axis}]
+
+ def mask(self, points: AxesPoints[Axis]) -> np.ndarray:
+ x = points[self.x_axis] - self.x_middle
+ y = points[self.y_axis] - self.y_middle
+ mask = x * x + y * y <= (self.radius * self.radius)
+ return mask
+
+
+
+
+[docs]
+@dataclass(config=StrictConfig)
+class Ellipse(Region[Axis]):
+ """Mask contains points of axis within an xy ellipse of given radius.
+
+ .. example_spec::
+
+ from scanspec.regions import Ellipse
+ from scanspec.specs import Line
+
+ grid = Line("y", 3, 8, 10) * ~Line("x", 1 ,8, 10)
+ spec = grid & Ellipse("x", "y", 5, 5, 2, 3, 75)
+ """
+
+ x_axis: Axis = Field(description="The name matching the x axis of the spec")
+ y_axis: Axis = Field(description="The name matching the y axis of the spec")
+ x_middle: float = Field(description="The central x point of the ellipse")
+ y_middle: float = Field(description="The central y point of the ellipse")
+ x_radius: float = Field(
+ description="The radius along the x axis of the ellipse", exc_min=0
+ )
+ y_radius: float = Field(
+ description="The radius along the y axis of the ellipse", exc_min=0
+ )
+ angle: float = Field(description="The angle of the ellipse (degrees)", default=0.0)
+
+ def axis_sets(self) -> List[Set[Axis]]:
+ return [{self.x_axis, self.y_axis}]
+
+ def mask(self, points: AxesPoints[Axis]) -> np.ndarray:
+ x = points[self.x_axis] - self.x_middle
+ y = points[self.y_axis] - self.y_middle
+ if self.angle != 0:
+ # Rotate src points by -angle
+ phi = np.radians(-self.angle)
+ tx = x * np.cos(phi) - y * np.sin(phi)
+ ty = x * np.sin(phi) + y * np.cos(phi)
+ x = tx
+ y = ty
+ mask = (x / self.x_radius) ** 2 + (y / self.y_radius) ** 2 <= 1
+ return mask
+
+
+
+
+[docs]
+def find_regions(obj) -> Iterator[Region[Axis]]:
+ """Recursively yield Regions from obj and its children."""
+ if hasattr(obj, "__pydantic_model__") and issubclass(
+ obj.__pydantic_model__, BaseModel
+ ):
+ if isinstance(obj, Region):
+ yield obj
+ for name in obj.__dict__.keys():
+ regions: Iterator[Region[Axis]] = find_regions(getattr(obj, name))
+ yield from regions
+
+
+import base64
+import json
+from enum import Enum
+from typing import List, Mapping, Optional, Tuple, Union
+
+import numpy as np
+from fastapi import Body, FastAPI
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.openapi.utils import get_openapi
+from fastapi.responses import JSONResponse
+from pydantic import Field
+from pydantic.dataclasses import dataclass
+
+from scanspec.core import AxesPoints, Frames, Path
+
+from .specs import Line, Spec
+
+app = FastAPI()
+
+#
+# Data Model
+#
+
+
+#: A set of points, that can be returned in various formats
+Points = Union[str, List[float]]
+
+
+
+[docs]
+@dataclass
+class ValidResponse:
+ """Response model for spec validation."""
+
+ input_spec: Spec = Field(description="The input scanspec")
+ valid_spec: Spec = Field(description="The validated version of the spec")
+
+
+
+
+[docs]
+class PointsFormat(str, Enum):
+ """Formats in which we can return points."""
+
+ STRING = "STRING"
+ FLOAT_LIST = "FLOAT_LIST"
+ BASE64_ENCODED = "BASE64_ENCODED"
+
+
+
+
+[docs]
+@dataclass
+class PointsRequest:
+ """A request for generated scan points."""
+
+ spec: Spec = Field(description="The spec from which to generate points")
+ max_frames: Optional[int] = Field(
+ description="The maximum number of points to return, if None will return "
+ "as many as calculated",
+ default=100000,
+ )
+ format: PointsFormat = Field(
+ description="The format in which to output the points data",
+ default=PointsFormat.FLOAT_LIST,
+ )
+
+
+
+
+[docs]
+@dataclass
+class GeneratedPointsResponse:
+ """Base class for responses that include generated point data."""
+
+ total_frames: int = Field(description="Total number of frames in spec")
+ returned_frames: int = Field(
+ description="Total of number of frames in this response, may be "
+ "less than total_frames due to downsampling etc."
+ )
+ format: PointsFormat = Field(description="Format of returned point data")
+
+
+
+
+[docs]
+@dataclass
+class MidpointsResponse(GeneratedPointsResponse):
+ """Midpoints of a generated scan."""
+
+ midpoints: Mapping[str, Points] = Field(
+ description="The midpoints of scan frames for each axis"
+ )
+
+
+
+
+[docs]
+@dataclass
+class BoundsResponse(GeneratedPointsResponse):
+ """Bounds of a generated scan."""
+
+ lower: Mapping[str, Points] = Field(
+ description="Lower bounds of scan frames if different from midpoints"
+ )
+ upper: Mapping[str, Points] = Field(
+ description="Upper bounds of scan frames if different from midpoints"
+ )
+
+
+
+
+[docs]
+@dataclass
+class GapResponse:
+ """Presence of gaps in a generated scan."""
+
+ gap: List[bool] = Field(
+ description="Boolean array indicating if there is a gap between each frame"
+ )
+
+
+
+
+[docs]
+@dataclass
+class SmallestStepResponse:
+ """Information about the smallest steps between points in a spec."""
+
+ absolute: float = Field(
+ description="Absolute smallest distance between two points on a single axis"
+ )
+ per_axis: Mapping[str, float] = Field(
+ description="Smallest distance between two points on each axis"
+ )
+
+
+
+#
+# API Routes
+#
+
+_EXAMPLE_SPEC = Line("y", 0.0, 10.0, 3) * Line("x", 0.0, 10.0, 4)
+_EXAMPLE_POINTS_REQUEST = PointsRequest(
+ _EXAMPLE_SPEC, max_frames=1024, format=PointsFormat.FLOAT_LIST
+)
+
+
+
+[docs]
+@app.post("/valid", response_model=ValidResponse)
+def valid(
+ spec: Spec = Body(..., examples=[_EXAMPLE_SPEC])
+) -> Union[ValidResponse, JSONResponse]:
+ """Validate wether a ScanSpec can produce a viable scan.
+
+ Args:
+ spec: The scanspec to validate
+
+ Returns:
+ ValidResponse: A canonical version of the spec if it is valid.
+ An error otherwise.
+ """
+ valid_spec = Spec.deserialize(spec.serialize())
+ return ValidResponse(spec, valid_spec)
+
+
+
+
+[docs]
+@app.post("/midpoints", response_model=MidpointsResponse)
+def midpoints(
+ request: PointsRequest = Body(
+ ...,
+ examples=[_EXAMPLE_POINTS_REQUEST],
+ )
+) -> MidpointsResponse:
+ """Generate midpoints from a scanspec.
+
+ A scanspec can produce bounded points (i.e. a point is valid if an
+ axis is between a minimum and and a maximum, see /bounds). The midpoints
+ are the middle of each set of bounds.
+
+ Args:
+ request: Scanspec and formatting info.
+
+ Returns:
+ MidpointsResponse: Midpoints of the scan
+ """
+ chunk, total_frames = _to_chunk(request)
+ return MidpointsResponse(
+ total_frames,
+ request.max_frames or total_frames,
+ request.format,
+ _format_axes_points(chunk.midpoints, request.format),
+ )
+
+
+
+
+[docs]
+@app.post("/bounds", response_model=BoundsResponse)
+def bounds(
+ request: PointsRequest = Body(
+ ...,
+ examples=[_EXAMPLE_POINTS_REQUEST],
+ )
+) -> BoundsResponse:
+ """Generate bounds from a scanspec.
+
+ A scanspec can produce points with lower and upper bounds.
+
+ Args:
+ request: Scanspec and formatting info.
+
+ Returns:
+ BoundsResponse: Bounds of the scan
+ """
+ chunk, total_frames = _to_chunk(request)
+ return BoundsResponse(
+ total_frames,
+ request.max_frames or total_frames,
+ request.format,
+ _format_axes_points(chunk.lower, request.format),
+ _format_axes_points(chunk.upper, request.format),
+ )
+
+
+
+
+[docs]
+@app.post("/gap", response_model=GapResponse)
+def gap(
+ spec: Spec = Body(
+ ...,
+ examples=[_EXAMPLE_SPEC],
+ )
+) -> GapResponse:
+ """Generate gaps from a scanspec.
+
+ A scanspec may indicate if there is a gap between two frames.
+ The array returned corresponds to whether or not there is a gap
+ after each frame.
+
+ Args:
+ request: Scanspec and formatting info.
+
+ Returns:
+ GapResponse: Bounds of the scan
+ """
+ dims = spec.calculate() # Grab dimensions from spec
+ path = Path(dims) # Convert to a path
+ gap = list(path.consume().gap)
+ return GapResponse(gap)
+
+
+
+
+[docs]
+@app.post("/smalleststep", response_model=SmallestStepResponse)
+def smallest_step(
+ spec: Spec = Body(..., examples=[_EXAMPLE_SPEC])
+) -> SmallestStepResponse:
+ """Calculate the smallest step in a scan, both absolutely and per-axis.
+
+ Ignore any steps of size 0.
+
+ Args:
+ spec: The spec of the scan
+
+ Returns:
+ SmallestStepResponse: A description of the smallest steps in the spec
+ """
+ dims = spec.calculate() # Grab dimensions from spec
+ path = Path(dims) # Convert to a path
+ chunk = path.consume()
+
+ absolute = _calc_smallest_step(list(chunk.midpoints.values()))
+ per_axis = {
+ axis: _calc_smallest_step([chunk.midpoints[axis]]) for axis in chunk.axes()
+ }
+
+ return SmallestStepResponse(absolute, per_axis)
+
+
+
+#
+# Utility Functions
+#
+
+
+def _to_chunk(request: PointsRequest) -> Tuple[Frames, int]:
+ spec = Spec.deserialize(request.spec)
+ dims = spec.calculate() # Grab dimensions from spec
+ path = Path(dims) # Convert to a path
+
+ # TOTAL FRAMES
+ total_frames = len(path) # Capture the total length of the path
+
+ # MAX FRAMES
+ # Limit the consumed data by the max_frames argument
+ max_frames = request.max_frames
+ if max_frames and (max_frames < len(path)):
+ # Cap the frames by the max limit
+ path = _reduce_frames(dims, max_frames)
+ # WARNING: path object is consumed after this statement
+ return path.consume(max_frames), total_frames
+
+
+def _format_axes_points(
+ axes_points: AxesPoints[str], format: PointsFormat
+) -> Mapping[str, Points]:
+ """Convert points to a requested format.
+
+ Args:
+ axes_points: The points to convert
+ format: The target format
+
+ Raises:
+ KeyError: If the function does not support the given format
+
+ Returns:
+ Mapping[str, Points]: A mapping of axis to formatted points.
+ """
+ if format is PointsFormat.FLOAT_LIST:
+ return {axis: list(points) for axis, points in axes_points.items()}
+ elif format is PointsFormat.STRING:
+ return {axis: str(points) for axis, points in axes_points.items()}
+ elif format is PointsFormat.BASE64_ENCODED:
+ return {
+ axis: base64.b64encode(points.tobytes()).decode()
+ for axis, points in axes_points.items()
+ }
+ else:
+ raise KeyError(f"Unknown format: {format}")
+
+
+def _reduce_frames(stack: List[Frames[str]], max_frames: int) -> Path:
+ """Removes frames from a spec so len(path) < max_frames.
+
+ Args:
+ stack: A stack of Frames created by a spec
+ max_frames: The maximum number of frames the user wishes to be returned
+ """
+ # Calculate the total number of frames
+ num_frames = 1
+ for frames in stack:
+ num_frames *= len(frames)
+
+ # Need each dim to be this much smaller
+ ratio = 1 / np.power(max_frames / num_frames, 1 / len(stack))
+
+ sub_frames = [_sub_sample(f, ratio) for f in stack]
+ return Path(sub_frames)
+
+
+def _sub_sample(frames: Frames[str], ratio: float) -> Frames:
+ """Provides a sub-sample Frames object whilst preserving its core structure.
+
+ Args:
+ frames: the Frames object to be reduced
+ ratio: the reduction ratio of the dimension
+ """
+ num_indexes = int(len(frames) / ratio)
+ indexes = np.linspace(0, len(frames) - 1, num_indexes, dtype=np.int32)
+ return frames.extract(indexes, calculate_gap=False)
+
+
+def _calc_smallest_step(points: List[np.ndarray]) -> float:
+ # Calc abs diffs of all axes, ignoring any zero values
+ absolute_diffs = [_abs_diffs(axis_midpoints) for axis_midpoints in points]
+ # Normalize and remove zeros
+ norm_diffs = np.linalg.norm(absolute_diffs, axis=0)
+ norm_diffs = norm_diffs[norm_diffs > 0.0]
+ # Return the smallest value (Aka. smallest step)
+ return np.amin(norm_diffs)
+
+
+def _abs_diffs(array: np.ndarray) -> np.ndarray:
+ """Calculates the absolute differences between adjacent elements in the array.
+
+ Args:
+ array: A 1xN array of numerical values
+
+ Returns:
+ A newly constucted array of absolute differences
+ """
+ # [array[1] - array[0], array[2] - array[1], ...]
+ adjacent_diffs = array[1:] - array[:-1]
+ return np.absolute(adjacent_diffs)
+
+
+
+[docs]
+def run_app(cors: bool = False, port: int = 8080) -> None:
+ """Run an application providing the scanspec service."""
+ if cors:
+ app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"],
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+ )
+
+ import uvicorn
+
+ uvicorn.run(app, port=port)
+
+
+
+
+[docs]
+def scanspec_schema_text() -> str:
+ """Generate the OpenAPI schema for the service as a string.
+
+ Returns:
+ str: The OpenAPI schema
+ """
+ return json.dumps(
+ get_openapi(
+ title=app.title,
+ version=app.version,
+ openapi_version=app.openapi_version,
+ description=app.description,
+ routes=app.routes,
+ )
+ )
+
+
+from __future__ import annotations
+
+from dataclasses import asdict
+from typing import Any, Callable, Dict, Generic, List, Mapping, Optional, Tuple, Type
+
+import numpy as np
+from pydantic import Field, parse_obj_as
+from pydantic.dataclasses import dataclass
+
+from .core import (
+ Axis,
+ Frames,
+ Midpoints,
+ Path,
+ SnakedFrames,
+ StrictConfig,
+ discriminated_union_of_subclasses,
+ gap_between_frames,
+ if_instance_do,
+ squash_frames,
+)
+from .regions import Region, get_mask
+
+__all__ = [
+ "DURATION",
+ "Spec",
+ "Product",
+ "Repeat",
+ "Zip",
+ "Mask",
+ "Snake",
+ "Concat",
+ "Squash",
+ "Line",
+ "Static",
+ "Spiral",
+ "fly",
+ "step",
+]
+
+
+#: Can be used as a special key to indicate how long each point should be
+DURATION = "DURATION"
+
+
+
+[docs]
+@discriminated_union_of_subclasses(config=StrictConfig)
+class Spec(Generic[Axis]):
+ """A serializable representation of the type and parameters of a scan.
+
+ Abstract baseclass for the specification of a scan. Supports operators:
+
+ - ``*``: Outer `Product` of two Specs, nesting the second within the first.
+ If the first operand is an integer, wrap it in a `Repeat`
+ - ``&``: `Mask` the Spec with a `Region`, excluding midpoints outside of it
+ - ``~``: `Snake` the Spec, reversing every other iteration of it
+ """
+
+
+[docs]
+ def axes(self) -> List[Axis]:
+ """Return the list of axes that are present in the scan.
+
+ Ordered from slowest moving to fastest moving.
+ """
+ raise NotImplementedError(self)
+
+
+
+[docs]
+ def calculate(self, bounds=True, nested=False) -> List[Frames[Axis]]:
+ """Produce a stack of nested `Frames` that form the scan.
+
+ Ordered from slowest moving to fastest moving.
+ """
+ raise NotImplementedError(self)
+
+
+
+[docs]
+ def frames(self) -> Frames[Axis]:
+ """Expand all the scan `Frames` and return them."""
+ return Path(self.calculate()).consume()
+
+
+
+[docs]
+ def midpoints(self) -> Midpoints[Axis]:
+ """Return `Midpoints` that can be iterated point by point."""
+ return Midpoints(self.calculate(bounds=False))
+
+
+
+[docs]
+ def shape(self) -> Tuple[int, ...]:
+ """Return the final, simplified shape of the scan."""
+ return tuple(len(dim) for dim in self.calculate())
+
+
+ def __rmul__(self, other) -> Product[Axis]:
+ return if_instance_do(other, int, lambda o: Product(Repeat(o), self))
+
+ def __mul__(self, other) -> Product[Axis]:
+ return if_instance_do(other, Spec, lambda o: Product(self, o))
+
+ def __and__(self, other) -> Mask[Axis]:
+ return if_instance_do(other, Region, lambda o: Mask(self, o))
+
+ def __invert__(self) -> Snake[Axis]:
+ return Snake(self)
+
+
+[docs]
+ def zip(self, other: Spec) -> Zip[Axis]:
+ """`Zip` the Spec with another, iterating in tandem."""
+ return Zip(self, other)
+
+
+
+[docs]
+ def concat(self, other: Spec) -> Concat[Axis]:
+ """`Concat` the Spec with another, iterating one after the other."""
+ return Concat(self, other)
+
+
+
+[docs]
+ def serialize(self) -> Mapping[str, Any]:
+ """Serialize the spec to a dictionary."""
+ return asdict(self)
+
+
+
+[docs]
+ @classmethod
+ def deserialize(cls, obj):
+ """Deserialize the spec from a dictionary."""
+ return parse_obj_as(cls, obj)
+
+
+
+
+
+[docs]
+@dataclass(config=StrictConfig)
+class Product(Spec[Axis]):
+ """Outer product of two Specs, nesting inner within outer.
+
+ This means that inner will run in its entirety at each point in outer.
+
+ .. example_spec::
+
+ from scanspec.specs import Line
+
+ spec = Line("y", 1, 2, 3) * Line("x", 3, 4, 12)
+ """
+
+ outer: Spec[Axis] = Field(description="Will be executed once")
+ inner: Spec[Axis] = Field(description="Will be executed len(outer) times")
+
+ def axes(self) -> List:
+ return self.outer.axes() + self.inner.axes()
+
+ def calculate(self, bounds=True, nested=False) -> List[Frames[Axis]]:
+ frames_outer = self.outer.calculate(bounds=False, nested=nested)
+ frames_inner = self.inner.calculate(bounds, nested=True)
+ return frames_outer + frames_inner
+
+
+
+
+[docs]
+@dataclass(config=StrictConfig)
+class Repeat(Spec[Axis]):
+ """Repeat an empty frame num times.
+
+ Can be used on the outside of a scan to repeat the same scan many times.
+
+ .. example_spec::
+
+ from scanspec.specs import Line
+
+ spec = 2 * ~Line.bounded("x", 3, 4, 1)
+
+ If you want snaked axes to have no gap between iterations you can do:
+
+ .. example_spec::
+
+ from scanspec.specs import Line, Repeat
+
+ spec = Repeat(2, gap=False) * ~Line.bounded("x", 3, 4, 1)
+
+ .. note:: There is no turnaround arrow at x=4
+ """
+
+ num: int = Field(min=1, description="Number of frames to produce")
+ gap: bool = Field(
+ description="If False and the slowest of the stack of Frames is snaked "
+ "then the end and start of consecutive iterations of Spec will have no gap",
+ default=True,
+ )
+
+ def axes(self) -> List:
+ return []
+
+ def calculate(self, bounds=True, nested=False) -> List[Frames[Axis]]:
+ return [Frames({}, gap=np.full(self.num, self.gap))]
+
+
+
+
+[docs]
+@dataclass(config=StrictConfig)
+class Zip(Spec[Axis]):
+ """Run two Specs in parallel, merging their midpoints together.
+
+ Typically formed using `Spec.zip`.
+
+ Stacks of Frames are merged by:
+
+ - If right creates a stack of a single Frames object of size 1, expand it to
+ the size of the fastest Frames object created by left
+ - Merge individual Frames objects together from fastest to slowest
+
+ This means that Zipping a Spec producing stack [l2, l1] with a Spec
+ producing stack [r1] will assert len(l1)==len(r1), and produce
+ stack [l2, l1.zip(r1)].
+
+ .. example_spec::
+
+ from scanspec.specs import Line
+
+ spec = Line("z", 1, 2, 3) * Line("y", 3, 4, 5).zip(Line("x", 4, 5, 5))
+ """
+
+ left: Spec[Axis] = Field(
+ description="The left-hand Spec to Zip, will appear earlier in axes"
+ )
+ right: Spec[Axis] = Field(
+ description="The right-hand Spec to Zip, will appear later in axes"
+ )
+
+ def axes(self) -> List:
+ return self.left.axes() + self.right.axes()
+
+ def calculate(self, bounds=True, nested=False) -> List[Frames[Axis]]:
+ frames_left = self.left.calculate(bounds, nested)
+ frames_right = self.right.calculate(bounds, nested)
+ assert len(frames_left) >= len(
+ frames_right
+ ), f"Zip requires len({self.left}) >= len({self.right})"
+
+ # Pad and expand the right to be the same size as left. Special case, if
+ # only one Frames object with size 1, expand to the right size
+ if len(frames_right) == 1 and len(frames_right[0]) == 1:
+ # Take the 0th element N times to make a repeated Frames object
+ indices = np.zeros(len(frames_left[-1]), dtype=np.int8)
+ repeated = frames_right[0].extract(indices)
+ if isinstance(frames_left[-1], SnakedFrames):
+ repeated = SnakedFrames.from_frames(repeated)
+ frames_right = [repeated]
+
+ # Left pad frames_right with Nones so they are the same size
+ npad = len(frames_left) - len(frames_right)
+ padded_right: List[Optional[Frames[Axis]]] = [None] * npad
+ # Mypy doesn't like this because lists are invariant:
+ # https://github.com/python/mypy/issues/4244
+ padded_right += frames_right # type: ignore
+
+ # Work through, zipping them together one by one
+ frames = []
+ for left, right in zip(frames_left, padded_right):
+ if right is None:
+ combined = left
+ else:
+ combined = left.zip(right)
+ assert isinstance(
+ combined, Frames
+ ), f"Padding went wrong {frames_left} {padded_right}"
+ frames.append(combined)
+ return frames
+
+
+
+
+[docs]
+@dataclass(config=StrictConfig)
+class Mask(Spec[Axis]):
+ """Restrict Spec to only midpoints that fall inside the given Region.
+
+ Typically created with the ``&`` operator. It also pushes down the
+ ``& | ^ -`` operators to its `Region` to avoid the need for brackets on
+ combinations of Regions.
+
+ If a Region spans multiple Frames objects, they will be squashed together.
+
+ .. example_spec::
+
+ from scanspec.regions import Circle
+ from scanspec.specs import Line
+
+ spec = Line("y", 1, 3, 3) * Line("x", 3, 5, 5) & Circle("x", "y", 4, 2, 1.2)
+
+ See Also: `why-squash-can-change-path`
+ """
+
+ spec: Spec[Axis] = Field(description="The Spec containing the source midpoints")
+ region: Region[Axis] = Field(description="The Region that midpoints will be inside")
+ check_path_changes: bool = Field(
+ description="If True path through scan will not be modified by squash",
+ default=True,
+ )
+
+ def axes(self) -> List:
+ return self.spec.axes()
+
+ def calculate(self, bounds=True, nested=False) -> List[Frames[Axis]]:
+ frames = self.spec.calculate(bounds, nested)
+ for axis_set in self.region.axis_sets():
+ # Find the start and end index of any dimensions containing these axes
+ matches = [i for i, d in enumerate(frames) if set(d.axes()) & axis_set]
+ assert matches, f"No Specs match axes {list(axis_set)}"
+ si, ei = matches[0], matches[-1]
+ if si != ei:
+ # The axis_set spans multiple Dimensions, squash them together
+ # If the spec to be squashed is nested (inside the Mask or outside)
+ # then check the path changes if requested
+ check_path_changes = (nested or si) and self.check_path_changes
+ squashed = squash_frames(frames[si : ei + 1], check_path_changes)
+ frames = frames[:si] + [squashed] + frames[ei + 1 :]
+ # Generate masks from the midpoints showing what's inside
+ masked_frames = []
+ for f in frames:
+ indices = get_mask(self.region, f.midpoints).nonzero()[0]
+ masked_frames.append(f.extract(indices))
+ return masked_frames
+
+ # *+ bind more tightly than &|^ so without these overrides we
+ # would need to add brackets to all combinations of Regions
+ def __or__(self, other: Region[Axis]) -> Mask[Axis]:
+ return if_instance_do(other, Region, lambda o: Mask(self.spec, self.region | o))
+
+ def __and__(self, other: Region[Axis]) -> Mask[Axis]:
+ return if_instance_do(other, Region, lambda o: Mask(self.spec, self.region & o))
+
+ def __xor__(self, other: Region[Axis]) -> Mask[Axis]:
+ return if_instance_do(other, Region, lambda o: Mask(self.spec, self.region ^ o))
+
+ # This is here for completeness, tends not to be called as - binds
+ # tighter than &
+ def __sub__(self, other: Region[Axis]) -> Mask[Axis]:
+ return if_instance_do(other, Region, lambda o: Mask(self.spec, self.region - o))
+
+
+
+
+[docs]
+@dataclass(config=StrictConfig)
+class Snake(Spec[Axis]):
+ """Run the Spec in reverse on every other iteration when nested.
+
+ Typically created with the ``~`` operator.
+
+ .. example_spec::
+
+ from scanspec.specs import Line
+
+ spec = Line("y", 1, 3, 3) * ~Line("x", 3, 5, 5)
+ """
+
+ spec: Spec[Axis] = Field(
+ description="The Spec to run in reverse every other iteration"
+ )
+
+ def axes(self) -> List:
+ return self.spec.axes()
+
+ def calculate(self, bounds=True, nested=False) -> List[Frames[Axis]]:
+ return [
+ SnakedFrames.from_frames(segment)
+ for segment in self.spec.calculate(bounds, nested)
+ ]
+
+
+
+
+[docs]
+@dataclass(config=StrictConfig)
+class Concat(Spec[Axis]):
+ """Concatenate two Specs together, running one after the other.
+
+ Each Dimension of left and right must contain the same axes. Typically
+ formed using `Spec.concat`.
+
+ .. example_spec::
+
+ from scanspec.specs import Line
+
+ spec = Line("x", 1, 3, 3).concat(Line("x", 4, 5, 5))
+ """
+
+ left: Spec[Axis] = Field(
+ description="The left-hand Spec to Concat, midpoints will appear earlier"
+ )
+ right: Spec[Axis] = Field(
+ description="The right-hand Spec to Concat, midpoints will appear later"
+ )
+
+ gap: bool = Field(
+ description="If True, force a gap in the output at the join", default=False
+ )
+ check_path_changes: bool = Field(
+ description="If True path through scan will not be modified by squash",
+ default=True,
+ )
+
+ def axes(self) -> List:
+ left_axes, right_axes = self.left.axes(), self.right.axes()
+ # Assuming the axes are the same, the order does not matter, we inherit the
+ # order from the left-hand side. See also scanspec.core.concat.
+ assert set(left_axes) == set(right_axes), f"axes {left_axes} != {right_axes}"
+ return left_axes
+
+ def calculate(self, bounds=True, nested=False) -> List[Frames[Axis]]:
+ dim_left = squash_frames(
+ self.left.calculate(bounds, nested), nested and self.check_path_changes
+ )
+ dim_right = squash_frames(
+ self.right.calculate(bounds, nested), nested and self.check_path_changes
+ )
+ dim = dim_left.concat(dim_right, self.gap)
+ return [dim]
+
+
+
+
+[docs]
+@dataclass(config=StrictConfig)
+class Squash(Spec[Axis]):
+ """Squash a stack of Frames together into a single expanded Frames object.
+
+ See Also:
+ `why-squash-can-change-path`
+
+ .. example_spec::
+
+ from scanspec.specs import Line, Squash
+
+ spec = Squash(Line("y", 1, 2, 3) * Line("x", 0, 1, 4))
+ """
+
+ spec: Spec[Axis] = Field(description="The Spec to squash the dimensions of")
+ check_path_changes: bool = Field(
+ description="If True path through scan will not be modified by squash",
+ default=True,
+ )
+
+ def axes(self) -> List:
+ return self.spec.axes()
+
+ def calculate(self, bounds=True, nested=False) -> List[Frames[Axis]]:
+ dims = self.spec.calculate(bounds, nested)
+ dim = squash_frames(dims, nested and self.check_path_changes)
+ return [dim]
+
+
+
+def _dimensions_from_indexes(
+ func: Callable[[np.ndarray], Dict[Axis, np.ndarray]],
+ axes: List,
+ num: int,
+ bounds: bool,
+) -> List[Frames[Axis]]:
+ # Calc num midpoints (fences) from 0.5 .. num - 0.5
+ midpoints_calc = func(np.linspace(0.5, num - 0.5, num))
+ midpoints = {a: midpoints_calc[a] for a in axes}
+ if bounds:
+ # Calc num + 1 bounds (posts) from 0 .. num
+ bounds_calc = func(np.linspace(0, num, num + 1))
+ lower = {a: bounds_calc[a][:-1] for a in axes}
+ upper = {a: bounds_calc[a][1:] for a in axes}
+ # Points must have no gap as upper[a][i] == lower[a][i+1]
+ # because we initialized it to be that way
+ gap = np.zeros(num, dtype=np.bool_)
+ dimension = Frames(midpoints, lower, upper, gap)
+ # But calc the first point as difference between first
+ # and last
+ gap[0] = gap_between_frames(dimension, dimension)
+ else:
+ # Gap can be calculated in Dimension
+ dimension = Frames(midpoints)
+ return [dimension]
+
+
+
+[docs]
+@dataclass(config=StrictConfig)
+class Line(Spec[Axis]):
+ """Linearly spaced frames with start and stop as first and last midpoints.
+
+ .. example_spec::
+
+ from scanspec.specs import Line
+
+ spec = Line("x", 1, 2, 5)
+ """
+
+ axis: Axis = Field(description="An identifier for what to move")
+ start: float = Field(description="Midpoint of the first point of the line")
+ stop: float = Field(description="Midpoint of the last point of the line")
+ num: int = Field(min=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
+ )
+
+
+[docs]
+ @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(min=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)
+
+
+
+
+
+[docs]
+@dataclass(config=StrictConfig)
+class Static(Spec[Axis]):
+ """A static frame, repeated num times, with axis at value.
+
+ Can be used to set axis=value at every point in a scan.
+
+ .. example_spec::
+
+ from scanspec.specs import Line, Static
+
+ spec = Line("y", 1, 2, 3).zip(Static("x", 3))
+ """
+
+ axis: Axis = Field(description="An identifier for what to move")
+ value: float = Field(description="The value at each point")
+ num: int = Field(min=1, description="Number of frames to produce", default=1)
+
+
+[docs]
+ @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),
+ ) -> 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
+ )
+
+
+
+
+[docs]
+@dataclass(config=StrictConfig)
+class Spiral(Spec[Axis]):
+ """Archimedean spiral of "x_axis" and "y_axis".
+
+ Starts at centre point ("x_start", "y_start") with angle "rotate". Produces
+ "num" points in a spiral spanning width of "x_range" and height of "y_range"
+
+ .. example_spec::
+
+ from scanspec.specs import Spiral
+
+ spec = Spiral("x", "y", 1, 5, 10, 50, 30)
+ """
+
+ # TODO: Make use of typing.Annotated upon fix of
+ # https://github.com/pydantic/pydantic/issues/3496
+ x_axis: Axis = Field(description="An identifier for what to move for x")
+ y_axis: Axis = Field(description="An identifier for what to move for y")
+ x_start: float = Field(description="x centre of the spiral")
+ y_start: float = Field(description="y centre of the spiral")
+ x_range: float = Field(description="x width of the spiral")
+ y_range: float = Field(description="y width of the spiral")
+ num: int = Field(min=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
+ )
+
+
+[docs]
+ @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
+ )
+
+
+
+
+
+[docs]
+def fly(spec: Spec[Axis], duration: float) -> Spec[Axis]:
+ """Flyscan, zipping with fixed duration for every frame.
+
+ Args:
+ spec: The source `Spec` to continuously move
+ duration: How long to spend at each frame in the spec
+
+ .. example_spec::
+
+ from scanspec.specs import Line, fly
+
+ spec = fly(Line("x", 1, 2, 3), 0.1)
+ """
+ return spec.zip(Static.duration(duration))
+
+
+
+
+[docs]
+def step(spec: Spec[Axis], duration: float, num: int = 1) -> Spec[Axis]:
+ """Step scan, with num frames of given duration at each frame in the spec.
+
+ Args:
+ spec: The source `Spec` with midpoints to move to and stop
+ duration: The duration of each scan frame
+ num: Number of frames to produce with given duration at each of frame
+ in the spec
+
+ .. example_spec::
+
+ from scanspec.specs import Line, step
+
+ spec = step(Line("x", 1, 2, 3), 0.1)
+ """
+ return spec * Static.duration(duration, num)
+
+
Short
+ */ + .o-tooltip--left { + position: relative; + } + + .o-tooltip--left:after { + opacity: 0; + visibility: hidden; + position: absolute; + content: attr(data-tooltip); + padding: .2em; + font-size: .8em; + left: -.2em; + background: grey; + color: white; + white-space: nowrap; + z-index: 2; + border-radius: 2px; + transform: translateX(-102%) translateY(0); + transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1); +} + +.o-tooltip--left:hover:after { + display: block; + opacity: 1; + visibility: visible; + transform: translateX(-100%) translateY(0); + transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1); + transition-delay: .5s; +} + +/* By default the copy button shouldn't show up when printing a page */ +@media print { + button.copybtn { + display: none; + } +} diff --git a/main/_static/copybutton.js b/main/_static/copybutton.js new file mode 100644 index 00000000..e0da1932 --- /dev/null +++ b/main/_static/copybutton.js @@ -0,0 +1,248 @@ +// Localization support +const messages = { + 'en': { + 'copy': 'Copy', + 'copy_to_clipboard': 'Copy to clipboard', + 'copy_success': 'Copied!', + 'copy_failure': 'Failed to copy', + }, + 'es' : { + 'copy': 'Copiar', + 'copy_to_clipboard': 'Copiar al portapapeles', + 'copy_success': '¡Copiado!', + 'copy_failure': 'Error al copiar', + }, + 'de' : { + 'copy': 'Kopieren', + 'copy_to_clipboard': 'In die Zwischenablage kopieren', + 'copy_success': 'Kopiert!', + 'copy_failure': 'Fehler beim Kopieren', + }, + 'fr' : { + 'copy': 'Copier', + 'copy_to_clipboard': 'Copier dans le presse-papier', + 'copy_success': 'Copié !', + 'copy_failure': 'Échec de la copie', + }, + 'ru': { + 'copy': 'Скопировать', + 'copy_to_clipboard': 'Скопировать в буфер', + 'copy_success': 'Скопировано!', + 'copy_failure': 'Не удалось скопировать', + }, + 'zh-CN': { + 'copy': '复制', + 'copy_to_clipboard': '复制到剪贴板', + 'copy_success': '复制成功!', + 'copy_failure': '复制失败', + }, + 'it' : { + 'copy': 'Copiare', + 'copy_to_clipboard': 'Copiato negli appunti', + 'copy_success': 'Copiato!', + 'copy_failure': 'Errore durante la copia', + } +} + +let locale = 'en' +if( document.documentElement.lang !== undefined + && messages[document.documentElement.lang] !== undefined ) { + locale = document.documentElement.lang +} + +let doc_url_root = DOCUMENTATION_OPTIONS.URL_ROOT; +if (doc_url_root == '#') { + doc_url_root = ''; +} + +/** + * SVG files for our copy buttons + */ +let iconCheck = `` + +// If the user specified their own SVG use that, otherwise use the default +let iconCopy = ``; +if (!iconCopy) { + iconCopy = `` +} + +/** + * Set up copy/paste for code blocks + */ + +const runWhenDOMLoaded = cb => { + if (document.readyState != 'loading') { + cb() + } else if (document.addEventListener) { + document.addEventListener('DOMContentLoaded', cb) + } else { + document.attachEvent('onreadystatechange', function() { + if (document.readyState == 'complete') cb() + }) + } +} + +const codeCellId = index => `codecell${index}` + +// Clears selected text since ClipboardJS will select the text when copying +const clearSelection = () => { + if (window.getSelection) { + window.getSelection().removeAllRanges() + } else if (document.selection) { + document.selection.empty() + } +} + +// Changes tooltip text for a moment, then changes it back +// We want the timeout of our `success` class to be a bit shorter than the +// tooltip and icon change, so that we can hide the icon before changing back. +var timeoutIcon = 2000; +var timeoutSuccessClass = 1500; + +const temporarilyChangeTooltip = (el, oldText, newText) => { + el.setAttribute('data-tooltip', newText) + el.classList.add('success') + // Remove success a little bit sooner than we change the tooltip + // So that we can use CSS to hide the copybutton first + setTimeout(() => el.classList.remove('success'), timeoutSuccessClass) + setTimeout(() => el.setAttribute('data-tooltip', oldText), timeoutIcon) +} + +// Changes the copy button icon for two seconds, then changes it back +const temporarilyChangeIcon = (el) => { + el.innerHTML = iconCheck; + setTimeout(() => {el.innerHTML = iconCopy}, timeoutIcon) +} + +const addCopyButtonToCodeCells = () => { + // If ClipboardJS hasn't loaded, wait a bit and try again. This + // happens because we load ClipboardJS asynchronously. + if (window.ClipboardJS === undefined) { + setTimeout(addCopyButtonToCodeCells, 250) + return + } + + // Add copybuttons to all of our code cells + const COPYBUTTON_SELECTOR = 'div.highlight pre'; + const codeCells = document.querySelectorAll(COPYBUTTON_SELECTOR) + codeCells.forEach((codeCell, index) => { + const id = codeCellId(index) + codeCell.setAttribute('id', id) + + const clipboardButton = id => + `` + codeCell.insertAdjacentHTML('afterend', clipboardButton(id)) + }) + +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + +/** + * Removes excluded text from a Node. + * + * @param {Node} target Node to filter. + * @param {string} exclude CSS selector of nodes to exclude. + * @returns {DOMString} Text from `target` with text removed. + */ +function filterText(target, exclude) { + const clone = target.cloneNode(true); // clone as to not modify the live DOM + if (exclude) { + // remove excluded nodes + clone.querySelectorAll(exclude).forEach(node => node.remove()); + } + return clone.innerText; +} + +// Callback when a copy button is clicked. Will be passed the node that was clicked +// should then grab the text and replace pieces of text that shouldn't be used in output +function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") { + var regexp; + var match; + + // Do we check for line continuation characters and "HERE-documents"? + var useLineCont = !!lineContinuationChar + var useHereDoc = !!hereDocDelim + + // create regexp to capture prompt and remaining line + if (isRegexp) { + regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)') + } else { + regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)') + } + + const outputLines = []; + var promptFound = false; + var gotLineCont = false; + var gotHereDoc = false; + const lineGotPrompt = []; + for (const line of textContent.split('\n')) { + match = line.match(regexp) + if (match || gotLineCont || gotHereDoc) { + promptFound = regexp.test(line) + lineGotPrompt.push(promptFound) + if (removePrompts && promptFound) { + outputLines.push(match[2]) + } else { + outputLines.push(line) + } + gotLineCont = line.endsWith(lineContinuationChar) & useLineCont + if (line.includes(hereDocDelim) & useHereDoc) + gotHereDoc = !gotHereDoc + } else if (!onlyCopyPromptLines) { + outputLines.push(line) + } else if (copyEmptyLines && line.trim() === '') { + outputLines.push(line) + } + } + + // If no lines with the prompt were found then just use original lines + if (lineGotPrompt.some(v => v === true)) { + textContent = outputLines.join('\n'); + } + + // Remove a trailing newline to avoid auto-running when pasting + if (textContent.endsWith("\n")) { + textContent = textContent.slice(0, -1) + } + return textContent +} + + +var copyTargetText = (trigger) => { + var target = document.querySelector(trigger.attributes['data-clipboard-target'].value); + + // get filtered text + let exclude = '.linenos'; + + let text = filterText(target, exclude); + return formatCopyText(text, '>>> |\\.\\.\\. |\\$ |In \\[\\d*\\]: | {2,5}\\.\\.\\.: | {5,8}: ', true, true, true, true, '', '') +} + + // Initialize with a callback so we can modify the text before copy + const clipboard = new ClipboardJS('.copybtn', {text: copyTargetText}) + + // Update UI with error/success messages + clipboard.on('success', event => { + clearSelection() + temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_success']) + temporarilyChangeIcon(event.trigger) + }) + + clipboard.on('error', event => { + temporarilyChangeTooltip(event.trigger, messages[locale]['copy'], messages[locale]['copy_failure']) + }) +} + +runWhenDOMLoaded(addCopyButtonToCodeCells) \ No newline at end of file diff --git a/main/_static/copybutton_funcs.js b/main/_static/copybutton_funcs.js new file mode 100644 index 00000000..dbe1aaad --- /dev/null +++ b/main/_static/copybutton_funcs.js @@ -0,0 +1,73 @@ +function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + +/** + * Removes excluded text from a Node. + * + * @param {Node} target Node to filter. + * @param {string} exclude CSS selector of nodes to exclude. + * @returns {DOMString} Text from `target` with text removed. + */ +export function filterText(target, exclude) { + const clone = target.cloneNode(true); // clone as to not modify the live DOM + if (exclude) { + // remove excluded nodes + clone.querySelectorAll(exclude).forEach(node => node.remove()); + } + return clone.innerText; +} + +// Callback when a copy button is clicked. Will be passed the node that was clicked +// should then grab the text and replace pieces of text that shouldn't be used in output +export function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") { + var regexp; + var match; + + // Do we check for line continuation characters and "HERE-documents"? + var useLineCont = !!lineContinuationChar + var useHereDoc = !!hereDocDelim + + // create regexp to capture prompt and remaining line + if (isRegexp) { + regexp = new RegExp('^(' + copybuttonPromptText + ')(.*)') + } else { + regexp = new RegExp('^(' + escapeRegExp(copybuttonPromptText) + ')(.*)') + } + + const outputLines = []; + var promptFound = false; + var gotLineCont = false; + var gotHereDoc = false; + const lineGotPrompt = []; + for (const line of textContent.split('\n')) { + match = line.match(regexp) + if (match || gotLineCont || gotHereDoc) { + promptFound = regexp.test(line) + lineGotPrompt.push(promptFound) + if (removePrompts && promptFound) { + outputLines.push(match[2]) + } else { + outputLines.push(line) + } + gotLineCont = line.endsWith(lineContinuationChar) & useLineCont + if (line.includes(hereDocDelim) & useHereDoc) + gotHereDoc = !gotHereDoc + } else if (!onlyCopyPromptLines) { + outputLines.push(line) + } else if (copyEmptyLines && line.trim() === '') { + outputLines.push(line) + } + } + + // If no lines with the prompt were found then just use original lines + if (lineGotPrompt.some(v => v === true)) { + textContent = outputLines.join('\n'); + } + + // Remove a trailing newline to avoid auto-running when pasting + if (textContent.endsWith("\n")) { + textContent = textContent.slice(0, -1) + } + return textContent +} diff --git a/main/_static/design-style.1e8bd061cd6da7fc9cf755528e8ffc24.min.css b/main/_static/design-style.1e8bd061cd6da7fc9cf755528e8ffc24.min.css new file mode 100644 index 00000000..eb19f698 --- /dev/null +++ b/main/_static/design-style.1e8bd061cd6da7fc9cf755528e8ffc24.min.css @@ -0,0 +1 @@ +.sd-bg-primary{background-color:var(--sd-color-primary) !important}.sd-bg-text-primary{color:var(--sd-color-primary-text) !important}button.sd-bg-primary:focus,button.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}a.sd-bg-primary:focus,a.sd-bg-primary:hover{background-color:var(--sd-color-primary-highlight) !important}.sd-bg-secondary{background-color:var(--sd-color-secondary) !important}.sd-bg-text-secondary{color:var(--sd-color-secondary-text) !important}button.sd-bg-secondary:focus,button.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}a.sd-bg-secondary:focus,a.sd-bg-secondary:hover{background-color:var(--sd-color-secondary-highlight) !important}.sd-bg-success{background-color:var(--sd-color-success) !important}.sd-bg-text-success{color:var(--sd-color-success-text) !important}button.sd-bg-success:focus,button.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}a.sd-bg-success:focus,a.sd-bg-success:hover{background-color:var(--sd-color-success-highlight) !important}.sd-bg-info{background-color:var(--sd-color-info) !important}.sd-bg-text-info{color:var(--sd-color-info-text) !important}button.sd-bg-info:focus,button.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}a.sd-bg-info:focus,a.sd-bg-info:hover{background-color:var(--sd-color-info-highlight) !important}.sd-bg-warning{background-color:var(--sd-color-warning) !important}.sd-bg-text-warning{color:var(--sd-color-warning-text) !important}button.sd-bg-warning:focus,button.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}a.sd-bg-warning:focus,a.sd-bg-warning:hover{background-color:var(--sd-color-warning-highlight) !important}.sd-bg-danger{background-color:var(--sd-color-danger) !important}.sd-bg-text-danger{color:var(--sd-color-danger-text) !important}button.sd-bg-danger:focus,button.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}a.sd-bg-danger:focus,a.sd-bg-danger:hover{background-color:var(--sd-color-danger-highlight) !important}.sd-bg-light{background-color:var(--sd-color-light) !important}.sd-bg-text-light{color:var(--sd-color-light-text) !important}button.sd-bg-light:focus,button.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}a.sd-bg-light:focus,a.sd-bg-light:hover{background-color:var(--sd-color-light-highlight) !important}.sd-bg-muted{background-color:var(--sd-color-muted) !important}.sd-bg-text-muted{color:var(--sd-color-muted-text) !important}button.sd-bg-muted:focus,button.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}a.sd-bg-muted:focus,a.sd-bg-muted:hover{background-color:var(--sd-color-muted-highlight) !important}.sd-bg-dark{background-color:var(--sd-color-dark) !important}.sd-bg-text-dark{color:var(--sd-color-dark-text) !important}button.sd-bg-dark:focus,button.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}a.sd-bg-dark:focus,a.sd-bg-dark:hover{background-color:var(--sd-color-dark-highlight) !important}.sd-bg-black{background-color:var(--sd-color-black) !important}.sd-bg-text-black{color:var(--sd-color-black-text) !important}button.sd-bg-black:focus,button.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}a.sd-bg-black:focus,a.sd-bg-black:hover{background-color:var(--sd-color-black-highlight) !important}.sd-bg-white{background-color:var(--sd-color-white) !important}.sd-bg-text-white{color:var(--sd-color-white-text) !important}button.sd-bg-white:focus,button.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}a.sd-bg-white:focus,a.sd-bg-white:hover{background-color:var(--sd-color-white-highlight) !important}.sd-text-primary,.sd-text-primary>p{color:var(--sd-color-primary) !important}a.sd-text-primary:focus,a.sd-text-primary:hover{color:var(--sd-color-primary-highlight) !important}.sd-text-secondary,.sd-text-secondary>p{color:var(--sd-color-secondary) !important}a.sd-text-secondary:focus,a.sd-text-secondary:hover{color:var(--sd-color-secondary-highlight) !important}.sd-text-success,.sd-text-success>p{color:var(--sd-color-success) !important}a.sd-text-success:focus,a.sd-text-success:hover{color:var(--sd-color-success-highlight) !important}.sd-text-info,.sd-text-info>p{color:var(--sd-color-info) !important}a.sd-text-info:focus,a.sd-text-info:hover{color:var(--sd-color-info-highlight) !important}.sd-text-warning,.sd-text-warning>p{color:var(--sd-color-warning) !important}a.sd-text-warning:focus,a.sd-text-warning:hover{color:var(--sd-color-warning-highlight) !important}.sd-text-danger,.sd-text-danger>p{color:var(--sd-color-danger) !important}a.sd-text-danger:focus,a.sd-text-danger:hover{color:var(--sd-color-danger-highlight) !important}.sd-text-light,.sd-text-light>p{color:var(--sd-color-light) !important}a.sd-text-light:focus,a.sd-text-light:hover{color:var(--sd-color-light-highlight) !important}.sd-text-muted,.sd-text-muted>p{color:var(--sd-color-muted) !important}a.sd-text-muted:focus,a.sd-text-muted:hover{color:var(--sd-color-muted-highlight) !important}.sd-text-dark,.sd-text-dark>p{color:var(--sd-color-dark) !important}a.sd-text-dark:focus,a.sd-text-dark:hover{color:var(--sd-color-dark-highlight) !important}.sd-text-black,.sd-text-black>p{color:var(--sd-color-black) !important}a.sd-text-black:focus,a.sd-text-black:hover{color:var(--sd-color-black-highlight) !important}.sd-text-white,.sd-text-white>p{color:var(--sd-color-white) !important}a.sd-text-white:focus,a.sd-text-white:hover{color:var(--sd-color-white-highlight) !important}.sd-outline-primary{border-color:var(--sd-color-primary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-primary:focus,a.sd-outline-primary:hover{border-color:var(--sd-color-primary-highlight) !important}.sd-outline-secondary{border-color:var(--sd-color-secondary) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-secondary:focus,a.sd-outline-secondary:hover{border-color:var(--sd-color-secondary-highlight) !important}.sd-outline-success{border-color:var(--sd-color-success) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-success:focus,a.sd-outline-success:hover{border-color:var(--sd-color-success-highlight) !important}.sd-outline-info{border-color:var(--sd-color-info) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-info:focus,a.sd-outline-info:hover{border-color:var(--sd-color-info-highlight) !important}.sd-outline-warning{border-color:var(--sd-color-warning) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-warning:focus,a.sd-outline-warning:hover{border-color:var(--sd-color-warning-highlight) !important}.sd-outline-danger{border-color:var(--sd-color-danger) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-danger:focus,a.sd-outline-danger:hover{border-color:var(--sd-color-danger-highlight) !important}.sd-outline-light{border-color:var(--sd-color-light) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-light:focus,a.sd-outline-light:hover{border-color:var(--sd-color-light-highlight) !important}.sd-outline-muted{border-color:var(--sd-color-muted) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-muted:focus,a.sd-outline-muted:hover{border-color:var(--sd-color-muted-highlight) !important}.sd-outline-dark{border-color:var(--sd-color-dark) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-dark:focus,a.sd-outline-dark:hover{border-color:var(--sd-color-dark-highlight) !important}.sd-outline-black{border-color:var(--sd-color-black) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-black:focus,a.sd-outline-black:hover{border-color:var(--sd-color-black-highlight) !important}.sd-outline-white{border-color:var(--sd-color-white) !important;border-style:solid !important;border-width:1px !important}a.sd-outline-white:focus,a.sd-outline-white:hover{border-color:var(--sd-color-white-highlight) !important}.sd-bg-transparent{background-color:transparent !important}.sd-outline-transparent{border-color:transparent !important}.sd-text-transparent{color:transparent !important}.sd-p-0{padding:0 !important}.sd-pt-0,.sd-py-0{padding-top:0 !important}.sd-pr-0,.sd-px-0{padding-right:0 !important}.sd-pb-0,.sd-py-0{padding-bottom:0 !important}.sd-pl-0,.sd-px-0{padding-left:0 !important}.sd-p-1{padding:.25rem !important}.sd-pt-1,.sd-py-1{padding-top:.25rem !important}.sd-pr-1,.sd-px-1{padding-right:.25rem !important}.sd-pb-1,.sd-py-1{padding-bottom:.25rem !important}.sd-pl-1,.sd-px-1{padding-left:.25rem !important}.sd-p-2{padding:.5rem !important}.sd-pt-2,.sd-py-2{padding-top:.5rem !important}.sd-pr-2,.sd-px-2{padding-right:.5rem !important}.sd-pb-2,.sd-py-2{padding-bottom:.5rem !important}.sd-pl-2,.sd-px-2{padding-left:.5rem !important}.sd-p-3{padding:1rem !important}.sd-pt-3,.sd-py-3{padding-top:1rem !important}.sd-pr-3,.sd-px-3{padding-right:1rem !important}.sd-pb-3,.sd-py-3{padding-bottom:1rem !important}.sd-pl-3,.sd-px-3{padding-left:1rem !important}.sd-p-4{padding:1.5rem !important}.sd-pt-4,.sd-py-4{padding-top:1.5rem !important}.sd-pr-4,.sd-px-4{padding-right:1.5rem !important}.sd-pb-4,.sd-py-4{padding-bottom:1.5rem !important}.sd-pl-4,.sd-px-4{padding-left:1.5rem !important}.sd-p-5{padding:3rem !important}.sd-pt-5,.sd-py-5{padding-top:3rem !important}.sd-pr-5,.sd-px-5{padding-right:3rem !important}.sd-pb-5,.sd-py-5{padding-bottom:3rem !important}.sd-pl-5,.sd-px-5{padding-left:3rem !important}.sd-m-auto{margin:auto !important}.sd-mt-auto,.sd-my-auto{margin-top:auto !important}.sd-mr-auto,.sd-mx-auto{margin-right:auto !important}.sd-mb-auto,.sd-my-auto{margin-bottom:auto !important}.sd-ml-auto,.sd-mx-auto{margin-left:auto !important}.sd-m-0{margin:0 !important}.sd-mt-0,.sd-my-0{margin-top:0 !important}.sd-mr-0,.sd-mx-0{margin-right:0 !important}.sd-mb-0,.sd-my-0{margin-bottom:0 !important}.sd-ml-0,.sd-mx-0{margin-left:0 !important}.sd-m-1{margin:.25rem !important}.sd-mt-1,.sd-my-1{margin-top:.25rem !important}.sd-mr-1,.sd-mx-1{margin-right:.25rem !important}.sd-mb-1,.sd-my-1{margin-bottom:.25rem !important}.sd-ml-1,.sd-mx-1{margin-left:.25rem !important}.sd-m-2{margin:.5rem !important}.sd-mt-2,.sd-my-2{margin-top:.5rem !important}.sd-mr-2,.sd-mx-2{margin-right:.5rem !important}.sd-mb-2,.sd-my-2{margin-bottom:.5rem !important}.sd-ml-2,.sd-mx-2{margin-left:.5rem !important}.sd-m-3{margin:1rem !important}.sd-mt-3,.sd-my-3{margin-top:1rem !important}.sd-mr-3,.sd-mx-3{margin-right:1rem !important}.sd-mb-3,.sd-my-3{margin-bottom:1rem !important}.sd-ml-3,.sd-mx-3{margin-left:1rem !important}.sd-m-4{margin:1.5rem !important}.sd-mt-4,.sd-my-4{margin-top:1.5rem !important}.sd-mr-4,.sd-mx-4{margin-right:1.5rem !important}.sd-mb-4,.sd-my-4{margin-bottom:1.5rem !important}.sd-ml-4,.sd-mx-4{margin-left:1.5rem !important}.sd-m-5{margin:3rem !important}.sd-mt-5,.sd-my-5{margin-top:3rem !important}.sd-mr-5,.sd-mx-5{margin-right:3rem !important}.sd-mb-5,.sd-my-5{margin-bottom:3rem !important}.sd-ml-5,.sd-mx-5{margin-left:3rem !important}.sd-w-25{width:25% !important}.sd-w-50{width:50% !important}.sd-w-75{width:75% !important}.sd-w-100{width:100% !important}.sd-w-auto{width:auto !important}.sd-h-25{height:25% !important}.sd-h-50{height:50% !important}.sd-h-75{height:75% !important}.sd-h-100{height:100% !important}.sd-h-auto{height:auto !important}.sd-d-none{display:none !important}.sd-d-inline{display:inline !important}.sd-d-inline-block{display:inline-block !important}.sd-d-block{display:block !important}.sd-d-grid{display:grid !important}.sd-d-flex-row{display:-ms-flexbox !important;display:flex !important;flex-direction:row !important}.sd-d-flex-column{display:-ms-flexbox !important;display:flex !important;flex-direction:column !important}.sd-d-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}@media(min-width: 576px){.sd-d-sm-none{display:none !important}.sd-d-sm-inline{display:inline !important}.sd-d-sm-inline-block{display:inline-block !important}.sd-d-sm-block{display:block !important}.sd-d-sm-grid{display:grid !important}.sd-d-sm-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-sm-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 768px){.sd-d-md-none{display:none !important}.sd-d-md-inline{display:inline !important}.sd-d-md-inline-block{display:inline-block !important}.sd-d-md-block{display:block !important}.sd-d-md-grid{display:grid !important}.sd-d-md-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-md-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 992px){.sd-d-lg-none{display:none !important}.sd-d-lg-inline{display:inline !important}.sd-d-lg-inline-block{display:inline-block !important}.sd-d-lg-block{display:block !important}.sd-d-lg-grid{display:grid !important}.sd-d-lg-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-lg-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}@media(min-width: 1200px){.sd-d-xl-none{display:none !important}.sd-d-xl-inline{display:inline !important}.sd-d-xl-inline-block{display:inline-block !important}.sd-d-xl-block{display:block !important}.sd-d-xl-grid{display:grid !important}.sd-d-xl-flex{display:-ms-flexbox !important;display:flex !important}.sd-d-xl-inline-flex{display:-ms-inline-flexbox !important;display:inline-flex !important}}.sd-align-major-start{justify-content:flex-start !important}.sd-align-major-end{justify-content:flex-end !important}.sd-align-major-center{justify-content:center !important}.sd-align-major-justify{justify-content:space-between !important}.sd-align-major-spaced{justify-content:space-evenly !important}.sd-align-minor-start{align-items:flex-start !important}.sd-align-minor-end{align-items:flex-end !important}.sd-align-minor-center{align-items:center !important}.sd-align-minor-stretch{align-items:stretch !important}.sd-text-justify{text-align:justify !important}.sd-text-left{text-align:left !important}.sd-text-right{text-align:right !important}.sd-text-center{text-align:center !important}.sd-font-weight-light{font-weight:300 !important}.sd-font-weight-lighter{font-weight:lighter !important}.sd-font-weight-normal{font-weight:400 !important}.sd-font-weight-bold{font-weight:700 !important}.sd-font-weight-bolder{font-weight:bolder !important}.sd-font-italic{font-style:italic !important}.sd-text-decoration-none{text-decoration:none !important}.sd-text-lowercase{text-transform:lowercase !important}.sd-text-uppercase{text-transform:uppercase !important}.sd-text-capitalize{text-transform:capitalize !important}.sd-text-wrap{white-space:normal !important}.sd-text-nowrap{white-space:nowrap !important}.sd-text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sd-fs-1,.sd-fs-1>p{font-size:calc(1.375rem + 1.5vw) !important;line-height:unset !important}.sd-fs-2,.sd-fs-2>p{font-size:calc(1.325rem + 0.9vw) !important;line-height:unset !important}.sd-fs-3,.sd-fs-3>p{font-size:calc(1.3rem + 0.6vw) !important;line-height:unset !important}.sd-fs-4,.sd-fs-4>p{font-size:calc(1.275rem + 0.3vw) !important;line-height:unset !important}.sd-fs-5,.sd-fs-5>p{font-size:1.25rem !important;line-height:unset !important}.sd-fs-6,.sd-fs-6>p{font-size:1rem !important;line-height:unset !important}.sd-border-0{border:0 solid !important}.sd-border-top-0{border-top:0 solid !important}.sd-border-bottom-0{border-bottom:0 solid !important}.sd-border-right-0{border-right:0 solid !important}.sd-border-left-0{border-left:0 solid !important}.sd-border-1{border:1px solid !important}.sd-border-top-1{border-top:1px solid !important}.sd-border-bottom-1{border-bottom:1px solid !important}.sd-border-right-1{border-right:1px solid !important}.sd-border-left-1{border-left:1px solid !important}.sd-border-2{border:2px solid !important}.sd-border-top-2{border-top:2px solid !important}.sd-border-bottom-2{border-bottom:2px solid !important}.sd-border-right-2{border-right:2px solid !important}.sd-border-left-2{border-left:2px solid !important}.sd-border-3{border:3px solid !important}.sd-border-top-3{border-top:3px solid !important}.sd-border-bottom-3{border-bottom:3px solid !important}.sd-border-right-3{border-right:3px solid !important}.sd-border-left-3{border-left:3px solid !important}.sd-border-4{border:4px solid !important}.sd-border-top-4{border-top:4px solid !important}.sd-border-bottom-4{border-bottom:4px solid !important}.sd-border-right-4{border-right:4px solid !important}.sd-border-left-4{border-left:4px solid !important}.sd-border-5{border:5px solid !important}.sd-border-top-5{border-top:5px solid !important}.sd-border-bottom-5{border-bottom:5px solid !important}.sd-border-right-5{border-right:5px solid !important}.sd-border-left-5{border-left:5px solid !important}.sd-rounded-0{border-radius:0 !important}.sd-rounded-1{border-radius:.2rem !important}.sd-rounded-2{border-radius:.3rem !important}.sd-rounded-3{border-radius:.5rem !important}.sd-rounded-pill{border-radius:50rem !important}.sd-rounded-circle{border-radius:50% !important}.shadow-none{box-shadow:none !important}.sd-shadow-sm{box-shadow:0 .125rem .25rem var(--sd-color-shadow) !important}.sd-shadow-md{box-shadow:0 .5rem 1rem var(--sd-color-shadow) !important}.sd-shadow-lg{box-shadow:0 1rem 3rem var(--sd-color-shadow) !important}@keyframes sd-slide-from-left{0%{transform:translateX(-100%)}100%{transform:translateX(0)}}@keyframes sd-slide-from-right{0%{transform:translateX(200%)}100%{transform:translateX(0)}}@keyframes sd-grow100{0%{transform:scale(0);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50{0%{transform:scale(0.5);opacity:.5}100%{transform:scale(1);opacity:1}}@keyframes sd-grow50-rot20{0%{transform:scale(0.5) rotateZ(-20deg);opacity:.5}75%{transform:scale(1) rotateZ(5deg);opacity:1}95%{transform:scale(1) rotateZ(-1deg);opacity:1}100%{transform:scale(1) rotateZ(0);opacity:1}}.sd-animate-slide-from-left{animation:1s ease-out 0s 1 normal none running sd-slide-from-left}.sd-animate-slide-from-right{animation:1s ease-out 0s 1 normal none running sd-slide-from-right}.sd-animate-grow100{animation:1s ease-out 0s 1 normal none running sd-grow100}.sd-animate-grow50{animation:1s ease-out 0s 1 normal none running sd-grow50}.sd-animate-grow50-rot20{animation:1s ease-out 0s 1 normal none running sd-grow50-rot20}.sd-badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.sd-badge:empty{display:none}a.sd-badge{text-decoration:none}.sd-btn .sd-badge{position:relative;top:-1px}.sd-btn{background-color:transparent;border:1px solid transparent;border-radius:.25rem;cursor:pointer;display:inline-block;font-weight:400;font-size:1rem;line-height:1.5;padding:.375rem .75rem;text-align:center;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;vertical-align:middle;user-select:none;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none}.sd-btn:hover{text-decoration:none}@media(prefers-reduced-motion: reduce){.sd-btn{transition:none}}.sd-btn-primary,.sd-btn-outline-primary:hover,.sd-btn-outline-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-primary:hover,.sd-btn-primary:focus{color:var(--sd-color-primary-text) !important;background-color:var(--sd-color-primary-highlight) !important;border-color:var(--sd-color-primary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-primary{color:var(--sd-color-primary) !important;border-color:var(--sd-color-primary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary,.sd-btn-outline-secondary:hover,.sd-btn-outline-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-secondary:hover,.sd-btn-secondary:focus{color:var(--sd-color-secondary-text) !important;background-color:var(--sd-color-secondary-highlight) !important;border-color:var(--sd-color-secondary-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-secondary{color:var(--sd-color-secondary) !important;border-color:var(--sd-color-secondary) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success,.sd-btn-outline-success:hover,.sd-btn-outline-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-success:hover,.sd-btn-success:focus{color:var(--sd-color-success-text) !important;background-color:var(--sd-color-success-highlight) !important;border-color:var(--sd-color-success-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-success{color:var(--sd-color-success) !important;border-color:var(--sd-color-success) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info,.sd-btn-outline-info:hover,.sd-btn-outline-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-info:hover,.sd-btn-info:focus{color:var(--sd-color-info-text) !important;background-color:var(--sd-color-info-highlight) !important;border-color:var(--sd-color-info-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-info{color:var(--sd-color-info) !important;border-color:var(--sd-color-info) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning,.sd-btn-outline-warning:hover,.sd-btn-outline-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-warning:hover,.sd-btn-warning:focus{color:var(--sd-color-warning-text) !important;background-color:var(--sd-color-warning-highlight) !important;border-color:var(--sd-color-warning-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-warning{color:var(--sd-color-warning) !important;border-color:var(--sd-color-warning) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger,.sd-btn-outline-danger:hover,.sd-btn-outline-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-danger:hover,.sd-btn-danger:focus{color:var(--sd-color-danger-text) !important;background-color:var(--sd-color-danger-highlight) !important;border-color:var(--sd-color-danger-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-danger{color:var(--sd-color-danger) !important;border-color:var(--sd-color-danger) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light,.sd-btn-outline-light:hover,.sd-btn-outline-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-light:hover,.sd-btn-light:focus{color:var(--sd-color-light-text) !important;background-color:var(--sd-color-light-highlight) !important;border-color:var(--sd-color-light-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-light{color:var(--sd-color-light) !important;border-color:var(--sd-color-light) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted,.sd-btn-outline-muted:hover,.sd-btn-outline-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-muted:hover,.sd-btn-muted:focus{color:var(--sd-color-muted-text) !important;background-color:var(--sd-color-muted-highlight) !important;border-color:var(--sd-color-muted-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-muted{color:var(--sd-color-muted) !important;border-color:var(--sd-color-muted) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark,.sd-btn-outline-dark:hover,.sd-btn-outline-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-dark:hover,.sd-btn-dark:focus{color:var(--sd-color-dark-text) !important;background-color:var(--sd-color-dark-highlight) !important;border-color:var(--sd-color-dark-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-dark{color:var(--sd-color-dark) !important;border-color:var(--sd-color-dark) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black,.sd-btn-outline-black:hover,.sd-btn-outline-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-black:hover,.sd-btn-black:focus{color:var(--sd-color-black-text) !important;background-color:var(--sd-color-black-highlight) !important;border-color:var(--sd-color-black-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-black{color:var(--sd-color-black) !important;border-color:var(--sd-color-black) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white,.sd-btn-outline-white:hover,.sd-btn-outline-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-btn-white:hover,.sd-btn-white:focus{color:var(--sd-color-white-text) !important;background-color:var(--sd-color-white-highlight) !important;border-color:var(--sd-color-white-highlight) !important;border-width:1px !important;border-style:solid !important}.sd-btn-outline-white{color:var(--sd-color-white) !important;border-color:var(--sd-color-white) !important;border-width:1px !important;border-style:solid !important}.sd-stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.sd-hide-link-text{font-size:0}.sd-octicon,.sd-material-icon{display:inline-block;fill:currentColor;vertical-align:middle}.sd-avatar-xs{border-radius:50%;object-fit:cover;object-position:center;width:1rem;height:1rem}.sd-avatar-sm{border-radius:50%;object-fit:cover;object-position:center;width:3rem;height:3rem}.sd-avatar-md{border-radius:50%;object-fit:cover;object-position:center;width:5rem;height:5rem}.sd-avatar-lg{border-radius:50%;object-fit:cover;object-position:center;width:7rem;height:7rem}.sd-avatar-xl{border-radius:50%;object-fit:cover;object-position:center;width:10rem;height:10rem}.sd-avatar-inherit{border-radius:50%;object-fit:cover;object-position:center;width:inherit;height:inherit}.sd-avatar-initial{border-radius:50%;object-fit:cover;object-position:center;width:initial;height:initial}.sd-card{background-clip:border-box;background-color:var(--sd-color-card-background);border:1px solid var(--sd-color-card-border);border-radius:.25rem;color:var(--sd-color-card-text);display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;position:relative;word-wrap:break-word}.sd-card>hr{margin-left:0;margin-right:0}.sd-card-hover:hover{border-color:var(--sd-color-card-border-hover);transform:scale(1.01)}.sd-card-body{-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem 1rem}.sd-card-title{margin-bottom:.5rem}.sd-card-subtitle{margin-top:-0.25rem;margin-bottom:0}.sd-card-text:last-child{margin-bottom:0}.sd-card-link:hover{text-decoration:none}.sd-card-link+.card-link{margin-left:1rem}.sd-card-header{padding:.5rem 1rem;margin-bottom:0;background-color:var(--sd-color-card-header);border-bottom:1px solid var(--sd-color-card-border)}.sd-card-header:first-child{border-radius:calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0}.sd-card-footer{padding:.5rem 1rem;background-color:var(--sd-color-card-footer);border-top:1px solid var(--sd-color-card-border)}.sd-card-footer:last-child{border-radius:0 0 calc(0.25rem - 1px) calc(0.25rem - 1px)}.sd-card-header-tabs{margin-right:-0.5rem;margin-bottom:-0.5rem;margin-left:-0.5rem;border-bottom:0}.sd-card-header-pills{margin-right:-0.5rem;margin-left:-0.5rem}.sd-card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom,.sd-card-img-top{width:100%}.sd-card-img,.sd-card-img-top{border-top-left-radius:calc(0.25rem - 1px);border-top-right-radius:calc(0.25rem - 1px)}.sd-card-img,.sd-card-img-bottom{border-bottom-left-radius:calc(0.25rem - 1px);border-bottom-right-radius:calc(0.25rem - 1px)}.sd-cards-carousel{width:100%;display:flex;flex-wrap:nowrap;-ms-flex-direction:row;flex-direction:row;overflow-x:hidden;scroll-snap-type:x mandatory}.sd-cards-carousel.sd-show-scrollbar{overflow-x:auto}.sd-cards-carousel:hover,.sd-cards-carousel:focus{overflow-x:auto}.sd-cards-carousel>.sd-card{flex-shrink:0;scroll-snap-align:start}.sd-cards-carousel>.sd-card:not(:last-child){margin-right:3px}.sd-card-cols-1>.sd-card{width:90%}.sd-card-cols-2>.sd-card{width:45%}.sd-card-cols-3>.sd-card{width:30%}.sd-card-cols-4>.sd-card{width:22.5%}.sd-card-cols-5>.sd-card{width:18%}.sd-card-cols-6>.sd-card{width:15%}.sd-card-cols-7>.sd-card{width:12.8571428571%}.sd-card-cols-8>.sd-card{width:11.25%}.sd-card-cols-9>.sd-card{width:10%}.sd-card-cols-10>.sd-card{width:9%}.sd-card-cols-11>.sd-card{width:8.1818181818%}.sd-card-cols-12>.sd-card{width:7.5%}.sd-container,.sd-container-fluid,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container-xl{margin-left:auto;margin-right:auto;padding-left:var(--sd-gutter-x, 0.75rem);padding-right:var(--sd-gutter-x, 0.75rem);width:100%}@media(min-width: 576px){.sd-container-sm,.sd-container{max-width:540px}}@media(min-width: 768px){.sd-container-md,.sd-container-sm,.sd-container{max-width:720px}}@media(min-width: 992px){.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:960px}}@media(min-width: 1200px){.sd-container-xl,.sd-container-lg,.sd-container-md,.sd-container-sm,.sd-container{max-width:1140px}}.sd-row{--sd-gutter-x: 1.5rem;--sd-gutter-y: 0;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-top:calc(var(--sd-gutter-y) * -1);margin-right:calc(var(--sd-gutter-x) * -0.5);margin-left:calc(var(--sd-gutter-x) * -0.5)}.sd-row>*{box-sizing:border-box;flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--sd-gutter-x) * 0.5);padding-left:calc(var(--sd-gutter-x) * 0.5);margin-top:var(--sd-gutter-y)}.sd-col{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-auto>*{flex:0 0 auto;width:auto}.sd-row-cols-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}@media(min-width: 576px){.sd-col-sm{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-sm-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-sm-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-sm-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-sm-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-sm-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-sm-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-sm-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-sm-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-sm-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-sm-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-sm-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-sm-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-sm-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 768px){.sd-col-md{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-md-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-md-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-md-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-md-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-md-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-md-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-md-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-md-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-md-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-md-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-md-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-md-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-md-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 992px){.sd-col-lg{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-lg-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-lg-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-lg-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-lg-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-lg-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-lg-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-lg-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-lg-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-lg-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-lg-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-lg-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-lg-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-lg-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}@media(min-width: 1200px){.sd-col-xl{flex:1 0 0%;-ms-flex:1 0 0%}.sd-row-cols-xl-auto{flex:1 0 auto;-ms-flex:1 0 auto;width:100%}.sd-row-cols-xl-1>*{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-row-cols-xl-2>*{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-row-cols-xl-3>*{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-row-cols-xl-4>*{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-row-cols-xl-5>*{flex:0 0 auto;-ms-flex:0 0 auto;width:20%}.sd-row-cols-xl-6>*{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-row-cols-xl-7>*{flex:0 0 auto;-ms-flex:0 0 auto;width:14.2857142857%}.sd-row-cols-xl-8>*{flex:0 0 auto;-ms-flex:0 0 auto;width:12.5%}.sd-row-cols-xl-9>*{flex:0 0 auto;-ms-flex:0 0 auto;width:11.1111111111%}.sd-row-cols-xl-10>*{flex:0 0 auto;-ms-flex:0 0 auto;width:10%}.sd-row-cols-xl-11>*{flex:0 0 auto;-ms-flex:0 0 auto;width:9.0909090909%}.sd-row-cols-xl-12>*{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}}.sd-col-auto{flex:0 0 auto;-ms-flex:0 0 auto;width:auto}.sd-col-1{flex:0 0 auto;-ms-flex:0 0 auto;width:8.3333333333%}.sd-col-2{flex:0 0 auto;-ms-flex:0 0 auto;width:16.6666666667%}.sd-col-3{flex:0 0 auto;-ms-flex:0 0 auto;width:25%}.sd-col-4{flex:0 0 auto;-ms-flex:0 0 auto;width:33.3333333333%}.sd-col-5{flex:0 0 auto;-ms-flex:0 0 auto;width:41.6666666667%}.sd-col-6{flex:0 0 auto;-ms-flex:0 0 auto;width:50%}.sd-col-7{flex:0 0 auto;-ms-flex:0 0 auto;width:58.3333333333%}.sd-col-8{flex:0 0 auto;-ms-flex:0 0 auto;width:66.6666666667%}.sd-col-9{flex:0 0 auto;-ms-flex:0 0 auto;width:75%}.sd-col-10{flex:0 0 auto;-ms-flex:0 0 auto;width:83.3333333333%}.sd-col-11{flex:0 0 auto;-ms-flex:0 0 auto;width:91.6666666667%}.sd-col-12{flex:0 0 auto;-ms-flex:0 0 auto;width:100%}.sd-g-0,.sd-gy-0{--sd-gutter-y: 0}.sd-g-0,.sd-gx-0{--sd-gutter-x: 0}.sd-g-1,.sd-gy-1{--sd-gutter-y: 0.25rem}.sd-g-1,.sd-gx-1{--sd-gutter-x: 0.25rem}.sd-g-2,.sd-gy-2{--sd-gutter-y: 0.5rem}.sd-g-2,.sd-gx-2{--sd-gutter-x: 0.5rem}.sd-g-3,.sd-gy-3{--sd-gutter-y: 1rem}.sd-g-3,.sd-gx-3{--sd-gutter-x: 1rem}.sd-g-4,.sd-gy-4{--sd-gutter-y: 1.5rem}.sd-g-4,.sd-gx-4{--sd-gutter-x: 1.5rem}.sd-g-5,.sd-gy-5{--sd-gutter-y: 3rem}.sd-g-5,.sd-gx-5{--sd-gutter-x: 3rem}@media(min-width: 576px){.sd-col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-sm-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-sm-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-sm-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-sm-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-sm-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-sm-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-sm-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-sm-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-sm-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-sm-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-sm-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-sm-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-sm-0,.sd-gy-sm-0{--sd-gutter-y: 0}.sd-g-sm-0,.sd-gx-sm-0{--sd-gutter-x: 0}.sd-g-sm-1,.sd-gy-sm-1{--sd-gutter-y: 0.25rem}.sd-g-sm-1,.sd-gx-sm-1{--sd-gutter-x: 0.25rem}.sd-g-sm-2,.sd-gy-sm-2{--sd-gutter-y: 0.5rem}.sd-g-sm-2,.sd-gx-sm-2{--sd-gutter-x: 0.5rem}.sd-g-sm-3,.sd-gy-sm-3{--sd-gutter-y: 1rem}.sd-g-sm-3,.sd-gx-sm-3{--sd-gutter-x: 1rem}.sd-g-sm-4,.sd-gy-sm-4{--sd-gutter-y: 1.5rem}.sd-g-sm-4,.sd-gx-sm-4{--sd-gutter-x: 1.5rem}.sd-g-sm-5,.sd-gy-sm-5{--sd-gutter-y: 3rem}.sd-g-sm-5,.sd-gx-sm-5{--sd-gutter-x: 3rem}}@media(min-width: 768px){.sd-col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-md-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-md-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-md-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-md-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-md-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-md-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-md-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-md-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-md-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-md-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-md-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-md-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-md-0,.sd-gy-md-0{--sd-gutter-y: 0}.sd-g-md-0,.sd-gx-md-0{--sd-gutter-x: 0}.sd-g-md-1,.sd-gy-md-1{--sd-gutter-y: 0.25rem}.sd-g-md-1,.sd-gx-md-1{--sd-gutter-x: 0.25rem}.sd-g-md-2,.sd-gy-md-2{--sd-gutter-y: 0.5rem}.sd-g-md-2,.sd-gx-md-2{--sd-gutter-x: 0.5rem}.sd-g-md-3,.sd-gy-md-3{--sd-gutter-y: 1rem}.sd-g-md-3,.sd-gx-md-3{--sd-gutter-x: 1rem}.sd-g-md-4,.sd-gy-md-4{--sd-gutter-y: 1.5rem}.sd-g-md-4,.sd-gx-md-4{--sd-gutter-x: 1.5rem}.sd-g-md-5,.sd-gy-md-5{--sd-gutter-y: 3rem}.sd-g-md-5,.sd-gx-md-5{--sd-gutter-x: 3rem}}@media(min-width: 992px){.sd-col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-lg-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-lg-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-lg-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-lg-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-lg-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-lg-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-lg-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-lg-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-lg-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-lg-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-lg-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-lg-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-lg-0,.sd-gy-lg-0{--sd-gutter-y: 0}.sd-g-lg-0,.sd-gx-lg-0{--sd-gutter-x: 0}.sd-g-lg-1,.sd-gy-lg-1{--sd-gutter-y: 0.25rem}.sd-g-lg-1,.sd-gx-lg-1{--sd-gutter-x: 0.25rem}.sd-g-lg-2,.sd-gy-lg-2{--sd-gutter-y: 0.5rem}.sd-g-lg-2,.sd-gx-lg-2{--sd-gutter-x: 0.5rem}.sd-g-lg-3,.sd-gy-lg-3{--sd-gutter-y: 1rem}.sd-g-lg-3,.sd-gx-lg-3{--sd-gutter-x: 1rem}.sd-g-lg-4,.sd-gy-lg-4{--sd-gutter-y: 1.5rem}.sd-g-lg-4,.sd-gx-lg-4{--sd-gutter-x: 1.5rem}.sd-g-lg-5,.sd-gy-lg-5{--sd-gutter-y: 3rem}.sd-g-lg-5,.sd-gx-lg-5{--sd-gutter-x: 3rem}}@media(min-width: 1200px){.sd-col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.sd-col-xl-1{-ms-flex:0 0 auto;flex:0 0 auto;width:8.3333333333%}.sd-col-xl-2{-ms-flex:0 0 auto;flex:0 0 auto;width:16.6666666667%}.sd-col-xl-3{-ms-flex:0 0 auto;flex:0 0 auto;width:25%}.sd-col-xl-4{-ms-flex:0 0 auto;flex:0 0 auto;width:33.3333333333%}.sd-col-xl-5{-ms-flex:0 0 auto;flex:0 0 auto;width:41.6666666667%}.sd-col-xl-6{-ms-flex:0 0 auto;flex:0 0 auto;width:50%}.sd-col-xl-7{-ms-flex:0 0 auto;flex:0 0 auto;width:58.3333333333%}.sd-col-xl-8{-ms-flex:0 0 auto;flex:0 0 auto;width:66.6666666667%}.sd-col-xl-9{-ms-flex:0 0 auto;flex:0 0 auto;width:75%}.sd-col-xl-10{-ms-flex:0 0 auto;flex:0 0 auto;width:83.3333333333%}.sd-col-xl-11{-ms-flex:0 0 auto;flex:0 0 auto;width:91.6666666667%}.sd-col-xl-12{-ms-flex:0 0 auto;flex:0 0 auto;width:100%}.sd-g-xl-0,.sd-gy-xl-0{--sd-gutter-y: 0}.sd-g-xl-0,.sd-gx-xl-0{--sd-gutter-x: 0}.sd-g-xl-1,.sd-gy-xl-1{--sd-gutter-y: 0.25rem}.sd-g-xl-1,.sd-gx-xl-1{--sd-gutter-x: 0.25rem}.sd-g-xl-2,.sd-gy-xl-2{--sd-gutter-y: 0.5rem}.sd-g-xl-2,.sd-gx-xl-2{--sd-gutter-x: 0.5rem}.sd-g-xl-3,.sd-gy-xl-3{--sd-gutter-y: 1rem}.sd-g-xl-3,.sd-gx-xl-3{--sd-gutter-x: 1rem}.sd-g-xl-4,.sd-gy-xl-4{--sd-gutter-y: 1.5rem}.sd-g-xl-4,.sd-gx-xl-4{--sd-gutter-x: 1.5rem}.sd-g-xl-5,.sd-gy-xl-5{--sd-gutter-y: 3rem}.sd-g-xl-5,.sd-gx-xl-5{--sd-gutter-x: 3rem}}.sd-flex-row-reverse{flex-direction:row-reverse !important}details.sd-dropdown{position:relative}details.sd-dropdown .sd-summary-title{font-weight:700;padding-right:3em !important;-moz-user-select:none;-ms-user-select:none;-webkit-user-select:none;user-select:none}details.sd-dropdown:hover{cursor:pointer}details.sd-dropdown .sd-summary-content{cursor:default}details.sd-dropdown summary{list-style:none;padding:1em}details.sd-dropdown summary .sd-octicon.no-title{vertical-align:middle}details.sd-dropdown[open] summary .sd-octicon.no-title{visibility:hidden}details.sd-dropdown summary::-webkit-details-marker{display:none}details.sd-dropdown summary:focus{outline:none}details.sd-dropdown .sd-summary-icon{margin-right:.5em}details.sd-dropdown .sd-summary-icon svg{opacity:.8}details.sd-dropdown summary:hover .sd-summary-up svg,details.sd-dropdown summary:hover .sd-summary-down svg{opacity:1;transform:scale(1.1)}details.sd-dropdown .sd-summary-up svg,details.sd-dropdown .sd-summary-down svg{display:block;opacity:.6}details.sd-dropdown .sd-summary-up,details.sd-dropdown .sd-summary-down{pointer-events:none;position:absolute;right:1em;top:1em}details.sd-dropdown[open]>.sd-summary-title .sd-summary-down{visibility:hidden}details.sd-dropdown:not([open])>.sd-summary-title .sd-summary-up{visibility:hidden}details.sd-dropdown:not([open]).sd-card{border:none}details.sd-dropdown:not([open])>.sd-card-header{border:1px solid var(--sd-color-card-border);border-radius:.25rem}details.sd-dropdown.sd-fade-in[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out;animation:sd-fade-in .5s ease-in-out}details.sd-dropdown.sd-fade-in-slide-down[open] summary~*{-moz-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;-webkit-animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out;animation:sd-fade-in .5s ease-in-out,sd-slide-down .5s ease-in-out}.sd-col>.sd-dropdown{width:100%}.sd-summary-content>.sd-tab-set:first-child{margin-top:0}@keyframes sd-fade-in{0%{opacity:0}100%{opacity:1}}@keyframes sd-slide-down{0%{transform:translate(0, -10px)}100%{transform:translate(0, 0)}}.sd-tab-set{border-radius:.125rem;display:flex;flex-wrap:wrap;margin:1em 0;position:relative}.sd-tab-set>input{opacity:0;position:absolute}.sd-tab-set>input:checked+label{border-color:var(--sd-color-tabs-underline-active);color:var(--sd-color-tabs-label-active)}.sd-tab-set>input:checked+label+.sd-tab-content{display:block}.sd-tab-set>input:not(:checked)+label:hover{color:var(--sd-color-tabs-label-hover);border-color:var(--sd-color-tabs-underline-hover)}.sd-tab-set>input:focus+label{outline-style:auto}.sd-tab-set>input:not(.focus-visible)+label{outline:none;-webkit-tap-highlight-color:transparent}.sd-tab-set>label{border-bottom:.125rem solid transparent;margin-bottom:0;color:var(--sd-color-tabs-label-inactive);border-color:var(--sd-color-tabs-underline-inactive);cursor:pointer;font-size:var(--sd-fontsize-tabs-label);font-weight:700;padding:1em 1.25em .5em;transition:color 250ms;width:auto;z-index:1}html .sd-tab-set>label:hover{color:var(--sd-color-tabs-label-active)}.sd-col>.sd-tab-set{width:100%}.sd-tab-content{box-shadow:0 -0.0625rem var(--sd-color-tabs-overline),0 .0625rem var(--sd-color-tabs-underline);display:none;order:99;padding-bottom:.75rem;padding-top:.75rem;width:100%}.sd-tab-content>:first-child{margin-top:0 !important}.sd-tab-content>:last-child{margin-bottom:0 !important}.sd-tab-content>.sd-tab-set{margin:0}.sd-sphinx-override,.sd-sphinx-override *{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}.sd-sphinx-override p{margin-top:0}:root{--sd-color-primary: #0071bc;--sd-color-secondary: #6c757d;--sd-color-success: #28a745;--sd-color-info: #17a2b8;--sd-color-warning: #f0b37e;--sd-color-danger: #dc3545;--sd-color-light: #f8f9fa;--sd-color-muted: #6c757d;--sd-color-dark: #212529;--sd-color-black: black;--sd-color-white: white;--sd-color-primary-highlight: #0060a0;--sd-color-secondary-highlight: #5c636a;--sd-color-success-highlight: #228e3b;--sd-color-info-highlight: #148a9c;--sd-color-warning-highlight: #cc986b;--sd-color-danger-highlight: #bb2d3b;--sd-color-light-highlight: #d3d4d5;--sd-color-muted-highlight: #5c636a;--sd-color-dark-highlight: #1c1f23;--sd-color-black-highlight: black;--sd-color-white-highlight: #d9d9d9;--sd-color-primary-text: #fff;--sd-color-secondary-text: #fff;--sd-color-success-text: #fff;--sd-color-info-text: #fff;--sd-color-warning-text: #212529;--sd-color-danger-text: #fff;--sd-color-light-text: #212529;--sd-color-muted-text: #fff;--sd-color-dark-text: #fff;--sd-color-black-text: #fff;--sd-color-white-text: #212529;--sd-color-shadow: rgba(0, 0, 0, 0.15);--sd-color-card-border: rgba(0, 0, 0, 0.125);--sd-color-card-border-hover: hsla(231, 99%, 66%, 1);--sd-color-card-background: transparent;--sd-color-card-text: inherit;--sd-color-card-header: transparent;--sd-color-card-footer: transparent;--sd-color-tabs-label-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-hover: hsla(231, 99%, 66%, 1);--sd-color-tabs-label-inactive: hsl(0, 0%, 66%);--sd-color-tabs-underline-active: hsla(231, 99%, 66%, 1);--sd-color-tabs-underline-hover: rgba(178, 206, 245, 0.62);--sd-color-tabs-underline-inactive: transparent;--sd-color-tabs-overline: rgb(222, 222, 222);--sd-color-tabs-underline: rgb(222, 222, 222);--sd-fontsize-tabs-label: 1rem} diff --git a/main/_static/design-tabs.js b/main/_static/design-tabs.js new file mode 100644 index 00000000..36b38cf0 --- /dev/null +++ b/main/_static/design-tabs.js @@ -0,0 +1,27 @@ +var sd_labels_by_text = {}; + +function ready() { + const li = document.getElementsByClassName("sd-tab-label"); + for (const label of li) { + syncId = label.getAttribute("data-sync-id"); + if (syncId) { + label.onclick = onLabelClick; + if (!sd_labels_by_text[syncId]) { + sd_labels_by_text[syncId] = []; + } + sd_labels_by_text[syncId].push(label); + } + } +} + +function onLabelClick() { + // Activate other inputs with the same sync id. + syncId = this.getAttribute("data-sync-id"); + for (label of sd_labels_by_text[syncId]) { + if (label === this) continue; + label.previousElementSibling.checked = true; + } + window.localStorage.setItem("sphinx-design-last-tab", syncId); +} + +document.addEventListener("DOMContentLoaded", ready, false); diff --git a/main/_static/doctools.js b/main/_static/doctools.js new file mode 100644 index 00000000..d06a71d7 --- /dev/null +++ b/main/_static/doctools.js @@ -0,0 +1,156 @@ +/* + * doctools.js + * ~~~~~~~~~~~ + * + * Base JavaScript utilities for all Sphinx HTML documentation. + * + * :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ +"use strict"; + +const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ + "TEXTAREA", + "INPUT", + "SELECT", + "BUTTON", +]); + +const _ready = (callback) => { + if (document.readyState !== "loading") { + callback(); + } else { + document.addEventListener("DOMContentLoaded", callback); + } +}; + +/** + * Small JavaScript module for the documentation. + */ +const Documentation = { + init: () => { + Documentation.initDomainIndexTable(); + Documentation.initOnKeyListeners(); + }, + + /** + * i18n support + */ + TRANSLATIONS: {}, + PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), + LOCALE: "unknown", + + // gettext and ngettext don't access this so that the functions + // can safely bound to a different name (_ = Documentation.gettext) + gettext: (string) => { + const translated = Documentation.TRANSLATIONS[string]; + switch (typeof translated) { + case "undefined": + return string; // no translation + case "string": + return translated; // translation exists + default: + return translated[0]; // (singular, plural) translation tuple exists + } + }, + + ngettext: (singular, plural, n) => { + const translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated !== "undefined") + return translated[Documentation.PLURAL_EXPR(n)]; + return n === 1 ? singular : plural; + }, + + addTranslations: (catalog) => { + Object.assign(Documentation.TRANSLATIONS, catalog.messages); + Documentation.PLURAL_EXPR = new Function( + "n", + `return (${catalog.plural_expr})` + ); + Documentation.LOCALE = catalog.locale; + }, + + /** + * helper function to focus on search bar + */ + focusSearchBar: () => { + document.querySelectorAll("input[name=q]")[0]?.focus(); + }, + + /** + * Initialise the domain index toggle buttons + */ + initDomainIndexTable: () => { + const toggler = (el) => { + const idNumber = el.id.substr(7); + const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); + if (el.src.substr(-9) === "minus.png") { + el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; + toggledRows.forEach((el) => (el.style.display = "none")); + } else { + el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; + toggledRows.forEach((el) => (el.style.display = "")); + } + }; + + const togglerElements = document.querySelectorAll("img.toggler"); + togglerElements.forEach((el) => + el.addEventListener("click", (event) => toggler(event.currentTarget)) + ); + togglerElements.forEach((el) => (el.style.display = "")); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); + }, + + initOnKeyListeners: () => { + // only install a listener if it is really needed + if ( + !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && + !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + ) + return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.altKey || event.ctrlKey || event.metaKey) return; + + if (!event.shiftKey) { + switch (event.key) { + case "ArrowLeft": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const prevLink = document.querySelector('link[rel="prev"]'); + if (prevLink && prevLink.href) { + window.location.href = prevLink.href; + event.preventDefault(); + } + break; + case "ArrowRight": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const nextLink = document.querySelector('link[rel="next"]'); + if (nextLink && nextLink.href) { + window.location.href = nextLink.href; + event.preventDefault(); + } + break; + } + } + + // some keyboard layouts may need Shift to get / + switch (event.key) { + case "/": + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; + Documentation.focusSearchBar(); + event.preventDefault(); + } + }); + }, +}; + +// quick alias for translations +const _ = Documentation.gettext; + +_ready(Documentation.init); diff --git a/main/_static/documentation_options.js b/main/_static/documentation_options.js new file mode 100644 index 00000000..1d0f3bee --- /dev/null +++ b/main/_static/documentation_options.js @@ -0,0 +1,13 @@ +const DOCUMENTATION_OPTIONS = { + VERSION: '0.6.5', + LANGUAGE: 'en', + COLLAPSE_INDEX: false, + BUILDER: 'html', + FILE_SUFFIX: '.html', + LINK_SUFFIX: '.html', + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '.txt', + NAVIGATION_WITH_KEYS: false, + SHOW_SEARCH_SUMMARY: true, + ENABLE_SEARCH_SHORTCUTS: true, +}; \ No newline at end of file diff --git a/main/_static/file.png b/main/_static/file.png new file mode 100644 index 00000000..a858a410 Binary files /dev/null and b/main/_static/file.png differ diff --git a/main/_static/graphviz.css b/main/_static/graphviz.css new file mode 100644 index 00000000..8d81c02e --- /dev/null +++ b/main/_static/graphviz.css @@ -0,0 +1,19 @@ +/* + * graphviz.css + * ~~~~~~~~~~~~ + * + * Sphinx stylesheet -- graphviz extension. + * + * :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +img.graphviz { + border: 0; + max-width: 100%; +} + +object.graphviz { + max-width: 100%; +} diff --git a/main/_static/language_data.js b/main/_static/language_data.js new file mode 100644 index 00000000..250f5665 --- /dev/null +++ b/main/_static/language_data.js @@ -0,0 +1,199 @@ +/* + * language_data.js + * ~~~~~~~~~~~~~~~~ + * + * This script contains the language-specific data used by searchtools.js, + * namely the list of stopwords, stemmer, scorer and splitter. + * + * :copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"]; + + +/* Non-minified version is copied as a separate JS file, is available */ + +/** + * Porter Stemmer + */ +var Stemmer = function() { + + var step2list = { + ational: 'ate', + tional: 'tion', + enci: 'ence', + anci: 'ance', + izer: 'ize', + bli: 'ble', + alli: 'al', + entli: 'ent', + eli: 'e', + ousli: 'ous', + ization: 'ize', + ation: 'ate', + ator: 'ate', + alism: 'al', + iveness: 'ive', + fulness: 'ful', + ousness: 'ous', + aliti: 'al', + iviti: 'ive', + biliti: 'ble', + logi: 'log' + }; + + var step3list = { + icate: 'ic', + ative: '', + alize: 'al', + iciti: 'ic', + ical: 'ic', + ful: '', + ness: '' + }; + + var c = "[^aeiou]"; // consonant + var v = "[aeiouy]"; // vowel + var C = c + "[^aeiouy]*"; // consonant sequence + var V = v + "[aeiou]*"; // vowel sequence + + var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0 + var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 + var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 + var s_v = "^(" + C + ")?" + v; // vowel in stem + + this.stemWord = function (w) { + var stem; + var suffix; + var firstch; + var origword = w; + + if (w.length < 3) + return w; + + var re; + var re2; + var re3; + var re4; + + firstch = w.substr(0,1); + if (firstch == "y") + w = firstch.toUpperCase() + w.substr(1); + + // Step 1a + re = /^(.+?)(ss|i)es$/; + re2 = /^(.+?)([^s])s$/; + + if (re.test(w)) + w = w.replace(re,"$1$2"); + else if (re2.test(w)) + w = w.replace(re2,"$1$2"); + + // Step 1b + re = /^(.+?)eed$/; + re2 = /^(.+?)(ed|ing)$/; + if (re.test(w)) { + var fp = re.exec(w); + re = new RegExp(mgr0); + if (re.test(fp[1])) { + re = /.$/; + w = w.replace(re,""); + } + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = new RegExp(s_v); + if (re2.test(stem)) { + w = stem; + re2 = /(at|bl|iz)$/; + re3 = new RegExp("([^aeiouylsz])\\1$"); + re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re2.test(w)) + w = w + "e"; + else if (re3.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + else if (re4.test(w)) + w = w + "e"; + } + } + + // Step 1c + re = /^(.+?)y$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(s_v); + if (re.test(stem)) + w = stem + "i"; + } + + // Step 2 + re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step2list[suffix]; + } + + // Step 3 + re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step3list[suffix]; + } + + // Step 4 + re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + re2 = /^(.+?)(s|t)(ion)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + if (re.test(stem)) + w = stem; + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = new RegExp(mgr1); + if (re2.test(stem)) + w = stem; + } + + // Step 5 + re = /^(.+?)e$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + re2 = new RegExp(meq1); + re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) + w = stem; + } + re = /ll$/; + re2 = new RegExp(mgr1); + if (re.test(w) && re2.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + + // and turn initial Y back to y + if (firstch == "y") + w = firstch.toLowerCase() + w.substr(1); + return w; + } +} + diff --git a/main/_static/minus.png b/main/_static/minus.png new file mode 100644 index 00000000..d96755fd Binary files /dev/null and b/main/_static/minus.png differ diff --git a/main/_static/plot_directive.css b/main/_static/plot_directive.css new file mode 100644 index 00000000..d45593c9 --- /dev/null +++ b/main/_static/plot_directive.css @@ -0,0 +1,16 @@ +/* + * plot_directive.css + * ~~~~~~~~~~~~ + * + * Stylesheet controlling images created using the `plot` directive within + * Sphinx. + * + * :copyright: Copyright 2020-* by the Matplotlib development team. + * :license: Matplotlib, see LICENSE for details. + * + */ + +img.plot-directive { + border: 0; + max-width: 100%; +} diff --git a/main/_static/plus.png b/main/_static/plus.png new file mode 100644 index 00000000..7107cec9 Binary files /dev/null and b/main/_static/plus.png differ diff --git a/main/_static/pygments.css b/main/_static/pygments.css new file mode 100644 index 00000000..997797f2 --- /dev/null +++ b/main/_static/pygments.css @@ -0,0 +1,152 @@ +html[data-theme="light"] .highlight pre { line-height: 125%; } +html[data-theme="light"] .highlight td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="light"] .highlight .hll { background-color: #7971292e } +html[data-theme="light"] .highlight { background: #fefefe; color: #545454 } +html[data-theme="light"] .highlight .c { color: #797129 } /* Comment */ +html[data-theme="light"] .highlight .err { color: #d91e18 } /* Error */ +html[data-theme="light"] .highlight .k { color: #7928a1 } /* Keyword */ +html[data-theme="light"] .highlight .l { color: #797129 } /* Literal */ +html[data-theme="light"] .highlight .n { color: #545454 } /* Name */ +html[data-theme="light"] .highlight .o { color: #008000 } /* Operator */ +html[data-theme="light"] .highlight .p { color: #545454 } /* Punctuation */ +html[data-theme="light"] .highlight .ch { color: #797129 } /* Comment.Hashbang */ +html[data-theme="light"] .highlight .cm { color: #797129 } /* Comment.Multiline */ +html[data-theme="light"] .highlight .cp { color: #797129 } /* Comment.Preproc */ +html[data-theme="light"] .highlight .cpf { color: #797129 } /* Comment.PreprocFile */ +html[data-theme="light"] .highlight .c1 { color: #797129 } /* Comment.Single */ +html[data-theme="light"] .highlight .cs { color: #797129 } /* Comment.Special */ +html[data-theme="light"] .highlight .gd { color: #007faa } /* Generic.Deleted */ +html[data-theme="light"] .highlight .ge { font-style: italic } /* Generic.Emph */ +html[data-theme="light"] .highlight .gh { color: #007faa } /* Generic.Heading */ +html[data-theme="light"] .highlight .gs { font-weight: bold } /* Generic.Strong */ +html[data-theme="light"] .highlight .gu { color: #007faa } /* Generic.Subheading */ +html[data-theme="light"] .highlight .kc { color: #7928a1 } /* Keyword.Constant */ +html[data-theme="light"] .highlight .kd { color: #7928a1 } /* Keyword.Declaration */ +html[data-theme="light"] .highlight .kn { color: #7928a1 } /* Keyword.Namespace */ +html[data-theme="light"] .highlight .kp { color: #7928a1 } /* Keyword.Pseudo */ +html[data-theme="light"] .highlight .kr { color: #7928a1 } /* Keyword.Reserved */ +html[data-theme="light"] .highlight .kt { color: #797129 } /* Keyword.Type */ +html[data-theme="light"] .highlight .ld { color: #797129 } /* Literal.Date */ +html[data-theme="light"] .highlight .m { color: #797129 } /* Literal.Number */ +html[data-theme="light"] .highlight .s { color: #008000 } /* Literal.String */ +html[data-theme="light"] .highlight .na { color: #797129 } /* Name.Attribute */ +html[data-theme="light"] .highlight .nb { color: #797129 } /* Name.Builtin */ +html[data-theme="light"] .highlight .nc { color: #007faa } /* Name.Class */ +html[data-theme="light"] .highlight .no { color: #007faa } /* Name.Constant */ +html[data-theme="light"] .highlight .nd { color: #797129 } /* Name.Decorator */ +html[data-theme="light"] .highlight .ni { color: #008000 } /* Name.Entity */ +html[data-theme="light"] .highlight .ne { color: #7928a1 } /* Name.Exception */ +html[data-theme="light"] .highlight .nf { color: #007faa } /* Name.Function */ +html[data-theme="light"] .highlight .nl { color: #797129 } /* Name.Label */ +html[data-theme="light"] .highlight .nn { color: #545454 } /* Name.Namespace */ +html[data-theme="light"] .highlight .nx { color: #545454 } /* Name.Other */ +html[data-theme="light"] .highlight .py { color: #007faa } /* Name.Property */ +html[data-theme="light"] .highlight .nt { color: #007faa } /* Name.Tag */ +html[data-theme="light"] .highlight .nv { color: #d91e18 } /* Name.Variable */ +html[data-theme="light"] .highlight .ow { color: #7928a1 } /* Operator.Word */ +html[data-theme="light"] .highlight .pm { color: #545454 } /* Punctuation.Marker */ +html[data-theme="light"] .highlight .w { color: #545454 } /* Text.Whitespace */ +html[data-theme="light"] .highlight .mb { color: #797129 } /* Literal.Number.Bin */ +html[data-theme="light"] .highlight .mf { color: #797129 } /* Literal.Number.Float */ +html[data-theme="light"] .highlight .mh { color: #797129 } /* Literal.Number.Hex */ +html[data-theme="light"] .highlight .mi { color: #797129 } /* Literal.Number.Integer */ +html[data-theme="light"] .highlight .mo { color: #797129 } /* Literal.Number.Oct */ +html[data-theme="light"] .highlight .sa { color: #008000 } /* Literal.String.Affix */ +html[data-theme="light"] .highlight .sb { color: #008000 } /* Literal.String.Backtick */ +html[data-theme="light"] .highlight .sc { color: #008000 } /* Literal.String.Char */ +html[data-theme="light"] .highlight .dl { color: #008000 } /* Literal.String.Delimiter */ +html[data-theme="light"] .highlight .sd { color: #008000 } /* Literal.String.Doc */ +html[data-theme="light"] .highlight .s2 { color: #008000 } /* Literal.String.Double */ +html[data-theme="light"] .highlight .se { color: #008000 } /* Literal.String.Escape */ +html[data-theme="light"] .highlight .sh { color: #008000 } /* Literal.String.Heredoc */ +html[data-theme="light"] .highlight .si { color: #008000 } /* Literal.String.Interpol */ +html[data-theme="light"] .highlight .sx { color: #008000 } /* Literal.String.Other */ +html[data-theme="light"] .highlight .sr { color: #d91e18 } /* Literal.String.Regex */ +html[data-theme="light"] .highlight .s1 { color: #008000 } /* Literal.String.Single */ +html[data-theme="light"] .highlight .ss { color: #007faa } /* Literal.String.Symbol */ +html[data-theme="light"] .highlight .bp { color: #797129 } /* Name.Builtin.Pseudo */ +html[data-theme="light"] .highlight .fm { color: #007faa } /* Name.Function.Magic */ +html[data-theme="light"] .highlight .vc { color: #d91e18 } /* Name.Variable.Class */ +html[data-theme="light"] .highlight .vg { color: #d91e18 } /* Name.Variable.Global */ +html[data-theme="light"] .highlight .vi { color: #d91e18 } /* Name.Variable.Instance */ +html[data-theme="light"] .highlight .vm { color: #797129 } /* Name.Variable.Magic */ +html[data-theme="light"] .highlight .il { color: #797129 } /* Literal.Number.Integer.Long */ +html[data-theme="dark"] .highlight pre { line-height: 125%; } +html[data-theme="dark"] .highlight td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +html[data-theme="dark"] .highlight .hll { background-color: #ffd9002e } +html[data-theme="dark"] .highlight { background: #2b2b2b; color: #f8f8f2 } +html[data-theme="dark"] .highlight .c { color: #ffd900 } /* Comment */ +html[data-theme="dark"] .highlight .err { color: #ffa07a } /* Error */ +html[data-theme="dark"] .highlight .k { color: #dcc6e0 } /* Keyword */ +html[data-theme="dark"] .highlight .l { color: #ffd900 } /* Literal */ +html[data-theme="dark"] .highlight .n { color: #f8f8f2 } /* Name */ +html[data-theme="dark"] .highlight .o { color: #abe338 } /* Operator */ +html[data-theme="dark"] .highlight .p { color: #f8f8f2 } /* Punctuation */ +html[data-theme="dark"] .highlight .ch { color: #ffd900 } /* Comment.Hashbang */ +html[data-theme="dark"] .highlight .cm { color: #ffd900 } /* Comment.Multiline */ +html[data-theme="dark"] .highlight .cp { color: #ffd900 } /* Comment.Preproc */ +html[data-theme="dark"] .highlight .cpf { color: #ffd900 } /* Comment.PreprocFile */ +html[data-theme="dark"] .highlight .c1 { color: #ffd900 } /* Comment.Single */ +html[data-theme="dark"] .highlight .cs { color: #ffd900 } /* Comment.Special */ +html[data-theme="dark"] .highlight .gd { color: #00e0e0 } /* Generic.Deleted */ +html[data-theme="dark"] .highlight .ge { font-style: italic } /* Generic.Emph */ +html[data-theme="dark"] .highlight .gh { color: #00e0e0 } /* Generic.Heading */ +html[data-theme="dark"] .highlight .gs { font-weight: bold } /* Generic.Strong */ +html[data-theme="dark"] .highlight .gu { color: #00e0e0 } /* Generic.Subheading */ +html[data-theme="dark"] .highlight .kc { color: #dcc6e0 } /* Keyword.Constant */ +html[data-theme="dark"] .highlight .kd { color: #dcc6e0 } /* Keyword.Declaration */ +html[data-theme="dark"] .highlight .kn { color: #dcc6e0 } /* Keyword.Namespace */ +html[data-theme="dark"] .highlight .kp { color: #dcc6e0 } /* Keyword.Pseudo */ +html[data-theme="dark"] .highlight .kr { color: #dcc6e0 } /* Keyword.Reserved */ +html[data-theme="dark"] .highlight .kt { color: #ffd900 } /* Keyword.Type */ +html[data-theme="dark"] .highlight .ld { color: #ffd900 } /* Literal.Date */ +html[data-theme="dark"] .highlight .m { color: #ffd900 } /* Literal.Number */ +html[data-theme="dark"] .highlight .s { color: #abe338 } /* Literal.String */ +html[data-theme="dark"] .highlight .na { color: #ffd900 } /* Name.Attribute */ +html[data-theme="dark"] .highlight .nb { color: #ffd900 } /* Name.Builtin */ +html[data-theme="dark"] .highlight .nc { color: #00e0e0 } /* Name.Class */ +html[data-theme="dark"] .highlight .no { color: #00e0e0 } /* Name.Constant */ +html[data-theme="dark"] .highlight .nd { color: #ffd900 } /* Name.Decorator */ +html[data-theme="dark"] .highlight .ni { color: #abe338 } /* Name.Entity */ +html[data-theme="dark"] .highlight .ne { color: #dcc6e0 } /* Name.Exception */ +html[data-theme="dark"] .highlight .nf { color: #00e0e0 } /* Name.Function */ +html[data-theme="dark"] .highlight .nl { color: #ffd900 } /* Name.Label */ +html[data-theme="dark"] .highlight .nn { color: #f8f8f2 } /* Name.Namespace */ +html[data-theme="dark"] .highlight .nx { color: #f8f8f2 } /* Name.Other */ +html[data-theme="dark"] .highlight .py { color: #00e0e0 } /* Name.Property */ +html[data-theme="dark"] .highlight .nt { color: #00e0e0 } /* Name.Tag */ +html[data-theme="dark"] .highlight .nv { color: #ffa07a } /* Name.Variable */ +html[data-theme="dark"] .highlight .ow { color: #dcc6e0 } /* Operator.Word */ +html[data-theme="dark"] .highlight .pm { color: #f8f8f2 } /* Punctuation.Marker */ +html[data-theme="dark"] .highlight .w { color: #f8f8f2 } /* Text.Whitespace */ +html[data-theme="dark"] .highlight .mb { color: #ffd900 } /* Literal.Number.Bin */ +html[data-theme="dark"] .highlight .mf { color: #ffd900 } /* Literal.Number.Float */ +html[data-theme="dark"] .highlight .mh { color: #ffd900 } /* Literal.Number.Hex */ +html[data-theme="dark"] .highlight .mi { color: #ffd900 } /* Literal.Number.Integer */ +html[data-theme="dark"] .highlight .mo { color: #ffd900 } /* Literal.Number.Oct */ +html[data-theme="dark"] .highlight .sa { color: #abe338 } /* Literal.String.Affix */ +html[data-theme="dark"] .highlight .sb { color: #abe338 } /* Literal.String.Backtick */ +html[data-theme="dark"] .highlight .sc { color: #abe338 } /* Literal.String.Char */ +html[data-theme="dark"] .highlight .dl { color: #abe338 } /* Literal.String.Delimiter */ +html[data-theme="dark"] .highlight .sd { color: #abe338 } /* Literal.String.Doc */ +html[data-theme="dark"] .highlight .s2 { color: #abe338 } /* Literal.String.Double */ +html[data-theme="dark"] .highlight .se { color: #abe338 } /* Literal.String.Escape */ +html[data-theme="dark"] .highlight .sh { color: #abe338 } /* Literal.String.Heredoc */ +html[data-theme="dark"] .highlight .si { color: #abe338 } /* Literal.String.Interpol */ +html[data-theme="dark"] .highlight .sx { color: #abe338 } /* Literal.String.Other */ +html[data-theme="dark"] .highlight .sr { color: #ffa07a } /* Literal.String.Regex */ +html[data-theme="dark"] .highlight .s1 { color: #abe338 } /* Literal.String.Single */ +html[data-theme="dark"] .highlight .ss { color: #00e0e0 } /* Literal.String.Symbol */ +html[data-theme="dark"] .highlight .bp { color: #ffd900 } /* Name.Builtin.Pseudo */ +html[data-theme="dark"] .highlight .fm { color: #00e0e0 } /* Name.Function.Magic */ +html[data-theme="dark"] .highlight .vc { color: #ffa07a } /* Name.Variable.Class */ +html[data-theme="dark"] .highlight .vg { color: #ffa07a } /* Name.Variable.Global */ +html[data-theme="dark"] .highlight .vi { color: #ffa07a } /* Name.Variable.Instance */ +html[data-theme="dark"] .highlight .vm { color: #ffd900 } /* Name.Variable.Magic */ +html[data-theme="dark"] .highlight .il { color: #ffd900 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/main/_static/scanspec-logo.svg b/main/_static/scanspec-logo.svg new file mode 100644 index 00000000..9ec265a0 --- /dev/null +++ b/main/_static/scanspec-logo.svg @@ -0,0 +1,363 @@ + + diff --git a/main/_static/scripts/bootstrap.js b/main/_static/scripts/bootstrap.js new file mode 100644 index 00000000..4e209b0e --- /dev/null +++ b/main/_static/scripts/bootstrap.js @@ -0,0 +1,3 @@ +/*! For license information please see bootstrap.js.LICENSE.txt */ +(()=>{"use strict";var t={d:(e,i)=>{for(var n in i)t.o(i,n)&&!t.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:i[n]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e),r:t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},e={};t.r(e),t.d(e,{afterMain:()=>E,afterRead:()=>v,afterWrite:()=>C,applyStyles:()=>$,arrow:()=>J,auto:()=>a,basePlacements:()=>l,beforeMain:()=>y,beforeRead:()=>_,beforeWrite:()=>A,bottom:()=>s,clippingParents:()=>d,computeStyles:()=>it,createPopper:()=>Dt,createPopperBase:()=>St,createPopperLite:()=>$t,detectOverflow:()=>_t,end:()=>h,eventListeners:()=>st,flip:()=>bt,hide:()=>wt,left:()=>r,main:()=>w,modifierPhases:()=>O,offset:()=>Et,placements:()=>g,popper:()=>f,popperGenerator:()=>Lt,popperOffsets:()=>At,preventOverflow:()=>Tt,read:()=>b,reference:()=>p,right:()=>o,start:()=>c,top:()=>n,variationPlacements:()=>m,viewport:()=>u,write:()=>T});var i={};t.r(i),t.d(i,{Alert:()=>Oe,Button:()=>ke,Carousel:()=>ri,Collapse:()=>yi,Dropdown:()=>Vi,Modal:()=>xn,Offcanvas:()=>Vn,Popover:()=>fs,ScrollSpy:()=>Ts,Tab:()=>Ks,Toast:()=>lo,Tooltip:()=>hs});var n="top",s="bottom",o="right",r="left",a="auto",l=[n,s,o,r],c="start",h="end",d="clippingParents",u="viewport",f="popper",p="reference",m=l.reduce((function(t,e){return t.concat([e+"-"+c,e+"-"+h])}),[]),g=[].concat(l,[a]).reduce((function(t,e){return t.concat([e,e+"-"+c,e+"-"+h])}),[]),_="beforeRead",b="read",v="afterRead",y="beforeMain",w="main",E="afterMain",A="beforeWrite",T="write",C="afterWrite",O=[_,b,v,y,w,E,A,T,C];function x(t){return t?(t.nodeName||"").toLowerCase():null}function k(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function L(t){return t instanceof k(t).Element||t instanceof Element}function S(t){return t instanceof k(t).HTMLElement||t instanceof HTMLElement}function D(t){return"undefined"!=typeof ShadowRoot&&(t instanceof k(t).ShadowRoot||t instanceof ShadowRoot)}const $={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];S(s)&&x(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});S(n)&&x(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function I(t){return t.split("-")[0]}var N=Math.max,P=Math.min,M=Math.round;function j(){var t=navigator.userAgentData;return null!=t&&t.brands&&Array.isArray(t.brands)?t.brands.map((function(t){return t.brand+"/"+t.version})).join(" "):navigator.userAgent}function F(){return!/^((?!chrome|android).)*safari/i.test(j())}function H(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1);var n=t.getBoundingClientRect(),s=1,o=1;e&&S(t)&&(s=t.offsetWidth>0&&M(n.width)/t.offsetWidth||1,o=t.offsetHeight>0&&M(n.height)/t.offsetHeight||1);var r=(L(t)?k(t):window).visualViewport,a=!F()&&i,l=(n.left+(a&&r?r.offsetLeft:0))/s,c=(n.top+(a&&r?r.offsetTop:0))/o,h=n.width/s,d=n.height/o;return{width:h,height:d,top:c,right:l+h,bottom:c+d,left:l,x:l,y:c}}function B(t){var e=H(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function W(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&D(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function z(t){return k(t).getComputedStyle(t)}function R(t){return["table","td","th"].indexOf(x(t))>=0}function q(t){return((L(t)?t.ownerDocument:t.document)||window.document).documentElement}function V(t){return"html"===x(t)?t:t.assignedSlot||t.parentNode||(D(t)?t.host:null)||q(t)}function Y(t){return S(t)&&"fixed"!==z(t).position?t.offsetParent:null}function K(t){for(var e=k(t),i=Y(t);i&&R(i)&&"static"===z(i).position;)i=Y(i);return i&&("html"===x(i)||"body"===x(i)&&"static"===z(i).position)?e:i||function(t){var e=/firefox/i.test(j());if(/Trident/i.test(j())&&S(t)&&"fixed"===z(t).position)return null;var i=V(t);for(D(i)&&(i=i.host);S(i)&&["html","body"].indexOf(x(i))<0;){var n=z(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function Q(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function X(t,e,i){return N(t,P(e,i))}function U(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function G(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}const J={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,a=t.name,c=t.options,h=i.elements.arrow,d=i.modifiersData.popperOffsets,u=I(i.placement),f=Q(u),p=[r,o].indexOf(u)>=0?"height":"width";if(h&&d){var m=function(t,e){return U("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:G(t,l))}(c.padding,i),g=B(h),_="y"===f?n:r,b="y"===f?s:o,v=i.rects.reference[p]+i.rects.reference[f]-d[f]-i.rects.popper[p],y=d[f]-i.rects.reference[f],w=K(h),E=w?"y"===f?w.clientHeight||0:w.clientWidth||0:0,A=v/2-y/2,T=m[_],C=E-g[p]-m[b],O=E/2-g[p]/2+A,x=X(T,O,C),k=f;i.modifiersData[a]=((e={})[k]=x,e.centerOffset=x-O,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&W(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Z(t){return t.split("-")[1]}var tt={top:"auto",right:"auto",bottom:"auto",left:"auto"};function et(t){var e,i=t.popper,a=t.popperRect,l=t.placement,c=t.variation,d=t.offsets,u=t.position,f=t.gpuAcceleration,p=t.adaptive,m=t.roundOffsets,g=t.isFixed,_=d.x,b=void 0===_?0:_,v=d.y,y=void 0===v?0:v,w="function"==typeof m?m({x:b,y}):{x:b,y};b=w.x,y=w.y;var E=d.hasOwnProperty("x"),A=d.hasOwnProperty("y"),T=r,C=n,O=window;if(p){var x=K(i),L="clientHeight",S="clientWidth";x===k(i)&&"static"!==z(x=q(i)).position&&"absolute"===u&&(L="scrollHeight",S="scrollWidth"),(l===n||(l===r||l===o)&&c===h)&&(C=s,y-=(g&&x===O&&O.visualViewport?O.visualViewport.height:x[L])-a.height,y*=f?1:-1),l!==r&&(l!==n&&l!==s||c!==h)||(T=o,b-=(g&&x===O&&O.visualViewport?O.visualViewport.width:x[S])-a.width,b*=f?1:-1)}var D,$=Object.assign({position:u},p&&tt),I=!0===m?function(t,e){var i=t.x,n=t.y,s=e.devicePixelRatio||1;return{x:M(i*s)/s||0,y:M(n*s)/s||0}}({x:b,y},k(i)):{x:b,y};return b=I.x,y=I.y,f?Object.assign({},$,((D={})[C]=A?"0":"",D[T]=E?"0":"",D.transform=(O.devicePixelRatio||1)<=1?"translate("+b+"px, "+y+"px)":"translate3d("+b+"px, "+y+"px, 0)",D)):Object.assign({},$,((e={})[C]=A?y+"px":"",e[T]=E?b+"px":"",e.transform="",e))}const it={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:I(e.placement),variation:Z(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s,isFixed:"fixed"===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,et(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,et(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var nt={passive:!0};const st={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=k(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,nt)})),a&&l.addEventListener("resize",i.update,nt),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,nt)})),a&&l.removeEventListener("resize",i.update,nt)}},data:{}};var ot={left:"right",right:"left",bottom:"top",top:"bottom"};function rt(t){return t.replace(/left|right|bottom|top/g,(function(t){return ot[t]}))}var at={start:"end",end:"start"};function lt(t){return t.replace(/start|end/g,(function(t){return at[t]}))}function ct(t){var e=k(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function ht(t){return H(q(t)).left+ct(t).scrollLeft}function dt(t){var e=z(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function ut(t){return["html","body","#document"].indexOf(x(t))>=0?t.ownerDocument.body:S(t)&&dt(t)?t:ut(V(t))}function ft(t,e){var i;void 0===e&&(e=[]);var n=ut(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=k(n),r=s?[o].concat(o.visualViewport||[],dt(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(ft(V(r)))}function pt(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function mt(t,e,i){return e===u?pt(function(t,e){var i=k(t),n=q(t),s=i.visualViewport,o=n.clientWidth,r=n.clientHeight,a=0,l=0;if(s){o=s.width,r=s.height;var c=F();(c||!c&&"fixed"===e)&&(a=s.offsetLeft,l=s.offsetTop)}return{width:o,height:r,x:a+ht(t),y:l}}(t,i)):L(e)?function(t,e){var i=H(t,!1,"fixed"===e);return i.top=i.top+t.clientTop,i.left=i.left+t.clientLeft,i.bottom=i.top+t.clientHeight,i.right=i.left+t.clientWidth,i.width=t.clientWidth,i.height=t.clientHeight,i.x=i.left,i.y=i.top,i}(e,i):pt(function(t){var e,i=q(t),n=ct(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=N(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=N(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+ht(t),l=-n.scrollTop;return"rtl"===z(s||i).direction&&(a+=N(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(q(t)))}function gt(t){var e,i=t.reference,a=t.element,l=t.placement,d=l?I(l):null,u=l?Z(l):null,f=i.x+i.width/2-a.width/2,p=i.y+i.height/2-a.height/2;switch(d){case n:e={x:f,y:i.y-a.height};break;case s:e={x:f,y:i.y+i.height};break;case o:e={x:i.x+i.width,y:p};break;case r:e={x:i.x-a.width,y:p};break;default:e={x:i.x,y:i.y}}var m=d?Q(d):null;if(null!=m){var g="y"===m?"height":"width";switch(u){case c:e[m]=e[m]-(i[g]/2-a[g]/2);break;case h:e[m]=e[m]+(i[g]/2-a[g]/2)}}return e}function _t(t,e){void 0===e&&(e={});var i=e,r=i.placement,a=void 0===r?t.placement:r,c=i.strategy,h=void 0===c?t.strategy:c,m=i.boundary,g=void 0===m?d:m,_=i.rootBoundary,b=void 0===_?u:_,v=i.elementContext,y=void 0===v?f:v,w=i.altBoundary,E=void 0!==w&&w,A=i.padding,T=void 0===A?0:A,C=U("number"!=typeof T?T:G(T,l)),O=y===f?p:f,k=t.rects.popper,D=t.elements[E?O:y],$=function(t,e,i,n){var s="clippingParents"===e?function(t){var e=ft(V(t)),i=["absolute","fixed"].indexOf(z(t).position)>=0&&S(t)?K(t):t;return L(i)?e.filter((function(t){return L(t)&&W(t,i)&&"body"!==x(t)})):[]}(t):[].concat(e),o=[].concat(s,[i]),r=o[0],a=o.reduce((function(e,i){var s=mt(t,i,n);return e.top=N(s.top,e.top),e.right=P(s.right,e.right),e.bottom=P(s.bottom,e.bottom),e.left=N(s.left,e.left),e}),mt(t,r,n));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}(L(D)?D:D.contextElement||q(t.elements.popper),g,b,h),I=H(t.elements.reference),M=gt({reference:I,element:k,strategy:"absolute",placement:a}),j=pt(Object.assign({},k,M)),F=y===f?j:I,B={top:$.top-F.top+C.top,bottom:F.bottom-$.bottom+C.bottom,left:$.left-F.left+C.left,right:F.right-$.right+C.right},R=t.modifiersData.offset;if(y===f&&R){var Y=R[a];Object.keys(B).forEach((function(t){var e=[o,s].indexOf(t)>=0?1:-1,i=[n,s].indexOf(t)>=0?"y":"x";B[t]+=Y[i]*e}))}return B}const bt={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,h=t.name;if(!e.modifiersData[h]._skip){for(var d=i.mainAxis,u=void 0===d||d,f=i.altAxis,p=void 0===f||f,_=i.fallbackPlacements,b=i.padding,v=i.boundary,y=i.rootBoundary,w=i.altBoundary,E=i.flipVariations,A=void 0===E||E,T=i.allowedAutoPlacements,C=e.options.placement,O=I(C),x=_||(O!==C&&A?function(t){if(I(t)===a)return[];var e=rt(t);return[lt(t),e,lt(e)]}(C):[rt(C)]),k=[C].concat(x).reduce((function(t,i){return t.concat(I(i)===a?function(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,c=i.allowedAutoPlacements,h=void 0===c?g:c,d=Z(n),u=d?a?m:m.filter((function(t){return Z(t)===d})):l,f=u.filter((function(t){return h.indexOf(t)>=0}));0===f.length&&(f=u);var p=f.reduce((function(e,i){return e[i]=_t(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[I(i)],e}),{});return Object.keys(p).sort((function(t,e){return p[t]-p[e]}))}(e,{placement:i,boundary:v,rootBoundary:y,padding:b,flipVariations:A,allowedAutoPlacements:T}):i)}),[]),L=e.rects.reference,S=e.rects.popper,D=new Map,$=!0,N=k[0],P=0;P