From d21a9ad32a1f352268959056873c33c4acb5b88f Mon Sep 17 00:00:00 2001 From: Richeek Das Date: Wed, 13 May 2026 02:06:32 -0400 Subject: [PATCH 1/3] Update docs. Make things easier to follow --- .github/workflows/docs.yml | 3 + .gitignore | 5 + docs/components/discovery.md | 45 +- docs/components/messages.md | 18 +- docs/components/node-and-executors.md | 71 +- docs/components/publisher-subscriber.md | 100 +- docs/components/serialization.md | 45 +- docs/concepts/architecture.md | 13 +- docs/concepts/async-execution-model.md | 38 +- docs/concepts/discovery-protocol.md | 13 +- docs/concepts/fingerprinting.md | 46 +- docs/concepts/message-wire-format.md | 40 +- docs/concepts/transport-and-qos.md | 26 +- docs/critique.md | 146 -- docs/gen_ref_pages.py | 175 +- docs/getting-started/discovery-daemon.md | 24 +- docs/getting-started/installation.md | 25 +- docs/getting-started/quickstart.md | 18 +- docs/guides/benchmarks.md | 30 +- docs/guides/debugging.md | 39 +- docs/guides/performance-tuning.md | 32 +- docs/guides/sync-mode.md | 117 + docs/index.md | 44 +- docs/mkdocs.yml | 10 +- docs/reference/core/executor.md | 3 + docs/reference/core/index.md | 3 + docs/reference/core/node.md | 3 + docs/reference/core/publisher.md | 3 + docs/reference/core/subscriber.md | 3 + docs/reference/core/subscriber_base.md | 3 + docs/reference/core/sync_subscriber.md | 3 + docs/reference/core/types.md | 3 + docs/reference/discovery/client.md | 3 + docs/reference/discovery/daemon.md | 3 + docs/reference/discovery/index.md | 3 + docs/reference/discovery/protocol.md | 3 + docs/reference/index.md | 37 + docs/reference/messages/base.md | 3 + docs/reference/messages/index.md | 3 + docs/reference/messages/standard.md | 3 + docs/reference/utils/hashing.md | 3 + docs/reference/utils/index.md | 3 + docs/reference/utils/logging.md | 3 + docs/reference/utils/loop.md | 3 + docs/reference/utils/runtime.md | 3 + docs/reference/utils/serialization.md | 3 + docs/reference/utils/tracing.md | 3 + docs/reference/zzz_extra.md | 1 + docs/site/404.html | 1502 ----------- docs/site/assets/images/favicon.png | Bin 1870 -> 0 bytes docs/site/assets/javascripts/LICENSE | 29 - .../assets/javascripts/bundle.63456bd9.min.js | 3 - .../workers/search.e2d2d235.min.js | 1 - .../stylesheets/classic/main.96fc3bb8.min.css | 1 - .../classic/palette.7dc9a0ad.min.css | 1 - .../stylesheets/modern/main.53a7feaf.min.css | 1 - .../modern/palette.dfe2e883.min.css | 1 - docs/site/components/discovery/index.html | 2027 -------------- docs/site/components/messages/index.html | 1916 -------------- .../components/node-and-executors/index.html | 2069 --------------- .../publisher-subscriber/index.html | 2331 ----------------- docs/site/components/serialization/index.html | 1980 -------------- docs/site/concepts/architecture/index.html | 1805 ------------- .../concepts/async-execution-model/index.html | 1840 ------------- .../concepts/discovery-protocol/index.html | 1930 -------------- docs/site/concepts/fingerprinting/index.html | 1853 ------------- .../concepts/message-wire-format/index.html | 1868 ------------- .../concepts/transport-and-qos/index.html | 1770 ------------- docs/site/critique/index.html | 2121 --------------- docs/site/gen_ref_pages.py | 47 - .../discovery-daemon/index.html | 1799 ------------- .../getting-started/installation/index.html | 1758 ------------- .../getting-started/quickstart/index.html | 1809 ------------- docs/site/guides/benchmarks/index.html | 1734 ------------ docs/site/guides/debugging/index.html | 1808 ------------- .../site/guides/performance-tuning/index.html | 1772 ------------- docs/site/index.html | 1758 ------------- docs/site/mkdocs.yml | 140 - docs/site/objects.inv | Bin 127 -> 0 bytes docs/site/search.json | 1 - docs/site/sitemap.xml | 72 - .../site/tutorials/custom-messages/index.html | 1942 -------------- .../tutorials/multi-node-system/index.html | 1980 -------------- .../tutorials/numpy-and-images/index.html | 1849 ------------- .../site/tutorials/pytorch-tensors/index.html | 1889 ------------- docs/tutorials/custom-messages.md | 35 +- docs/tutorials/multi-node-system.md | 27 +- docs/tutorials/numpy-and-images.md | 45 +- docs/tutorials/pytorch-tensors.md | 29 +- pyproject.toml | 3 - 90 files changed, 642 insertions(+), 46131 deletions(-) delete mode 100644 docs/critique.md create mode 100644 docs/guides/sync-mode.md create mode 100644 docs/reference/core/executor.md create mode 100644 docs/reference/core/index.md create mode 100644 docs/reference/core/node.md create mode 100644 docs/reference/core/publisher.md create mode 100644 docs/reference/core/subscriber.md create mode 100644 docs/reference/core/subscriber_base.md create mode 100644 docs/reference/core/sync_subscriber.md create mode 100644 docs/reference/core/types.md create mode 100644 docs/reference/discovery/client.md create mode 100644 docs/reference/discovery/daemon.md create mode 100644 docs/reference/discovery/index.md create mode 100644 docs/reference/discovery/protocol.md create mode 100644 docs/reference/index.md create mode 100644 docs/reference/messages/base.md create mode 100644 docs/reference/messages/index.md create mode 100644 docs/reference/messages/standard.md create mode 100644 docs/reference/utils/hashing.md create mode 100644 docs/reference/utils/index.md create mode 100644 docs/reference/utils/logging.md create mode 100644 docs/reference/utils/loop.md create mode 100644 docs/reference/utils/runtime.md create mode 100644 docs/reference/utils/serialization.md create mode 100644 docs/reference/utils/tracing.md create mode 100644 docs/reference/zzz_extra.md delete mode 100644 docs/site/404.html delete mode 100644 docs/site/assets/images/favicon.png delete mode 100644 docs/site/assets/javascripts/LICENSE delete mode 100644 docs/site/assets/javascripts/bundle.63456bd9.min.js delete mode 100644 docs/site/assets/javascripts/workers/search.e2d2d235.min.js delete mode 100644 docs/site/assets/stylesheets/classic/main.96fc3bb8.min.css delete mode 100644 docs/site/assets/stylesheets/classic/palette.7dc9a0ad.min.css delete mode 100644 docs/site/assets/stylesheets/modern/main.53a7feaf.min.css delete mode 100644 docs/site/assets/stylesheets/modern/palette.dfe2e883.min.css delete mode 100644 docs/site/components/discovery/index.html delete mode 100644 docs/site/components/messages/index.html delete mode 100644 docs/site/components/node-and-executors/index.html delete mode 100644 docs/site/components/publisher-subscriber/index.html delete mode 100644 docs/site/components/serialization/index.html delete mode 100644 docs/site/concepts/architecture/index.html delete mode 100644 docs/site/concepts/async-execution-model/index.html delete mode 100644 docs/site/concepts/discovery-protocol/index.html delete mode 100644 docs/site/concepts/fingerprinting/index.html delete mode 100644 docs/site/concepts/message-wire-format/index.html delete mode 100644 docs/site/concepts/transport-and-qos/index.html delete mode 100644 docs/site/critique/index.html delete mode 100644 docs/site/gen_ref_pages.py delete mode 100644 docs/site/getting-started/discovery-daemon/index.html delete mode 100644 docs/site/getting-started/installation/index.html delete mode 100644 docs/site/getting-started/quickstart/index.html delete mode 100644 docs/site/guides/benchmarks/index.html delete mode 100644 docs/site/guides/debugging/index.html delete mode 100644 docs/site/guides/performance-tuning/index.html delete mode 100644 docs/site/index.html delete mode 100644 docs/site/mkdocs.yml delete mode 100644 docs/site/objects.inv delete mode 100644 docs/site/search.json delete mode 100644 docs/site/sitemap.xml delete mode 100644 docs/site/tutorials/custom-messages/index.html delete mode 100644 docs/site/tutorials/multi-node-system/index.html delete mode 100644 docs/site/tutorials/numpy-and-images/index.html delete mode 100644 docs/site/tutorials/pytorch-tensors/index.html diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 9cc373d..98dfb32 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -35,6 +35,9 @@ jobs: python -m pip install --upgrade pip pip install -e ".[docs]" + - name: Verify generated API reference is in sync + run: python docs/gen_ref_pages.py --check + - name: Build docs run: zensical build -f docs/mkdocs.yml diff --git a/.gitignore b/.gitignore index e1f6795..f3a666a 100644 --- a/.gitignore +++ b/.gitignore @@ -206,3 +206,8 @@ cython_debug/ marimo/_static/ marimo/_lsp/ __marimo__/ + +# Internal-only docs (not published) +/docs/critique.md +/docs/plans/ +/docs/TODO diff --git a/docs/components/discovery.md b/docs/components/discovery.md index 607d2b6..c30845b 100644 --- a/docs/components/discovery.md +++ b/docs/components/discovery.md @@ -4,10 +4,7 @@ > [`cortex.discovery.client`](../reference/discovery/client.md), > [`cortex.discovery.protocol`](../reference/discovery/protocol.md) -Discovery is Cortex's control plane: a single long-lived process that maps -topic names to ZMQ endpoints. It sits off the data path — once a subscriber -has an endpoint, messages flow publisher → subscriber directly without the -daemon's involvement. +A single long-lived process mapping topic names to ZMQ endpoints. Off the data path — once a subscriber has an endpoint, messages flow publisher → subscriber directly. ## Moving parts @@ -25,21 +22,15 @@ flowchart LR PR -.-> CL ``` -Everyone agrees on the wire format via `protocol.py`. The daemon runs a -single-threaded REP loop. The client speaks REQ from every publisher and -subscriber in the graph. +Both sides agree on the wire format via `protocol.py`. The daemon runs a single-threaded REP loop; publishers/subscribers speak REQ. ## Daemon -Implemented in [`DiscoveryDaemon`][cortex.discovery.daemon.DiscoveryDaemon]. - -Key behaviors: +[`DiscoveryDaemon`][cortex.discovery.daemon.DiscoveryDaemon]: - Binds `zmq.REP` at `ipc:///tmp/cortex/discovery.sock` by default. -- Maintains `_topics: dict[str, TopicInfo]` — **one publisher per topic**. -- `RCVTIMEO=1000` on the socket so the loop can check `_running` for clean - Ctrl-C. This also means the daemon is naturally single-request-at-a-time — - a slow client blocks all others. +- Maintains `_topics: dict[str, TopicInfo]` — one publisher per topic. +- `RCVTIMEO=1000` so the loop can check `_running` for clean Ctrl-C. Single request at a time — a slow client blocks others. ### State transitions @@ -66,12 +57,7 @@ stateDiagram-v2 ## Client -Implemented in [`DiscoveryClient`][cortex.discovery.client.DiscoveryClient]. - -Thin REQ wrapper around the protocol. Important operational detail: **REQ -sockets stick after a timeout** — they block subsequent sends waiting for a -reply that never came. The client handles this by closing and recreating the -socket on every timeout (`_reconnect`). Callers don't see it. +[`DiscoveryClient`][cortex.discovery.client.DiscoveryClient] is a thin REQ wrapper. Operational detail: **REQ sockets stick after a timeout** — they block subsequent sends waiting for a reply that never came. The client closes and recreates the socket on every timeout (`_reconnect`); callers don't see it. ### REQ timeout recovery @@ -98,7 +84,7 @@ flowchart TD ## Protocol -Implemented in [`cortex.discovery.protocol`](../reference/discovery/protocol.md). +[`cortex.discovery.protocol`](../reference/discovery/protocol.md): | Type | Purpose | | -------------------------------------------------------------------- | ----------------------------------------- | @@ -108,21 +94,16 @@ Implemented in [`cortex.discovery.protocol`](../reference/discovery/protocol.md) | [`DiscoveryRequest`][cortex.discovery.protocol.DiscoveryRequest] | command + optional topic_info / topic_name | | [`DiscoveryResponse`][cortex.discovery.protocol.DiscoveryResponse] | status, message, topic_info, topics | -All payloads are msgpack. `TopicInfo` is nested as a packed sub-blob so -discovery responses stay flat. - -## Known limitations +All payloads are msgpack. `TopicInfo` is nested as a packed sub-blob so responses stay flat. -Summarized here, detailed in [critique.md](../critique.md): +## Limitations -- One-publisher-per-topic. -- No heartbeats or leases — crashed publishers leave stale entries. -- Single-threaded REP — slow client starves others. -- `retries=1` in the client is a fencepost; effective retries today is zero. -- Daemon state lost on restart; publishers do not auto-re-register. +- One publisher per topic. +- No heartbeats or leases — a crashed publisher leaves a stale entry. +- Single-threaded REP — a slow client blocks others. +- Daemon state is lost on restart; publishers don't auto-re-register. ## See also - [Concepts → Discovery protocol](../concepts/discovery-protocol.md) - [Getting started → Running the discovery daemon](../getting-started/discovery-daemon.md) -- [Critique](../critique.md) diff --git a/docs/components/messages.md b/docs/components/messages.md index 619ad96..e69140e 100644 --- a/docs/components/messages.md +++ b/docs/components/messages.md @@ -3,9 +3,7 @@ > **Source:** [`cortex.messages.base`](../reference/messages/base.md), > [`cortex.messages.standard`](../reference/messages/standard.md) -Messages are just `@dataclass`es that inherit from -[`Message`][cortex.messages.base.Message]. Registering with the type system, -computing a fingerprint, and (de)serialization all happen automatically. +A message is a `@dataclass` that inherits from [`Message`][cortex.messages.base.Message]. Registration, fingerprinting, and serialization are automatic. ## Anatomy of a message @@ -55,22 +53,16 @@ class JointTrajectory(Message): frame_id: str = "" ``` -That is the entire contract. The class is registered into -[`MessageType._registry`][cortex.messages.base.MessageType] by fingerprint at -import time, and gains: +The class registers into [`MessageType._registry`][cortex.messages.base.MessageType] by fingerprint at import time and gains: - `JointTrajectory.fingerprint()` — 64-bit ID. -- `msg.to_frames()` / `JointTrajectory.from_frames(frames)` — the transport path. -- `msg.to_bytes()` / `JointTrajectory.from_bytes(data)` — the legacy blob path. +- `msg.to_frames()` / `JointTrajectory.from_frames(frames)` — transport path. +- `msg.to_bytes()` / `JointTrajectory.from_bytes(data)` — legacy single-blob path. - `Message.decode(blob)` — class dispatch via fingerprint registry. ## Sequence numbering -!!! warning "Class-level counter" - `Message._sequence_counter` is shared across **all publisher instances** of - the same message class in the process. Two `ArrayMessage` publishers - interleave sequence numbers. Per-topic gap detection therefore needs a - per-publisher counter today; see [critique.md § 12](../critique.md). +Per-publisher monotonic counter, attached to each message's header. A class-level fallback counter on `Message._sequence_counter` covers direct `to_bytes`/`to_frames` use outside a Publisher (tests, ad-hoc serialization). ## Built-in messages diff --git a/docs/components/node-and-executors.md b/docs/components/node-and-executors.md index c022a02..6dc3a6a 100644 --- a/docs/components/node-and-executors.md +++ b/docs/components/node-and-executors.md @@ -3,10 +3,7 @@ > **Source:** [`cortex.core.node`](../reference/core/node.md), > [`cortex.core.executor`](../reference/core/executor.md) -A [`Node`][cortex.core.node.Node] is the user-facing composition unit: it owns -a shared ZMQ async context and a collection of publishers, subscribers, and -timers. Executors provide the scheduling primitives that timers and -subscriber receive loops run on. +A [`Node`][cortex.core.node.Node] owns a ZMQ async context, a set of publishers/subscribers, and a list of timers. Executors are the loops that drive timers and subscriber receive paths. ## Responsibilities @@ -28,11 +25,7 @@ flowchart TB S -. uses .-> CTX ``` -One node = one process boundary in practice. Nothing stops you running -multiple nodes in the same process (`asyncio.gather([n.run() for n in nodes])`, -see [`examples/multi_node_system.py`](https://github.com/sudoRicheek/cortex/blob/main/examples/multi_node_system.py)), -but remember they share the same event loop — a slow callback in one still -blocks the others. +One node ≈ one process. You can run several in the same process via `asyncio.gather([n.run() for n in nodes])` ([`examples/multi_node_system.py`](https://github.com/sudoRicheek/cortex/blob/main/examples/multi_node_system.py)) — but they share the event loop, so a slow callback in one blocks the others. ## Lifecycle @@ -47,11 +40,9 @@ stateDiagram-v2 Closed --> [*]: context terminated ``` -### `node.run()` +`node.run()` spawns one asyncio task per timer and one per callback-bearing subscriber, then `asyncio.gather`s them. -Spawns one asyncio task per timer and one per callback-bearing subscriber, -then `asyncio.gather`s them. Returns when all tasks complete or the node is -stopped. +`node.close()` stops executors, cancels tasks, closes all sockets, and terms the ZMQ context. Idempotent. ```python async with Node("my_node") as node: @@ -61,15 +52,9 @@ async with Node("my_node") as node: # __aexit__ calls close() automatically ``` -### `node.close()` - -Stops all executors, cancels outstanding tasks, closes every publisher and -subscriber (each of which unregisters/unbinds their own socket), and -terminates the shared ZMQ context. Idempotent. - ## Executors -Two flavours, both subclasses of `BaseExecutor`. +Two subclasses of `BaseExecutor`: ```mermaid classDiagram @@ -95,24 +80,11 @@ classDiagram ### `AsyncExecutor` -"Run this coroutine as fast as possible, yielding between iterations." - -```mermaid -flowchart LR - Start --> Check{running?} - Check -- no --> End - Check -- yes --> Call[await func] - Call -- exception --> Log[log error] - Log --> Sleep - Call --> Sleep[await sleep 0] - Sleep --> Check -``` - -Used by `Subscriber.run` to drive the receive-dispatch loop. +Tight loop: `await func(); await asyncio.sleep(0)`. Used by `Subscriber.run` for receive-dispatch. ### `RateExecutor` -"Run this coroutine at a constant rate, catching up on overruns." +Fixed-grid timer at `rate_hz`. `next_exec_time` is initialized once, then advances by exactly one `interval` per callback invocation — never reset to "now." ```mermaid flowchart TD @@ -122,21 +94,12 @@ flowchart TD Now --> Due{now >= next?} Due -- yes --> Call[await func] Call --> Advance[next += interval] - Advance --> Behind{next < now?} - Behind -- yes --> Reset[next = now + interval] - Behind -- no --> Wait - Reset --> Wait - Due -- no --> Wait[await sleep next - now] + Advance --> Wait + Due -- no --> Wait[await sleep max 0, next - now] Wait --> Loop ``` -The catch-up branch silently drops ticks — if your 100 Hz callback takes -20 ms once, you do not get two callbacks back-to-back; you skip one tick. - -!!! warning "Redundant yield" - Today there is an `await asyncio.sleep(0)` inside the loop *and* - `await asyncio.sleep(max(0, dt))` at the bottom. That generates an extra - wakeup per tick. See [critique § 15](../critique.md). +**Missed ticks are not skipped.** If a 100 Hz callback overruns by 20 ms, the next two ticks fire back-to-back with zero-length sleeps until the clock catches up. The grid is preserved; no tick is silently dropped. ## Timer usage @@ -145,21 +108,11 @@ node.create_timer(1.0 / 30, self.publish_frame) # 30 Hz node.create_timer(1.0, self.log_stats) # 1 Hz ``` -Timers are plain async functions — no decorator, no magic. They run in the -same event loop as subscriber callbacks, so the same head-of-line caveat -applies. +Plain async functions, no decorator. They share the event loop with subscriber callbacks — same head-of-line caveat. ## Shared ZMQ context -Every publisher and subscriber created through a node **reuses** the node's -`zmq.asyncio.Context`. This means: - -- Socket creation is cheap. -- io threads are shared across all sockets in the node. -- Terminating the node's context cleanly shuts down all its sockets. - -Do not create your own context inside callbacks; you'll leak resources and -defeat the shared-io-thread optimization. +Every publisher/subscriber created through a node reuses the node's `zmq.asyncio.Context`. Socket creation is cheap, io threads are shared, terminating the context shuts everything down. Don't create your own context inside callbacks. ## Minimal complete node diff --git a/docs/components/publisher-subscriber.md b/docs/components/publisher-subscriber.md index c6b69f8..93c59fd 100644 --- a/docs/components/publisher-subscriber.md +++ b/docs/components/publisher-subscriber.md @@ -3,10 +3,7 @@ > **Source:** [`cortex.core.publisher`](../reference/core/publisher.md), > [`cortex.core.subscriber`](../reference/core/subscriber.md) -The data-plane workhorses. A `Publisher` binds a ZMQ `PUB` socket and registers -with discovery; a `Subscriber` looks up the endpoint, connects a `SUB` socket, -and drives an async receive loop. Discovery is consulted **once per topic** on -startup — it is not on the hot path. +A `Publisher` binds a ZMQ `PUB` socket and registers with discovery. A `Subscriber` looks up the endpoint, connects a `SUB` socket, and drives an async receive loop. Discovery is consulted once per topic on startup; it's not on the hot path. ## Relationship to the rest of the stack @@ -27,9 +24,7 @@ flowchart LR ### Construction -Always create via [`Node.create_publisher`][cortex.core.node.Node.create_publisher] — -direct construction works but skips the shared ZMQ context reuse and the -node-level registration bookkeeping. +Always create via [`Node.create_publisher`][cortex.core.node.Node.create_publisher]. Direct construction works but skips the shared ZMQ context and node-level bookkeeping. ```python pub = node.create_publisher( @@ -59,15 +54,10 @@ sequenceDiagram Note over Pub: ready; user can publish() ``` -Two things worth calling out: +Two notes: -1. The IPC address is derived deterministically from `node_name` and - `topic_name` via [`generate_ipc_address`][cortex.core.publisher.generate_ipc_address]: - `ipc:///tmp/cortex/topics/__.sock`. -2. `_setup_socket` unlinks any existing file at that path before binding. That - protects against crash-leftover sockets, but also means **two publishers - configured with the same `node_name + topic_name` in the same process tree - will silently stomp each other** — see [critique § 10](../critique.md). +1. The IPC address is derived from `node_name + topic_name` via [`generate_ipc_address`][cortex.core.publisher.generate_ipc_address]: `ipc:///tmp/cortex/topics/__.sock`. +2. `_setup_socket` unlinks any existing file at that path before binding. That cleans up crash-leftover sockets — but two publishers with the same `node_name + topic_name` silently stomp each other. ### Publish path @@ -87,32 +77,26 @@ flowchart LR Send -->|zmq.Again| Drop[return False] ``` -`publish()` is **synchronous** and returns a boolean: +`publish()` is synchronous and returns a `bool`: - `True` — handed to ZMQ successfully. -- `False` — `zmq.Again`, queue full, message dropped. +- `False` — `zmq.Again` (queue full, message dropped) or any other exception (logged, swallowed). -Any other exception is logged and swallowed; `publish` still returns `False`. -For robotics code this "fire and forget" is intentional — the caller decides -whether to retry based on the return value and the topic's role. +Fire-and-forget. The caller decides whether to retry based on the return value and the topic's role. ### Async context quirk -`Node` owns a `zmq.asyncio.Context`. The `Publisher` constructor detects this -and wraps a **sync** `zmq.Context` around the same underlying io threads: +`Node` owns a `zmq.asyncio.Context`. The `Publisher` constructor detects this and wraps a sync `zmq.Context` around the same underlying io threads: ```python if isinstance(self._context, zmq.asyncio.Context): self._context: zmq.Context = zmq.Context(self._context) ``` -This keeps `publish()` a normal function call instead of forcing every publish -to be `await`ed. It is the right performance choice, but it has consequences: +This keeps `publish()` a plain function call. Performance win, with one consequence: !!! danger "`zmq.PUB` is not thread-safe" - Do not call `publish()` on the same `Publisher` from multiple threads - (or multiple asyncio tasks that could race on `send_multipart`). Serialize - per-publisher calls yourself if you fan out work. + Don't call `publish()` on the same `Publisher` from multiple threads or from tasks that race on `send_multipart`. Serialize per-publisher calls yourself if you fan out work. ### Lifecycle and cleanup @@ -126,16 +110,11 @@ stateDiagram-v2 Closed --> [*]: unregister,
unlink .sock file ``` -`Publisher.close()` is best-effort: it unregisters from the daemon (silently -tolerates a dead daemon), closes the socket, and removes the IPC file. -Exceptions from any one step do not block the others. +`Publisher.close()` unregisters from the daemon (tolerates a dead daemon), closes the socket, and removes the IPC file. Exceptions in any step don't block the others. ### Statistics -`publisher.publish_count`, `publisher.last_publish_time`, and -`publisher.is_registered` are exposed for instrumentation. They update on the -hot path with no locking — read them from the same task that calls `publish()` -for deterministic numbers. +`publish_count`, `last_publish_time`, `is_registered` are exposed for instrumentation. Updated on the hot path with no locking — read them from the same task that calls `publish()` for deterministic numbers. ## Subscriber @@ -152,8 +131,7 @@ sub = node.create_subscriber( ) ``` -If `callback` is `None`, the subscriber is passive — call `await sub.receive()` -manually. With a callback, `Node.run()` will drive the receive loop. +If `callback` is `None`, the subscriber is passive — call `await sub.receive()` manually. With a callback, `Node.run()` drives the receive loop. ### Startup sequence @@ -183,9 +161,7 @@ sequenceDiagram S->>Pub: SUB connect + SUBSCRIBE topic ``` -The constructor tries a non-blocking lookup first so that when a publisher is -already up, no polling is needed. The polling fallback only kicks in inside -`sub.run()` via [`wait_for_topic_async`][cortex.discovery.client.DiscoveryClient.wait_for_topic_async]. +The constructor tries a non-blocking lookup first. If the publisher is already up, no polling is needed. Otherwise the polling fallback runs inside `sub.run()` via [`wait_for_topic_async`][cortex.discovery.client.DiscoveryClient.wait_for_topic_async]. ### Receive loop @@ -199,17 +175,12 @@ flowchart LR Yield --> Loop ``` -- `copy=False` means each frame is a `zmq.Frame` — the metadata and array - buffers are memoryview-able without a copy. See - [`cortex.utils.serialization`](../reference/utils/serialization.md). -- The one-frame fast path (`len(payload_frames) == 1`) handles legacy - publishers still on the single-blob path — it falls back to - `from_bytes` on the single payload buffer. +- `copy=False` makes each frame a `zmq.Frame` — metadata and array buffers are memoryview-able without copying. See [`cortex.utils.serialization`](../reference/utils/serialization.md). +- The one-frame fast path (`len(payload_frames) == 1`) handles legacy publishers on the single-blob path — falls back to `from_bytes`. ### Head-of-line blocking -The callback runs **inline** in the receive loop. A slow callback stalls -everything: +The callback runs inline in the receive loop. A slow callback stalls it: ```mermaid gantt @@ -225,7 +196,7 @@ gantt callback m2 :52, 55 ``` -If callbacks do meaningful work, dispatch them to a task or thread pool: +If callbacks do real work, dispatch them to a task or thread pool: ```python import asyncio @@ -234,42 +205,15 @@ async def on_image(msg, header): asyncio.create_task(process_in_background(msg, header)) ``` -Or use a bounded queue + worker pattern. The roadmap item in -[critique § 6](../critique.md) is to lift this into the framework. +Or use a bounded queue + worker pattern of your own. ### Fingerprint verification -On connect the subscriber compares its class's fingerprint to the one in the -registry entry. Today a mismatch only logs a warning and **proceeds anyway** — -downstream decoding will then fail hard. Treat fingerprint warnings as errors -in your code. +On connect the subscriber compares its class's fingerprint to the registry entry. In async mode, mismatch logs a warning and continues (downstream decode will fail loudly). For strict behavior pass `strict_fingerprint=True`, or use `mode='sync'` (always strict). ### Cleanup -`Subscriber.close()` stops the executor, closes the discovery client and SUB -socket, and flips `is_connected` to `False`. Safe to call multiple times; -errors are suppressed so teardown does not cascade. - -## Statistics and instrumentation - -| Property | Publisher | Subscriber | -| ---------------------------------------- | --------- | ---------- | -| `publish_count` / `receive_count` | ✓ | ✓ | -| `last_publish_time` / `last_receive_time`| ✓ | ✓ | -| `is_registered` / `is_connected` | ✓ | ✓ | -| `topic_info` | | ✓ | - -None of these are atomic; treat them as coarse gauges. - -## Common pitfalls - -| Symptom | Cause | Fix | -| ------------------------------------------ | ------------------------------------------------------------------------------------------ | ---------------------------------------- | -| First N messages not received | ZMQ "slow joiner": SUB not connected yet when PUB started publishing | Let subscriber start first, or sleep briefly before first publish | -| Subscriber receives nothing, no errors | Topic name mismatch, or forgot to call `node.run()` | Log both sides; run `cortex-discovery --log-level DEBUG` | -| `publish()` returns `False` repeatedly | Subscriber can't keep up; SNDHWM reached | Increase `queue_size`, or reduce publish rate | -| Mutating a received array "corrupts" later | Decoded arrays alias ZMQ frame memory | `arr = arr.copy()` before mutating | -| Two processes stomp each other's socket | Same `node_name + topic_name` | Unique node names per process | +`Subscriber.close()` stops the executor, closes the discovery client and SUB socket, and flips `is_connected` to `False`. Idempotent; errors are suppressed so teardown doesn't cascade. ## See also diff --git a/docs/components/serialization.md b/docs/components/serialization.md index 485b6b7..94c1c78 100644 --- a/docs/components/serialization.md +++ b/docs/components/serialization.md @@ -3,10 +3,12 @@ > **Source:** [`cortex.utils.serialization`](../reference/utils/serialization.md), > [`cortex.utils.hashing`](../reference/utils/hashing.md) -Two encodings live side by side: a **multipart / out-of-band** path that the -transport actually uses, and a **single-blob** path kept for the legacy -`Message.to_bytes` / `decode` API and tests. Both support the same Python -types; only their frame layout differs. +Two encodings: + +- **Multipart / OOB** — what the transport uses. Arrays ride as separate frames. +- **Single-blob** — legacy `Message.to_bytes` / `decode`, used by tests and persistence. + +Both support the same Python types; only the frame layout differs. ## Supported types @@ -34,8 +36,7 @@ types; only their frame layout differs. Bufs2 --> Out ``` - The function of interest is - [`serialize_message_frames`][cortex.utils.serialization.serialize_message_frames]: + Entry point: [`serialize_message_frames`][cortex.utils.serialization.serialize_message_frames]: ```python metadata_bytes, [buf0, buf1, ...] = serialize_message_frames(values) @@ -52,14 +53,11 @@ types; only their frame layout differs. Ext --> Blob[single bytes blob] ``` - The single blob round-trips through `serialize(value)` → - `deserialize(data)`. Useful for persisting to disk, caches, or when you - need a self-contained payload without tracking extra buffers. + `serialize(value)` → `deserialize(data)`. Useful for persistence or any self-contained payload without extra buffers. ## OOB descriptors -An out-of-band descriptor is a small dict that takes the place of the array -inside the msgpack metadata: +A small dict that takes the place of the array inside the msgpack metadata: ```python # numpy @@ -70,9 +68,7 @@ inside the msgpack metadata: "shape": [1, 3, 224, 224], "device": "cuda:0", "requires_grad": True} ``` -The `buffer` index refers into the ZMQ frames that follow the metadata. -Nested structures (dict of arrays, list of tensors, etc.) are walked -recursively by `_encode_transport_value` / `_decode_transport_value`. +`buffer` is the index into the ZMQ frames after the metadata. Nested structures (dict of arrays, list of tensors) are walked recursively by `_encode_transport_value` / `_decode_transport_value`. ## Zero-copy on the decode side @@ -91,30 +87,17 @@ sequenceDiagram ``` !!! warning "Aliasing caveat" - The returned NumPy array is **a view over the ZMQ frame buffer**. It is - safe to read as long as the frame lives, which is at least until your - callback returns. If you need to: - - - mutate the array, or - - keep it past the callback, - - call `arr = arr.copy()` first. This is cheap compared to the savings on - the hot path. + The returned NumPy array is a view over the ZMQ frame buffer. Safe to read while the frame lives (at least until your callback returns). If you need to mutate or keep it past the callback, `arr = arr.copy()` first. ## PyTorch specifics -- Tensors are **always moved to CPU** for transport. Transport frames carry - the tensor's CPU bytes plus the original device string. -- On decode, CUDA tensors are moved back to the original device when CUDA is - available; otherwise they stay on CPU. +- Tensors are always moved to CPU for transport. +- On decode, CUDA tensors are moved back to the original device if CUDA is available; otherwise they stay on CPU. - `requires_grad` is preserved. ## Fingerprinting -Separate but related: [`compute_fingerprint(cls)`][cortex.utils.hashing.compute_fingerprint] -computes a 64-bit identity from the module path, class name, and sorted -`field:type` pairs. Cached per-class in `_fingerprint_cache`. See -[Concepts → Fingerprinting](../concepts/fingerprinting.md) for the full story. +[`compute_fingerprint(cls)`][cortex.utils.hashing.compute_fingerprint] computes a 64-bit identity from module path, class name, and sorted `field:type` pairs. Cached per-class. Full story: [Concepts → Fingerprinting](../concepts/fingerprinting.md). ## When to use each helper diff --git a/docs/concepts/architecture.md b/docs/concepts/architecture.md index ef5159a..bc86d7a 100644 --- a/docs/concepts/architecture.md +++ b/docs/concepts/architecture.md @@ -1,8 +1,6 @@ # Architecture -Cortex has three moving parts: the **discovery daemon**, **publisher** nodes, -and **subscriber** nodes. They coordinate over ZeroMQ — a REQ/REP control plane -for discovery and a PUB/SUB data plane for messages. +Three moving parts: the **discovery daemon**, **publisher** nodes, **subscriber** nodes. They coordinate over ZeroMQ — REQ/REP for discovery (control plane), PUB/SUB for messages (data plane). ## High-level view @@ -29,7 +27,7 @@ flowchart TB ## Message journey -Tracing one frame end to end: +One frame, end to end: ```mermaid sequenceDiagram @@ -53,8 +51,7 @@ sequenceDiagram Sub->>CB: await callback(msg, header) ``` -Key invariant: array buffers ride as **separate ZMQ frames**, not inline in the -metadata. See [Message wire format](message-wire-format.md). +Array buffers ride as **separate ZMQ frames**, not inline in the metadata. See [Message wire format](message-wire-format.md). ## Process layout @@ -84,9 +81,7 @@ flowchart LR PUB2 -.->|IPC| SUB2 ``` -Each topic gets its own IPC socket under `/tmp/cortex/topics/`. A single `Node` -shares one `zmq.asyncio.Context` across all its publishers and subscribers to -avoid per-socket io thread overhead. +Each topic gets its own IPC socket under `/tmp/cortex/topics/`. A `Node` shares one `zmq.asyncio.Context` across all its publishers and subscribers to avoid per-socket io thread overhead. ## See also diff --git a/docs/concepts/async-execution-model.md b/docs/concepts/async-execution-model.md index 4449ca7..6f1b401 100644 --- a/docs/concepts/async-execution-model.md +++ b/docs/concepts/async-execution-model.md @@ -1,8 +1,6 @@ # Async execution model -Cortex nodes are asyncio-native. One event loop per process drives all -publishers, subscribers, and timers for that node. On Linux and macOS, -[`cortex.run`][cortex.utils.loop.run] prefers `uvloop` for lower tail latency. +Cortex nodes are asyncio-native: one event loop per process drives all publishers, subscribers, and timers for that node. On Linux/macOS, [`cortex.run`][cortex.utils.loop.run] uses `uvloop` when available. ## Node task graph @@ -15,9 +13,7 @@ flowchart TB Loop --> S2[Subscriber 2
AsyncExecutor] ``` -`Node.run()` spawns one task per timer (`RateExecutor`) and one per -callback-bearing subscriber (`AsyncExecutor`). It then `asyncio.gather`s them -until cancelled. +`Node.run()` spawns one task per timer (`RateExecutor`) and one per callback-bearing subscriber (`AsyncExecutor`), then `asyncio.gather`s them until cancelled. ## `RateExecutor` cadence @@ -27,19 +23,18 @@ sequenceDiagram participant R as RateExecutor participant CB as callback - loop every interval + loop while running L->>R: resume - R->>CB: await callback() - R->>R: next_exec_time += interval - alt fell behind - R->>R: next_exec_time = now + interval + R->>R: now = perf_counter() + alt now >= next_exec_time + R->>CB: await callback() + R->>R: next_exec_time += interval end - R->>L: sleep(next_exec_time - now) + R->>L: sleep(max(0, next_exec_time - now)) end ``` -Catch-up logic **silently drops ticks** when a callback overruns its period — -something to keep in mind for control loops. +`next_exec_time` is set once at start and advances by exactly one `interval` per callback fire. If a callback overruns, the next sleep is zero-length until the clock catches up — **ticks are never skipped**, they fire back-to-back until aligned with the grid. ## `AsyncExecutor` receive loop @@ -61,25 +56,18 @@ sequenceDiagram ``` !!! warning "Head-of-line blocking" - A slow callback stalls the receive loop. Messages pile up on the SUB HWM - and get evicted. If you expect variable-latency work, offload callback - bodies to `asyncio.create_task(...)` or a thread pool. + A slow callback stalls the receive loop. Messages pile up on the SUB HWM and get evicted. Offload variable-latency work to `asyncio.create_task(...)` or a thread pool. ## Publish is sync-inside-async -The `Publisher` uses a sync `zmq.Context` (shadowed onto the node's async -context). `publish()` is a plain function call — no `await`. This avoids the -overhead of the async zmq integration on the send path. +`Publisher` holds a plain `zmq.Context` (shadowed onto the node's async context). `publish()` is a regular function call — no `await` — to skip the async zmq send overhead. !!! danger "Not thread-safe" - A `zmq.PUB` socket is not safe to call from multiple threads or tasks - concurrently. Serialize calls to `publish()` per publisher. + `zmq.PUB` sockets are not safe to call from multiple threads or tasks concurrently. Serialize calls to `publish()` per publisher. ## uvloop -On Unix, importing `cortex.run` checks for `uvloop` and uses it if present. -Measured impact: modest throughput improvement, meaningful p99 latency -reduction on high-rate small messages. +On Unix, `cortex.run` uses `uvloop` if it's installed. Lower p99 latency on high-rate small messages. ## See also diff --git a/docs/concepts/discovery-protocol.md b/docs/concepts/discovery-protocol.md index 9a73108..699496f 100644 --- a/docs/concepts/discovery-protocol.md +++ b/docs/concepts/discovery-protocol.md @@ -1,8 +1,6 @@ # Discovery protocol -The discovery daemon speaks a tiny msgpack-over-REQ/REP protocol. It is not -on the data path — once a subscriber has the endpoint, messages flow -publisher → subscriber directly. +A msgpack-over-REQ/REP protocol. Not on the data path — once a subscriber has the endpoint, messages flow publisher → subscriber directly. ## Commands @@ -68,13 +66,11 @@ sequenceDiagram end ``` -`wait_for_topic_async` implements the retry loop with `asyncio.sleep` so the -event loop keeps spinning. +`wait_for_topic_async` runs the retry loop with `asyncio.sleep` so the event loop keeps spinning. ## REQ-socket recovery -ZMQ `REQ` sockets enter a bad state after a missed reply — they block further -sends. The client detects `zmq.Again` on timeout and rebuilds the socket: +A ZMQ `REQ` socket gets stuck after a missed reply. The client detects `zmq.Again` on timeout and rebuilds the socket: ```mermaid flowchart TD @@ -100,9 +96,6 @@ See [`DiscoveryClient._reconnect`][cortex.discovery.client.DiscoveryClient]. | Two publishers, same topic | Second registration rejected with `ALREADY_EXISTS`. | | Subscriber looks up before publisher | `NOT_FOUND`; caller may `wait_for_topic` to poll. | -Roadmap items (see [critique.md](../critique.md)) to address these: leases with -heartbeats, multi-publisher support, and notify-on-change. - ## See also - [`cortex.discovery.protocol`](../reference/discovery/protocol.md) diff --git a/docs/concepts/fingerprinting.md b/docs/concepts/fingerprinting.md index b442c22..87cd06b 100644 --- a/docs/concepts/fingerprinting.md +++ b/docs/concepts/fingerprinting.md @@ -1,13 +1,9 @@ # Fingerprinting -Every message class gets a **64-bit identifier** derived from its name and -field schema. The fingerprint rides in the header of every published message -and does two jobs: +Every message class has a 64-bit identifier derived from its name + field schema. The fingerprint rides in every message header and does two jobs: -1. **Type dispatch** — `Message.decode(bytes)` looks up the right class in the - [`MessageType`][cortex.messages.base.MessageType] registry. -2. **Compatibility check** — subscribers verify that the topic they looked up - advertises the same fingerprint as the type they were written against. +1. **Type dispatch** — `Message.decode(bytes)` looks up the class in [`MessageType`][cortex.messages.base.MessageType]. +2. **Compatibility check** — subscribers verify the topic advertises the same fingerprint as their compiled-against type. ## Derivation @@ -26,14 +22,11 @@ canonical = f"{cls.__module__}.{cls.__qualname__}|{','.join(sorted('name:type')) fingerprint = int.from_bytes(sha256(canonical.encode()).digest()[:8], "big") ``` -The result is cached per-class in `_fingerprint_cache`, computed once lazily. +Cached per-class in `_fingerprint_cache`, computed once lazily. ## Registry -`Message.__init_subclass__` auto-registers every concrete subclass into -[`MessageType._registry`][cortex.messages.base.MessageType] keyed by -fingerprint. Nothing else to do — decorating your dataclass with -`@dataclass` and inheriting from `Message` is enough. +`Message.__init_subclass__` auto-registers every concrete subclass into [`MessageType._registry`][cortex.messages.base.MessageType] keyed by fingerprint. `@dataclass` + inherit from `Message` is enough. ```python from dataclasses import dataclass @@ -49,23 +42,21 @@ print(hex(JointState.fingerprint())) ## When fingerprints change -The fingerprint is **not stable across edits that touch**: +Not stable across: -- Module path or class name (`cortex.messages.standard.ArrayMessage` renamed - anywhere). +- Module path or class name. - Field names. -- Field *type annotations as spelled* (see the PEP 563 caveat below). +- Field *type annotations as spelled* (see PEP 563 caveat below). -It is stable across: +Stable across: - Adding/removing unrelated classes. - Reordering methods. -- Changing docstrings or default values. +- Docstring or default-value changes. ## Subscriber check -On connect, the subscriber compares the topic's advertised fingerprint against -the one it computed from its message class: +On connect, the subscriber compares the topic's advertised fingerprint against the one it computes from its message class: ```mermaid sequenceDiagram @@ -82,23 +73,14 @@ sequenceDiagram end ``` -!!! warning "Today: mismatch is a warning, not an error" - A fingerprint mismatch currently only logs a warning — see [critique.md](../critique.md). - Downstream decoding will fail hard. Until that is tightened, prefer to - re-exchange type definitions between processes rather than rely on this guard. +!!! warning "Mismatch is a warning, not an error" + A fingerprint mismatch on the async subscriber path logs a warning and continues; decoding will likely fail afterward. Sync subscribers (`mode='sync'`) raise on mismatch. To get the strict behavior on async subscribers, pass `strict_fingerprint=True`. ## PEP 563 caveat -`field.type` may be a **string** (under `from __future__ import annotations`) -or a **real type** otherwise. The canonical string differs in the two cases, -so the same class can fingerprint differently across import environments. - -When defining messages shared between processes, either use the same import -style in both, or rely on the runtime `typing.get_type_hints(cls)` equivalent -once that lands upstream. +`field.type` may be a **string** (under `from __future__ import annotations`) or a **real type** otherwise. The canonical string differs in the two cases, so the same class can fingerprint differently across import environments. Use the same import style on both sides. ## See also - [`cortex.utils.hashing`](../reference/utils/hashing.md) — `compute_fingerprint`, cache helpers - [Message wire format](message-wire-format.md) -- [Critique § code-level issue 13](../critique.md) diff --git a/docs/concepts/message-wire-format.md b/docs/concepts/message-wire-format.md index 48a4ac0..c0b73a1 100644 --- a/docs/concepts/message-wire-format.md +++ b/docs/concepts/message-wire-format.md @@ -1,8 +1,6 @@ # Message wire format -Cortex uses **ZeroMQ multipart messages**. Each published message is a list of -frames rather than a single blob. That lets array payloads ride as raw -contiguous buffers — no copy into a Python `bytes`, no re-copy by ZMQ. +Cortex uses **ZeroMQ multipart messages**. Each publish is a list of frames, not a single blob — array payloads ride as raw contiguous buffers (zero copy on send and receive). ## Frames on the wire @@ -16,11 +14,13 @@ flowchart LR | Frame | Contents | Size | | ------- | ---------------------------- | ------------ | -| 0 | Topic name (UTF-8) | variable | +| 0 | Topic name (UTF-8) — prepended by `Publisher`, not part of `Message.to_frames()` | variable | | 1 | [`MessageHeader`][cortex.messages.base.MessageHeader] | **24 bytes** (3 × u64, big-endian) | | 2 | msgpack-packed ordered field values; arrays replaced by OOB descriptors | small | | 3..N | `np.ndarray.tobytes()` / `tensor.numpy().tobytes()`, contiguous | payload-sized | +`Message.to_frames()` returns frames 1..N (header + metadata + OOB). The Publisher prepends Frame 0 for ZMQ's SUB topic filter. + ## Header layout ``` @@ -29,18 +29,15 @@ offset 0 8 16 24 big-endian throughout ``` -- `fp` — 64-bit message fingerprint, computed from class name and field schema. +- `fp` — 64-bit message fingerprint, computed from class name + field schema. - `ts` — publisher wall-clock in nanoseconds (`time.time_ns()`). - `seq` — per-process, per-message-type monotonic counter. ## Metadata (Frame 2) -Field values are packed **in declaration order** (not by name), so the receiver -reconstructs using the dataclass's cached field tuple. This removes per-message -field-name encoding. +Field values are packed **in declaration order** (not by name); the receiver reconstructs using the dataclass's cached field tuple. Skips per-message field-name encoding. -Arrays and tensors appear in the metadata as small dict stand-ins called -**OOB descriptors**: +Arrays and tensors appear as small **OOB descriptors**: ```json { @@ -51,16 +48,15 @@ Arrays and tensors appear in the metadata as small dict stand-ins called } ``` -The `buffer` index refers into Frames 3..N. The receiver reconstructs: +`buffer` is the index into Frames 3..N. The receiver reconstructs: ```python np.frombuffer(frame.buffer, dtype=np.dtype(desc["dtype"])).reshape(desc["shape"]) ``` -No copy. The resulting array **aliases the ZMQ frame memory** — copy it if you -need ownership or mutability (see [Performance tuning](../guides/performance-tuning.md)). +No copy. The array **aliases the ZMQ frame memory** — copy it if you need ownership or mutability (see [Performance tuning](../guides/performance-tuning.md)). -## Full encode/decode flow +## Encode flow ```mermaid sequenceDiagram @@ -75,22 +71,18 @@ sequenceDiagram S->>E: for each value, walk nested dicts/lists E-->>S: scalar stays inline; array → OOB descriptor + buffer appended S-->>M: (metadata_bytes, [buf0, buf1, ...]) - M-->>Z: [topic, header, metadata, *buffers] + M-->>Z: Publisher sends [topic, header, metadata, *buffers] ``` -## The legacy single-blob path +## Legacy single-blob path -`Message.to_bytes()` / `from_bytes()` / `Message.decode()` still exist. They -pack *everything* into one msgpack blob using `ExtType` for arrays. That path -is retained for tests and opportunistic use; the transport always uses the -multipart path above. +`Message.to_bytes()` / `from_bytes()` / `Message.decode()` still exist. They pack everything into one msgpack blob using `ExtType` for arrays. Used by tests and ad-hoc serialization; the transport always uses multipart. !!! warning "Mismatch trap" - Bytes captured from the wire cannot be fed to `Message.decode()` — the wire - format is multipart, not a single blob. Use `Message.from_frames(frames)`. + Bytes captured from the wire cannot be fed to `Message.decode()` — the wire format is multipart, not a single blob. Use `Message.from_frames(frames)`. ## See also - [Fingerprinting](fingerprinting.md) -- [`cortex.utils.serialization`](../reference/utils/serialization.md) — encoding helpers -- [`cortex.messages.base`](../reference/messages/base.md) — `Message`, `MessageHeader` +- [`cortex.utils.serialization`](../reference/utils/serialization.md) +- [`cortex.messages.base`](../reference/messages/base.md) diff --git a/docs/concepts/transport-and-qos.md b/docs/concepts/transport-and-qos.md index 766b669..628a53c 100644 --- a/docs/concepts/transport-and-qos.md +++ b/docs/concepts/transport-and-qos.md @@ -1,8 +1,6 @@ # Transport & QoS -*Stub — deep dive coming in a later pass.* - -## Current socket settings +## Socket settings | Socket | Option | Value | Notes | | ------------ | ------------- | ----- | ------------------------------------- | @@ -10,24 +8,12 @@ | Publisher PUB | `LINGER` | 0 | Immediate close | | Subscriber SUB | `RCVHWM` | 10 | Oldest messages evicted when full | | Subscriber SUB | `LINGER` | 0 | | -| Daemon REP | `RCVTIMEO` | 1000 ms | Allows Ctrl-C responsiveness | +| Daemon REP | `RCVTIMEO` | 1000 ms | Keeps Ctrl-C responsive | | Daemon REP | `LINGER` | 0 | | -## Today's delivery semantics - -- Publisher uses `zmq.NOBLOCK`: if the send queue is full, the message is - **silently dropped**. -- Subscriber HWM is a ring buffer: old messages are **silently evicted** on - overflow. - -This is fine for best-effort telemetry. It is unsafe for control commands. - -## Planned QoS profiles - -Taking inspiration from DDS, three profiles are enough for most robotics use: +## Delivery semantics -- `best_effort_latest` — conflate; keep only newest (camera frames). -- `reliable_queue` — publisher blocks or errors (control commands). -- `dropping_queue` — current behavior with an exposed drop counter (telemetry). +- Publisher uses `zmq.NOBLOCK`: if the send queue is full, the message is **silently dropped**. +- Subscriber HWM is a ring buffer: oldest messages are **silently evicted** on overflow. -See [critique.md § 4](../critique.md) for rationale. +Fine for best-effort telemetry. Not safe for control commands without an application-level ack — the bytes can be lost on either side. diff --git a/docs/critique.md b/docs/critique.md deleted file mode 100644 index 6677641..0000000 --- a/docs/critique.md +++ /dev/null @@ -1,146 +0,0 @@ -# Cortex Critique - -A bottom-up review of Cortex as it stands today, with a focus on its viability as a communication library for robotics. This complements [design-review.md](design-review.md) with concrete code-level findings and benchmark observations. - -## How Cortex works (bottom-up) - -### 1. Fingerprinting — `utils/hashing.py` - -A message class's identity is a 64-bit integer: - -``` -fingerprint = SHA-256(f"{module}.{qualname}|{','.join(sorted('field:type'))}")[:8] -``` - -- Computed lazily and cached in `_fingerprint_cache`. -- `field.type` is a string when `from __future__ import annotations` is active and a real type otherwise. The fingerprint therefore depends on how the module was imported — fragile for cross-repo use. -- Field ordering is sorted alphabetically in the fingerprint, but the wire layout uses dataclass declaration order. Two classes could theoretically fingerprint identically but interpret the wire differently. - -### 2. Message base — `messages/base.py` - -Each dataclass inheriting `Message` is auto-registered via `__init_subclass__` into `MessageType._registry[fingerprint] = cls`. - -Wire format (multipart transport, what publishers actually use): - -``` -Frame 0: topic bytes (for PUB/SUB filter) -Frame 1: 24-byte header (fingerprint u64, timestamp_ns u64, sequence u64, big-endian) -Frame 2: msgpack of ordered field values with OOB descriptors -Frame 3..N: raw contiguous array buffers (zero-copy) -``` - -There is a second, legacy single-blob path (`to_bytes` / `from_bytes`) that embeds array bytes inside a single msgpack blob using ExtType. It is retained for `Message.decode(...)` and tests, but is not what the transport uses. - -### 3. Serialization — `utils/serialization.py` - -Two strategies coexist: - -- `_msgpack_default` / `_msgpack_ext_hook` (inline): arrays/tensors get packed as msgpack ExtType inside the single blob. Used by the legacy path. -- `_encode_transport_value` / `_decode_transport_value` (out-of-band): each array/tensor is replaced with a tiny dict `{__cortex_oob__: "numpy", buffer: i, dtype, shape}` and its raw bytes are appended as separate ZMQ frames. Reconstruction uses `np.frombuffer(frame.buffer, dtype).reshape(shape)` with no copy. - -After the March 2026 optimizations: zero-copy decode, schema-ordered values (field names no longer repeated per message), and cached field-name tuples. - -### 4. Discovery — `discovery/daemon.py` and `discovery/client.py` - -Single-threaded `zmq.REP` over IPC at `ipc:///tmp/cortex/discovery.sock`. - -- Registry is a plain `dict[str, TopicInfo]`, enforcing one publisher per topic. -- RCVTIMEO=1s so the run loop can poll `_running` for Ctrl-C. -- Commands: REGISTER, UNREGISTER, LOOKUP, LIST, SHUTDOWN. -- Request/response payloads are msgpack. -- Client uses REQ with close-and-recreate on timeout (REQ sockets are stuck after a missed reply). - -### 5. Publisher / Subscriber — `core/publisher.py`, `core/subscriber.py` - -- **Publisher**: binds a `zmq.PUB` at `ipc:///tmp/cortex/topics/__.sock`, registers via the discovery client, publishes multipart `[topic, header, metadata, *buffers]` with `zmq.NOBLOCK`. If the `Node` hands it an async context, it wraps a sync `zmq.Context(self._context)` around the same underlying zmq io threads so publishing stays synchronous. -- **Subscriber**: uses an async context, looks up the topic (optionally waits), connects `zmq.SUB`, sets a topic filter, loops via `AsyncExecutor` doing `recv_multipart(copy=False)` → `Message.from_frames`. - -### 6. Node + Executors — `core/node.py`, `core/executor.py` - -A `Node` owns a shared `zmq.asyncio.Context`, plus lists of publishers, subscribers, and timers. Each timer gets a `RateExecutor(fn, rate_hz)`. `node.run()` creates asyncio tasks for every timer and every callback-subscriber, then `asyncio.gather`. `RateExecutor` uses `perf_counter` plus `asyncio.sleep(max(0, next-now))`. `cortex.run` prefers uvloop on Unix. - -## Benchmark results - -Measured on this machine with the in-repo benchmark suite: - -| Metric | Value | -| ------------------------- | --------------------------- | -| Small-payload latency | mean 556 µs, p99 1075 µs | -| 64KB latency | mean 919 µs, p99 1.4 ms | -| Tiny array throughput | 21.8k msg/s | -| 1MB array throughput | 7.7k msg/s, 8.0 GB/s | -| 4MB array throughput | 2.25k msg/s, 9.4 GB/s | -| 1080p RGB frames | 1422 fps, 8.8 GB/s | -| Raw wire+decode (inproc) | 35 µs roundtrip (4MB array) | - -The delta between the **~35 µs raw wire** and **~550 µs end-to-end** is asyncio scheduling, context-switch between publisher timer and subscriber recv, and Python callback dispatch. Serialization is close to memcpy-bandwidth on large payloads — the OOB transport is pulling its weight. - -## What can be improved - -### Design-level (biggest wins) - -1. **Latency floor is too high for control loops.** ~550 µs mean and ~1.5 ms p99 is dominated by `asyncio` + `zmq.asyncio`, not zmq itself. Control topics should be able to opt into a synchronous thread-plus-`zmq.Poller` receive path targeting <100 µs p99. Async should be the default, not the only option. - -2. **Discovery is a single REQ/REP chokepoint with stop-the-world semantics.** On crashes, stale topic entries are never reclaimed — a crashed publisher's IPC file stays on disk and the registry keeps pointing at a dead socket. Add leases with heartbeats (publisher renews every N seconds; daemon evicts stale entries), or a peer-gossip model where every node beacons presence. The current daemon has no concurrency — one slow client blocks all others. - -3. **One-publisher-per-topic is a hard limit for robotics.** Redundant IMUs, failover, and multi-source fusion are all blocked. The registry should accept N publishers per topic and subscribers should `connect()` to all of them — ZMQ SUB handles fan-in natively. - -4. **No backpressure semantics.** `pub.publish()` is `NOBLOCK` and silently drops on HWM. Subscriber HWM=10 on SUB evicts old messages by default. Robotics needs per-topic QoS profiles similar to DDS: - - `best_effort_latest` — camera frames: drop old, keep newest (`ZMQ_CONFLATE=1`). - - `reliable_queue` — commands: block or surface an error. - - `dropping_queue` — telemetry: current behavior, but with a drop counter. - -5. **No liveness or drop detection.** A subscriber has no way to know the publisher died. Sequence numbers exist in the header but are never checked for gaps. Automatic gap-counting in Subscriber would be gold for debugging. - -6. **Callback execution blocks the receive loop.** A 10 ms callback accumulates on SUB HWM and drops. Receive, decode, and user-callback execution should be decoupled with a bounded work queue and one or more worker coroutines/threads per subscriber. ROS 2 executors have this distinction for a reason. - -7. **Local-only transport in practice.** Addresses are hardcoded `ipc://` paths under `/tmp`. Multi-host robotics (robot ↔ base-station) needs TCP transport in discovery, NIC selection, and topology-aware addressing. - -8. **No shared memory for huge payloads.** At 9 GB/s on 4 MB arrays, every subscriber gets a fresh copy. For multi-subscriber camera or LiDAR fan-out, a shared-memory transport (posix shm + ring buffer + zmq for control-plane notifications) would give true zero-copy. - -### Code-level issues - -9. `publisher.py:91-95` — `zmq.Context(self._context)` creates a shadowed sync context sharing the async context's io threads. Correct, but subtle. `zmq.PUB` is **not thread-safe** — calling `pub.publish()` from multiple asyncio tasks on the same socket is undefined. Needs docs or a lock. - -10. `publisher.py:117-118` — the publisher unlinks any existing socket file on startup. If two publishers on the same host use the same node name + topic, the second silently steals the socket. Should fail loudly. - -11. `subscriber.py:155-160` — fingerprint mismatch logs a warning and proceeds anyway. That is a silent-data-corruption path. Should refuse to connect. - -12. `messages/base.py:109-129` — `_sequence_counter` is **class-level**, shared across every Publisher instance of that message type in the process. Two publishers of `ArrayMessage` interleave sequences — breaking per-topic drop detection. Move it onto the `Publisher`. - -13. `utils/hashing.py:34-38` — `field.type` is a string with PEP 563 and a real type otherwise; the resulting fingerprint differs across import environments. Use `typing.get_type_hints(cls)` consistently. - -14. `discovery/client.py:78-101` — `retries=1` default means zero retries (loop runs once). Fencepost bug. - -15. `core/executor.py:119-147` — `RateExecutor` has both `await asyncio.sleep(0)` inside the loop and `await asyncio.sleep(max(0, dt))` at the bottom. The first is redundant and creates unnecessary wakeups. Catch-up logic silently eats dropped ticks; control loops often need to know. - -16. `discovery/daemon.py:87` — RCVTIMEO=1s means Ctrl-C takes up to 1s to take effect and request throughput is throttled. A `zmq.Poller` with a shutdown PAIR socket gives clean immediate shutdown. - -17. `messages/standard.py:146-150` — `ImageMessage.__post_init__` auto-fill is non-idempotent across deserialization round-trips. Minor. - -18. `discovery/daemon.py:168-177` — same-publisher re-registration is allowed; if its IPC path changed, existing subscribers are never told. Needs a lease or a "changed" notification. - -19. **No CI test for cross-process fingerprint stability.** Given how much safety rides on fingerprints, every standard message type deserves a stored golden fingerprint asserted in CI. - -20. **`from_bytes` vs `from_frames` asymmetry is a trap.** `Message.decode(bytes)` only handles the inline path. If anyone captures bytes from the wire (the multipart path) and calls `decode()`, it will fail silently. Unify the paths or rename `decode`. - -21. **No async publish.** `send_multipart` briefly blocks on HWM/context switch; inside an async timer callback this is a hidden blocking call. An async `publish` variant would help. - -### Schema evolution - -22. No optional fields, no versioning. For long-lived robotics deployments, add: - - field defaults (so fingerprints tolerate missing trailing fields on decode), - - an `msg_schema_version: int = 1` convention, - - eventually, a real wire schema (FlatBuffers, Cap'n Proto, or generated-from-.fbs dataclasses). - -## Summary - -Cortex is a well-built, honest small-system IPC library. The **serialization is genuinely fast** — hitting memcpy-bandwidth on 4 MB arrays with zero-copy OOB frames. The **latency floor (~550 µs p50, ~1.5 ms p99)** is limited by asyncio, not zmq. The **discovery, QoS, liveness, and single-host assumptions** are the real blockers for using this as robotics middleware. - -Recommended path if adopting Cortex for robotics: - -1. Add per-topic QoS profiles with drop counters (1-2 days). -2. Add a synchronous-threaded subscriber option for low-latency control (1 day). -3. Add heartbeats/leases and multi-publisher support to discovery (3-5 days). -4. Add TCP transport and host-aware discovery (2-3 days). -5. Then consider shared memory and schema evolution. diff --git a/docs/gen_ref_pages.py b/docs/gen_ref_pages.py index a6c4f5a..c1e8007 100644 --- a/docs/gen_ref_pages.py +++ b/docs/gen_ref_pages.py @@ -1,47 +1,164 @@ +#!/usr/bin/env python3 """Generate one API reference page per module under ``src/cortex/``. -Executed by ``mkdocs-gen-files`` during the build. Emits: +Writes static Markdown files into ``docs/reference/`` that mkdocstrings +renders into the API reference section of the site. Run this once before +``zensical build``; CI does it automatically via the docs workflow. -- ``reference//.md`` for every non-dunder module, -- ``reference//index.md`` for every ``__init__.py``, -- ``reference/SUMMARY.md`` consumed by ``mkdocs-literate-nav``. +Why a standalone script: ``zensical`` (this project's static site +generator) doesn't run ``mkdocs-gen-files`` / ``mkdocs-literate-nav`` / +``mkdocs-section-index``. So we materialise the reference pages on disk +ourselves. Generated files are committed to keep the docs build +reproducible without re-running the generator. -Keeping this generated means adding a new module needs zero doc edits. +Usage: + python docs/gen_ref_pages.py [--check] + + --check Exit non-zero if the generated tree differs from what is + checked in. """ +from __future__ import annotations +import argparse +import shutil +import sys from pathlib import Path -import mkdocs_gen_files - -# This script lives at ``docs/gen_ref_pages.py`` and is executed by -# mkdocs-gen-files with the mkdocs.yml directory as cwd. Anchor to the -# repo root so the generator finds ``src/cortex`` regardless of cwd. REPO_ROOT = Path(__file__).resolve().parent.parent SRC_ROOT = REPO_ROOT / "src" +DOCS_DIR = Path(__file__).resolve().parent +REFERENCE_DIR = DOCS_DIR / "reference" PACKAGE = "cortex" -nav = mkdocs_gen_files.Nav() -for path in sorted((SRC_ROOT / PACKAGE).rglob("*.py")): - module_path = path.relative_to(SRC_ROOT).with_suffix("") - doc_path = Path("reference", *module_path.parts[1:]).with_suffix(".md") - parts = tuple(module_path.parts) +def collect_modules() -> list[tuple[Path, str]]: + """Walk the source tree and return (doc_path, identifier) pairs. + + ``doc_path`` is the .md path relative to ``docs/reference/``. + ``identifier`` is the dotted module path passed to mkdocstrings. + """ + out: list[tuple[Path, str]] = [] + for path in sorted((SRC_ROOT / PACKAGE).rglob("*.py")): + module_path = path.relative_to(SRC_ROOT).with_suffix("") + doc_path = Path(*module_path.parts[1:]).with_suffix(".md") + parts = tuple(module_path.parts) + + if parts[-1] == "__init__": + parts = parts[:-1] + doc_path = doc_path.with_name("index.md") if doc_path.parts else Path( + "index.md") + elif parts[-1].startswith("_"): + continue + + identifier = ".".join(parts) if parts else PACKAGE + out.append((doc_path, identifier)) + return out + + +def render(modules: list[tuple[Path, str]]) -> dict[Path, str]: + """Render every (doc_path -> content) the reference tree needs. + + Module pages get a heading + the mkdocstrings directive. The top-level + index.md additionally lists every submodule for navigation. + """ + pages: dict[Path, str] = {} + + # Per-module pages. + for doc_path, identifier in modules: + pages[doc_path] = f"# `{identifier}`\n\n::: {identifier}\n" + + # Replace the top-level index.md with a hand-shaped index. We avoid + # running mkdocstrings on the top-level `cortex` package here because + # its __init__ pulls in zmq/numpy/msgpack — fine inside a per-module + # page, but it would inflate the index unnecessarily. + top_lines = [ + "# API reference", + "", + "Auto-generated from the `cortex` package source. Re-run " + "`python docs/gen_ref_pages.py` when modules are added or renamed.", + "", + ] + # Group entries by their top-level subpackage (core / discovery / messages / + # utils) so the listing is scannable. + by_subpackage: dict[str, list[tuple[Path, str]]] = {} + for doc_path, identifier in modules: + if identifier == PACKAGE: + continue + parts = identifier.split(".") + sub = parts[1] if len(parts) >= 2 else PACKAGE + by_subpackage.setdefault(sub, []).append((doc_path, identifier)) + + for sub in sorted(by_subpackage): + top_lines.append(f"## `cortex.{sub}`") + top_lines.append("") + for doc_path, identifier in sorted(by_subpackage[sub]): + rel_link = doc_path.as_posix() + top_lines.append(f"- [`{identifier}`]({rel_link})") + top_lines.append("") + pages[Path("index.md")] = "\n".join(top_lines) + + return pages + + +def write_pages(pages: dict[Path, str]) -> None: + if REFERENCE_DIR.exists(): + shutil.rmtree(REFERENCE_DIR) + REFERENCE_DIR.mkdir(parents=True) + for rel_path, content in pages.items(): + out = REFERENCE_DIR / rel_path + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(content) + + +def check_pages(pages: dict[Path, str]) -> int: + drift: list[str] = [] + expected_paths = {Path(p) for p in pages} + actual_paths = { + p.relative_to(REFERENCE_DIR) + for p in REFERENCE_DIR.rglob("*.md") + } if REFERENCE_DIR.exists() else set() + + for missing in sorted(expected_paths - actual_paths): + drift.append(f"missing: docs/reference/{missing.as_posix()}") + for extra in sorted(actual_paths - expected_paths): + drift.append(f"unexpected: docs/reference/{extra.as_posix()}") + for rel in sorted(expected_paths & actual_paths): + on_disk = (REFERENCE_DIR / rel).read_text() + if on_disk != pages[rel]: + drift.append(f"out of date: docs/reference/{rel.as_posix()}") + + if drift: + print("error: docs/reference/ is out of sync with src/cortex/:", file=sys.stderr) + for line in drift: + print(f" - {line}", file=sys.stderr) + print( + "Run `python docs/gen_ref_pages.py` and commit the result.", + file=sys.stderr, + ) + return 1 + print(f"ok: docs/reference/ matches src/cortex/ ({len(pages)} pages)") + return 0 + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument( + "--check", + action="store_true", + help="Exit non-zero if the generated tree differs from disk.", + ) + args = ap.parse_args() - if parts[-1] == "__init__": - parts = parts[:-1] - doc_path = doc_path.with_name("index.md") - elif parts[-1].startswith("_"): - continue + modules = collect_modules() + pages = render(modules) - nav_parts = parts[1:] if parts[1:] else ("cortex",) - nav[nav_parts] = doc_path.relative_to("reference").as_posix() + if args.check: + return check_pages(pages) - identifier = ".".join(parts) if parts else PACKAGE - with mkdocs_gen_files.open(doc_path, "w") as f: - f.write(f"# `{identifier}`\n\n") - f.write(f"::: {identifier}\n") + write_pages(pages) + print(f"wrote {len(pages)} pages under {REFERENCE_DIR}") + return 0 - mkdocs_gen_files.set_edit_path(doc_path, path.relative_to(REPO_ROOT)) -with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as f: - f.writelines(nav.build_literate_nav()) +if __name__ == "__main__": + sys.exit(main()) diff --git a/docs/getting-started/discovery-daemon.md b/docs/getting-started/discovery-daemon.md index 518bc55..4ab316f 100644 --- a/docs/getting-started/discovery-daemon.md +++ b/docs/getting-started/discovery-daemon.md @@ -1,24 +1,22 @@ -# Running the Discovery Daemon +# Discovery daemon -The discovery daemon is a lightweight REP service that maintains the registry -of active topics. Publishers register on startup; subscribers look up the -endpoint and connect directly. +A REP service maintaining the registry of active topics. Publishers register on startup; subscribers look up the endpoint and connect directly. ## Start -=== "As a script" +=== "Script" ```bash cortex-discovery ``` -=== "As a module" +=== "Module" ```bash python -m cortex.discovery.daemon ``` -=== "As a systemd service" +=== "systemd" ```ini title="/etc/systemd/system/cortex-discovery.service" [Unit] @@ -35,7 +33,7 @@ endpoint and connect directly. WantedBy=multi-user.target ``` -## Command-line options +## Flags | Flag | Default | Description | | ------------- | -------------------------------------- | ------------------------------- | @@ -56,14 +54,10 @@ stateDiagram-v2 ## Troubleshooting **"Address already in use"** -: Another daemon (or a stale socket file) is holding the path. - `rm /tmp/cortex/discovery.sock` and restart. +: Another daemon or a stale socket file is holding the path. `rm /tmp/cortex/discovery.sock` and restart. **Subscribers time out looking up topics** -: Daemon not running, or publisher failed to register. Run with - `--log-level DEBUG` and watch for REGISTER / LOOKUP lines. +: Daemon not running, or the publisher failed to register. Run with `--log-level DEBUG` and watch for REGISTER / LOOKUP lines. **Daemon crash leaves stale entries** -: Today, entries are only removed on explicit UNREGISTER. A crashed - publisher's topic stays in the registry pointing at a dead socket. - Restarting the daemon clears all state. +: Entries are only removed on explicit UNREGISTER. A crashed publisher's topic stays in the registry pointing at a dead socket. Restarting the daemon clears all state. diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 3ea43a0..0e19a1e 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -3,10 +3,10 @@ ## Requirements - Python **3.10+** -- Linux or macOS (Windows works but without `uvloop`) +- Linux or macOS (Windows works without `uvloop`) - ZeroMQ shared library (bundled via `pyzmq`) -## Install from source +## From source ```bash git clone https://github.com/sudoRicheek/cortex.git @@ -16,20 +16,10 @@ pip install -e ".[dev]" ## Optional extras -=== "PyTorch support" - - ```bash - pip install -e ".[torch]" - ``` - - Enables [`TensorMessage`][cortex.messages.standard.TensorMessage] and - torch-aware serialization paths. - -=== "Everything" - - ```bash - pip install -e ".[all]" - ``` +```bash +pip install -e ".[torch]" # TensorMessage + torch-aware serialization +pip install -e ".[all]" # everything +``` ## Verify @@ -38,5 +28,4 @@ import cortex print(cortex.__version__) ``` -If that prints a version string, you're ready. Continue to the -[Quickstart](quickstart.md). +If that prints a version string, continue to the [Quickstart](quickstart.md). diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index 470f12e..b553a6f 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -1,22 +1,22 @@ # Quickstart -A three-terminal pub/sub loop in under two minutes. +A three-terminal pub/sub loop. -## 1. Start the discovery daemon +## 1. Discovery daemon ```bash cortex-discovery ``` -Leave it running. This is the single service that maps topic names to -IPC endpoints. +Leave it running. This is the service that maps topic names to IPC endpoints. ## 2. Publisher ```python title="pub.py" import numpy as np import cortex -from cortex import Node, ArrayMessage +from cortex import Node +from cortex.messages.standard import ArrayMessage class SensorNode(Node): @@ -52,8 +52,9 @@ python pub.py ```python title="sub.py" import cortex -from cortex import Node, ArrayMessage +from cortex import Node from cortex.messages.base import MessageHeader +from cortex.messages.standard import ArrayMessage async def on_data(msg: ArrayMessage, header: MessageHeader): @@ -82,7 +83,7 @@ if __name__ == "__main__": python sub.py ``` -## What just happened +## What happened ```mermaid sequenceDiagram @@ -100,5 +101,4 @@ sequenceDiagram end ``` -See [Concepts → Architecture](../concepts/architecture.md) for the end-to-end -picture, or jump into a [custom message tutorial](../tutorials/custom-messages.md). +Next: [Concepts → Architecture](../concepts/architecture.md) or [Tutorials → Custom messages](../tutorials/custom-messages.md). diff --git a/docs/guides/benchmarks.md b/docs/guides/benchmarks.md index f7fad29..f4b0f10 100644 --- a/docs/guides/benchmarks.md +++ b/docs/guides/benchmarks.md @@ -1,35 +1,27 @@ # Benchmarks -Cortex ships an in-repo benchmark suite at [`benchmarks/`](https://github.com/sudoRicheek/cortex/tree/main/benchmarks). +Suite at [`benchmarks/`](https://github.com/sudoRicheek/cortex/tree/main/benchmarks). ## Run ```bash -# Terminal 1 +# terminal 1 cortex-discovery -# Terminal 2 +# terminal 2 python benchmarks/bench_all.py --output results.json ``` Individual benchmarks: -- `benchmarks/bench_latency.py` — one-way publisher→subscriber latency. -- `benchmarks/bench_throughput.py` — messages/sec and MB/sec. -- `benchmarks/bench_all.py` — full matrix with summary and optional JSON dump. +- `bench_latency.py` — one-way publisher→subscriber latency (async). +- `bench_latency_sync.py` — raw sync zmq baseline (no asyncio). +- `bench_latency_inproc.py` — in-process pub + sync sub + a CPU-bound asyncio neighbour, to expose GIL contention. +- `bench_throughput.py` — messages/sec and MB/sec. +- `bench_all.py` — full matrix; JSON output via `--output`. ## Reading results -- `p99` is what matters for real-time-ish workloads; `mean` can hide jitter. -- For array workloads, `MB/s` approaching memcpy bandwidth is a good sign - that zero-copy transport is working. -- Serialization overhead via `inproc` sockets with `copy=False` is reported - separately — that isolates the encode/decode path from the network path. - -## Tips - -- Pin publisher and subscriber to separate cores for stable latency numbers. -- Disable Turbo-Boost / set CPU governor to `performance` for reproducible - runs. -- Always measure with the discovery daemon also running (it is off the hot - path but can steal a little cache). +- **p99** is what matters for real-time workloads; the mean hides jitter. +- For array workloads, `MB/s` approaching memcpy bandwidth means zero-copy transport is working. +- Serialization overhead via `inproc` sockets with `copy=False` is reported separately to isolate encode/decode from the network path. diff --git a/docs/guides/debugging.md b/docs/guides/debugging.md index 881e5e9..5c621f8 100644 --- a/docs/guides/debugging.md +++ b/docs/guides/debugging.md @@ -2,9 +2,7 @@ ## Subscriber hangs on startup -Most likely: the daemon is not running, or the topic name is mistyped. -`DiscoveryClient.wait_for_topic_async` polls every 500 ms until the topic -appears or the timeout fires. +The daemon isn't running or the topic name is mistyped. `DiscoveryClient.wait_for_topic_async` polls every 500 ms until the topic appears or the timeout fires. ```bash cortex-discovery --log-level DEBUG @@ -12,40 +10,30 @@ cortex-discovery --log-level DEBUG Watch for `LOOKUP topic: /x -> NOT FOUND`. -## Publisher "works" but subscriber receives nothing +## Publisher "works" but subscriber gets nothing -ZMQ PUB drops messages for which no matching SUB is connected yet. If your -publisher starts first and publishes immediately, the first few messages are -lost — this is the classic ZMQ slow-joiner problem. +Classic ZMQ slow-joiner problem: PUB drops messages for which no SUB is connected yet. If the publisher starts first and publishes immediately, the first few messages are lost. -Workarounds: +Fixes: -- Have the publisher wait briefly after bind before publishing the first message. -- Have the subscriber wait-for-topic (the default) so it comes up after the - publisher registered. +- Make the subscriber start first (or use `wait_for_topic=True`, the default). +- Have the publisher sleep briefly after bind before its first publish. -## Stale `/tmp/cortex/topics/*.sock` files +## Stale `/tmp/cortex/topics/*.sock` -If a publisher exits uncleanly, its IPC socket file remains. Cortex's -`Publisher._setup_socket` unlinks any existing file at the same path on the -**next bind** — so restarting the publisher fixes it. Otherwise: +If a publisher exits uncleanly, its IPC socket file remains. `Publisher._setup_socket` unlinks any existing file at the same path on the next bind — so restarting the publisher fixes it. Manual cleanup: ```bash rm /tmp/cortex/topics/.sock ``` -## Daemon state survives restarts — but doesn't +## Daemon state doesn't survive restart -The registry is **in-memory**. Restarting the daemon wipes all state; -publishers do not auto-re-register today. Restart your publishers after -restarting the daemon. +The registry is in-memory. Restarting the daemon wipes everything; publishers don't auto-re-register. Restart your publishers after restarting the daemon. -## Fingerprint mismatch warning +## Fingerprint mismatch -If you see -`Message type mismatch for /x: expected FooMessage, got BarMessage` — -the topic was registered with a different message class. Either rename the -topic or align the classes. +`Message type mismatch for /x: expected FooMessage, got BarMessage` — the topic was registered with a different class. Either rename the topic or align the classes. The async path logs and continues; use `strict_fingerprint=True` to raise instead. ## Debug logging @@ -54,5 +42,4 @@ import logging logging.basicConfig(level=logging.DEBUG) ``` -Cortex uses standard `logging`. Interesting loggers: `cortex.publisher`, -`cortex.subscriber`, `cortex.node`, `cortex.discovery`, `cortex.discovery.client`. +Useful loggers: `cortex.publisher`, `cortex.subscriber`, `cortex.node`, `cortex.discovery`, `cortex.discovery.client`. diff --git a/docs/guides/performance-tuning.md b/docs/guides/performance-tuning.md index ce8828f..da31bde 100644 --- a/docs/guides/performance-tuning.md +++ b/docs/guides/performance-tuning.md @@ -1,37 +1,35 @@ # Performance tuning -Current measured numbers on the repo's benchmark suite (single workstation): +Measured on the repo's benchmark suite, single workstation: | Workload | Throughput / latency | | --------------------- | ------------------------------- | | Small payload latency | mean 556 µs, p99 1075 µs | -| 1MB array throughput | 7.7k msg/s, 8.0 GB/s | -| 4MB array throughput | 2.25k msg/s, 9.4 GB/s | +| 1 MB array throughput | 7.7k msg/s, 8.0 GB/s | +| 4 MB array throughput | 2.25k msg/s, 9.4 GB/s | | 1080p RGB | 1422 fps, 8.8 GB/s | -See [Benchmarks guide](benchmarks.md) to reproduce. +See [Benchmarks](benchmarks.md) to reproduce. ## Copy-on-use -Decoded NumPy arrays **alias the ZMQ frame memory**. That is what makes -large-payload throughput close to memcpy bandwidth — but it means: +Decoded NumPy arrays **alias the ZMQ frame memory** — that's how large-payload throughput hits memcpy bandwidth. Consequence: -- If you intend to mutate the array, `arr = arr.copy()` first. -- If you intend to hold the array past the callback, copy it first. +- Mutate the array? `arr = arr.copy()` first. +- Hold the array past the callback? Copy first. ## Queue sizing -Per-socket HWM defaults to 10. Increase `queue_size` on high-rate producers -whose subscribers are known to be slow — but remember that ZMQ drops silently -at the HWM. +Per-socket HWM defaults to 10. Increase `queue_size` on high-rate producers whose subscribers are slow — but remember ZMQ drops silently at the HWM. -## When to prefer the inline path +## When to prefer the single-blob path -Single tiny messages (primitives only, < 1 KB) see no benefit from multipart. -The inline `to_bytes` path is still fine there. Publishers always use -multipart today. +Tiny messages (primitives only, < 1 KB) see no benefit from multipart. The inline `to_bytes` path is fine there. The transport always uses multipart today. ## uvloop -Installed by default on Unix. Drops tail latency on high-rate small messages -noticeably. No action needed. +Installed by default on Unix. Drops tail latency on high-rate small messages. + +## Sync subscriber mode + +For control loops needing sub-100 µs p99, see [Sync subscriber mode](sync-mode.md). Bypasses asyncio + `zmq.asyncio` on the receive path. diff --git a/docs/guides/sync-mode.md b/docs/guides/sync-mode.md new file mode 100644 index 0000000..a5c47e4 --- /dev/null +++ b/docs/guides/sync-mode.md @@ -0,0 +1,117 @@ +# Sync subscriber & publisher mode + +`mode='sync'` opts a subscriber or publisher out of asyncio. Use it for control loops where p99 jitter — not throughput — is what matters. + +## When to use which + +| Use case | Mode | Why | +|---|---|---| +| Camera frames, point clouds, logs | `async` | Throughput-bound; latency floor irrelevant. | +| Dashboards, HTTP / WebSocket streaming | `async` | Composes with `asyncio` servers. | +| Closed-loop control at > 100 Hz | `sync` | Sub-100 µs p99 reachable; jitter matters. | +| Robot teleop commands | `sync` | Operator-perceived latency. | +| Heartbeats, registration | `async` | Once per second — not worth the complexity. | + +If in doubt, start with `async`. + +## Sync subscriber + +```python +def on_cmd(msg, header): # plain function, NOT async def + apply_command(msg) + +node.create_subscriber( + topic_name="/cmd/wheel", + message_type=WheelCommand, + callback=on_cmd, + mode="sync", + queue_size=1, # latest-wins (default) + cpu_affinity=[3], # optional, Linux + sched_priority=20, # optional, requires CAP_SYS_NICE +) +``` + +Construction blocks on a discovery lookup (`TimeoutError` if the topic never registers). The receive thread starts when `node.run()` is called and joins cleanly on `node.close()`. + +### Contracts + +- **Synchronous callback only.** The receive loop runs on a dedicated OS thread; passing an `async def` raises `TypeError` at construction time. +- **Strict fingerprint check.** A topic/type mismatch raises `MessageFingerprintError` instead of logging a warning. Silent type confusion is unacceptable for control topics. +- **Latest-wins by default.** `queue_size=1` so the receiver drops old messages on overflow and always sees the freshest command. Override if backlog tolerance matters more than freshness. +- **Independent zmq context.** No shared IO threads with the asyncio context — a stuck callback can't back-pressure the rest of the node. + +### Determinism knobs + +- `cpu_affinity=[N, ...]` — pin the receive thread to specific CPUs (Linux). Most useful on busy systems where the kernel migrates the thread between cores. +- `sched_priority=N` — run under `SCHED_FIFO` at priority `N` (Linux, requires `CAP_SYS_NICE`). Reach for this when `cpu_affinity` alone isn't enough — typically when other RT-priority work shares the machine. Failure to set is logged; the thread keeps running on the default scheduler. + +Both are no-ops on platforms that don't support them. + +## Sync publisher + +The default publisher shares the node's `zmq.asyncio.Context` (with a sync shadow). For publishers driven from a non-asyncio thread — a tight C-extension loop, or the body of a sync subscriber callback — pass `mode='sync'`: + +```python +pub = node.create_publisher( + topic_name="/cmd/wheel", + message_type=WheelCommand, + mode="sync", +) +``` + +Gives the publisher its own independent `zmq.Context`. `publish()` becomes a direct syscall on the calling thread, no asyncio IO threads in the path. The `Node` tracks the extra context and terms it on close. + +!!! danger "Still not thread-safe" + `zmq.PUB` is not safe across threads. Only call `publish()` from one thread per Publisher. + +## Free-threaded CPython + +The sync floor is what you get from raw zmq + `zmq.Poller`. To clear it further when sharing a process with a busy asyncio loop, run on `python3.14t` (free-threaded build, PEP 779) with the GIL actually disabled: + +```bash +PYTHON_GIL=0 python3.14t your_node.py +``` + +`msgpack._cmsgpack` re-enables the GIL on free-threaded builds, so `PYTHON_GIL=0` is the documented override. Cortex emits a one-line runtime hint when the configuration is suboptimal. + +## Tracing the budget + +Set `CORTEX_TRACE_LATENCY=N` to record up to N per-stage samples (recv, decode, callback) in ns: + +```python +import os +os.environ["CORTEX_TRACE_LATENCY"] = "10000" + +# ... run workload ... + +from cortex.utils import tracing +samples = tracing.snapshot() +``` + +No-op when the env var is unset. + +## Measured numbers (Linux x86_64, 500 Hz, IPC, 256 B) + +Wire floor (raw sync zmq + Poller, separate processes): + +| | p50 | p99 | +|---|---|---| +| Sync | 190–360 µs | 430–900 µs | + +The interesting number is same-process contention: a sync subscriber sharing a process with a CPU-bound asyncio loop. With moderate load (4 ms burn / 5 ms period, ~80 % of one core): + +| Runtime | p50 | p99 | max | +|---|---|---|---| +| 3.14 (GIL) | 530 µs | 1180 µs | **4910 µs** | +| 3.14t (free-threaded, `PYTHON_GIL=0`) | 730 µs | 1130 µs | **1520 µs** | + +Median is slightly higher on the free-threaded build (single-thread CPython is slower without biased-locking) but **max is 3× tighter**. Control loops live or die by p99.9 / max, not median. + +Reproduce with `benchmarks/bench_latency_inproc.py`. See [Benchmarks](benchmarks.md) for the full matrix. + +## Recap + +- `mode="async"` for telemetry, throughput, and integration with asyncio code. +- `mode="sync"` for control loops where jitter dominates correctness. +- `python3.14t` to clear sub-100 µs under in-process contention. +- Sync callbacks must be `def`, not `async def`. Caught at construction. diff --git a/docs/index.md b/docs/index.md index a0a2a64..94f70b3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,36 +1,34 @@ # Cortex -**A lightweight Python framework for inter-process communication over ZeroMQ.** - -Cortex is a pub/sub layer designed to feel obvious. Nodes publish typed messages on named topics; subscribers receive them via async callbacks. A tiny discovery daemon tells subscribers where to connect. Native support for NumPy arrays and PyTorch tensors keeps robotics- and ML-shaped payloads fast. +Lightweight Python pub/sub over ZeroMQ IPC. Typed messages, automatic topic discovery, zero-copy frames for NumPy and PyTorch.
- :material-rocket-launch-outline: **[Getting started](getting-started/quickstart.md)** - Install, start the daemon, publish your first message in under two minutes. + Install, start the daemon, publish your first message. - :material-book-open-variant: **[Concepts](concepts/architecture.md)** - How the wire format, fingerprinting, discovery handshake, and async execution fit together. + Wire format, fingerprinting, discovery handshake, async execution. - :material-puzzle-outline: **[Components](components/messages.md)** - Deep dives into the Messages, Discovery, and Core modules. + Messages, discovery, publisher/subscriber, node + executors. - :material-api: **[API reference](reference/index.md)** - Auto-generated from the source. Always matches the code on `main`. + Auto-generated from source.
-## Highlights +## What you get -- **Publisher / Subscriber pattern** over ZeroMQ PUB/SUB sockets. -- **Discovery service** for automatic topic → endpoint resolution. -- **IPC transport** with zero-copy frames for large NumPy / PyTorch payloads. -- **64-bit fingerprint hashing** for fast message-type identification. -- **uvloop-backed async** on Linux/macOS for lower tail latency. +- PUB/SUB over ZeroMQ IPC. +- A discovery daemon for topic → endpoint resolution. +- 64-bit message fingerprints; strict type matching. +- Zero-copy out-of-band frames for NumPy arrays and PyTorch tensors. +- Async (`asyncio` + `uvloop`) and synchronous subscriber modes. ## Minimal example @@ -39,7 +37,8 @@ Cortex is a pub/sub layer designed to feel obvious. Nodes publish typed messages ```python import numpy as np import cortex - from cortex import Node, ArrayMessage + from cortex import Node + from cortex.messages.standard import ArrayMessage class Cam(Node): @@ -59,8 +58,9 @@ Cortex is a pub/sub layer designed to feel obvious. Nodes publish typed messages ```python import cortex - from cortex import Node, ArrayMessage + from cortex import Node from cortex.messages.base import MessageHeader + from cortex.messages.standard import ArrayMessage async def on_frame(msg: ArrayMessage, header: MessageHeader): @@ -76,8 +76,14 @@ Cortex is a pub/sub layer designed to feel obvious. Nodes publish typed messages cortex.run(Viewer().run()) ``` -## Project status +Run `cortex-discovery` once in the background, then run both files. + +## C++ implementation + +A standalone C++ port of the wire format (header, msgpack metadata, OOB buffers, discovery client, pub/sub) lives in [`cortex_wire_cpp/`](https://github.com/sudoRicheek/cortex/tree/main/cortex_wire_cpp). It's pure CMake, no ROS dependency, and exists for native consumers that want sub-millisecond latency without Python in the loop. See the [`README`](https://github.com/sudoRicheek/cortex/blob/main/cortex_wire_cpp/README.md) for the layout and [`DOCS.md`](https://github.com/sudoRicheek/cortex/blob/main/cortex_wire_cpp/DOCS.md) for the API. + +A ROS 2 bridge built on top is at [`ros2_bridge/`](https://github.com/sudoRicheek/cortex/tree/main/ros2_bridge). + +## Scope -Cortex targets single-host process graphs today. See [design-review.md](design-review.md) -and [critique.md](critique.md) for an honest account of current limits and the -roadmap toward multi-host robotics use. +Cortex targets single-host process graphs. Multi-host is not supported today. diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index b72ae17..936feb9 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -76,12 +76,6 @@ markdown_extensions: plugins: - search - - section-index - - literate-nav: - nav_file: SUMMARY.md - - gen-files: - scripts: - - gen_ref_pages.py - mkdocstrings: default_handler: python handlers: @@ -129,10 +123,10 @@ nav: - tutorials/pytorch-tensors.md - Guides: - guides/performance-tuning.md + - guides/sync-mode.md - guides/benchmarks.md - guides/debugging.md - - Critique: critique.md - - API reference: reference/ + - API reference: reference/index.md extra: social: diff --git a/docs/reference/core/executor.md b/docs/reference/core/executor.md new file mode 100644 index 0000000..7873d31 --- /dev/null +++ b/docs/reference/core/executor.md @@ -0,0 +1,3 @@ +# `cortex.core.executor` + +::: cortex.core.executor diff --git a/docs/reference/core/index.md b/docs/reference/core/index.md new file mode 100644 index 0000000..77f033b --- /dev/null +++ b/docs/reference/core/index.md @@ -0,0 +1,3 @@ +# `cortex.core` + +::: cortex.core diff --git a/docs/reference/core/node.md b/docs/reference/core/node.md new file mode 100644 index 0000000..297ad82 --- /dev/null +++ b/docs/reference/core/node.md @@ -0,0 +1,3 @@ +# `cortex.core.node` + +::: cortex.core.node diff --git a/docs/reference/core/publisher.md b/docs/reference/core/publisher.md new file mode 100644 index 0000000..023678e --- /dev/null +++ b/docs/reference/core/publisher.md @@ -0,0 +1,3 @@ +# `cortex.core.publisher` + +::: cortex.core.publisher diff --git a/docs/reference/core/subscriber.md b/docs/reference/core/subscriber.md new file mode 100644 index 0000000..4838953 --- /dev/null +++ b/docs/reference/core/subscriber.md @@ -0,0 +1,3 @@ +# `cortex.core.subscriber` + +::: cortex.core.subscriber diff --git a/docs/reference/core/subscriber_base.md b/docs/reference/core/subscriber_base.md new file mode 100644 index 0000000..d4b5545 --- /dev/null +++ b/docs/reference/core/subscriber_base.md @@ -0,0 +1,3 @@ +# `cortex.core.subscriber_base` + +::: cortex.core.subscriber_base diff --git a/docs/reference/core/sync_subscriber.md b/docs/reference/core/sync_subscriber.md new file mode 100644 index 0000000..e7419d0 --- /dev/null +++ b/docs/reference/core/sync_subscriber.md @@ -0,0 +1,3 @@ +# `cortex.core.sync_subscriber` + +::: cortex.core.sync_subscriber diff --git a/docs/reference/core/types.md b/docs/reference/core/types.md new file mode 100644 index 0000000..52240c9 --- /dev/null +++ b/docs/reference/core/types.md @@ -0,0 +1,3 @@ +# `cortex.core.types` + +::: cortex.core.types diff --git a/docs/reference/discovery/client.md b/docs/reference/discovery/client.md new file mode 100644 index 0000000..7f3d355 --- /dev/null +++ b/docs/reference/discovery/client.md @@ -0,0 +1,3 @@ +# `cortex.discovery.client` + +::: cortex.discovery.client diff --git a/docs/reference/discovery/daemon.md b/docs/reference/discovery/daemon.md new file mode 100644 index 0000000..4c26bfd --- /dev/null +++ b/docs/reference/discovery/daemon.md @@ -0,0 +1,3 @@ +# `cortex.discovery.daemon` + +::: cortex.discovery.daemon diff --git a/docs/reference/discovery/index.md b/docs/reference/discovery/index.md new file mode 100644 index 0000000..bb29e67 --- /dev/null +++ b/docs/reference/discovery/index.md @@ -0,0 +1,3 @@ +# `cortex.discovery` + +::: cortex.discovery diff --git a/docs/reference/discovery/protocol.md b/docs/reference/discovery/protocol.md new file mode 100644 index 0000000..8d225ff --- /dev/null +++ b/docs/reference/discovery/protocol.md @@ -0,0 +1,3 @@ +# `cortex.discovery.protocol` + +::: cortex.discovery.protocol diff --git a/docs/reference/index.md b/docs/reference/index.md new file mode 100644 index 0000000..8bff6fa --- /dev/null +++ b/docs/reference/index.md @@ -0,0 +1,37 @@ +# API reference + +Auto-generated from the `cortex` package source. Re-run `python docs/gen_ref_pages.py` when modules are added or renamed. + +## `cortex.core` + +- [`cortex.core.executor`](core/executor.md) +- [`cortex.core`](core/index.md) +- [`cortex.core.node`](core/node.md) +- [`cortex.core.publisher`](core/publisher.md) +- [`cortex.core.subscriber`](core/subscriber.md) +- [`cortex.core.subscriber_base`](core/subscriber_base.md) +- [`cortex.core.sync_subscriber`](core/sync_subscriber.md) +- [`cortex.core.types`](core/types.md) + +## `cortex.discovery` + +- [`cortex.discovery.client`](discovery/client.md) +- [`cortex.discovery.daemon`](discovery/daemon.md) +- [`cortex.discovery`](discovery/index.md) +- [`cortex.discovery.protocol`](discovery/protocol.md) + +## `cortex.messages` + +- [`cortex.messages.base`](messages/base.md) +- [`cortex.messages`](messages/index.md) +- [`cortex.messages.standard`](messages/standard.md) + +## `cortex.utils` + +- [`cortex.utils.hashing`](utils/hashing.md) +- [`cortex.utils`](utils/index.md) +- [`cortex.utils.logging`](utils/logging.md) +- [`cortex.utils.loop`](utils/loop.md) +- [`cortex.utils.runtime`](utils/runtime.md) +- [`cortex.utils.serialization`](utils/serialization.md) +- [`cortex.utils.tracing`](utils/tracing.md) diff --git a/docs/reference/messages/base.md b/docs/reference/messages/base.md new file mode 100644 index 0000000..b01b1f1 --- /dev/null +++ b/docs/reference/messages/base.md @@ -0,0 +1,3 @@ +# `cortex.messages.base` + +::: cortex.messages.base diff --git a/docs/reference/messages/index.md b/docs/reference/messages/index.md new file mode 100644 index 0000000..f683320 --- /dev/null +++ b/docs/reference/messages/index.md @@ -0,0 +1,3 @@ +# `cortex.messages` + +::: cortex.messages diff --git a/docs/reference/messages/standard.md b/docs/reference/messages/standard.md new file mode 100644 index 0000000..6530e04 --- /dev/null +++ b/docs/reference/messages/standard.md @@ -0,0 +1,3 @@ +# `cortex.messages.standard` + +::: cortex.messages.standard diff --git a/docs/reference/utils/hashing.md b/docs/reference/utils/hashing.md new file mode 100644 index 0000000..e5cde8e --- /dev/null +++ b/docs/reference/utils/hashing.md @@ -0,0 +1,3 @@ +# `cortex.utils.hashing` + +::: cortex.utils.hashing diff --git a/docs/reference/utils/index.md b/docs/reference/utils/index.md new file mode 100644 index 0000000..2691bd8 --- /dev/null +++ b/docs/reference/utils/index.md @@ -0,0 +1,3 @@ +# `cortex.utils` + +::: cortex.utils diff --git a/docs/reference/utils/logging.md b/docs/reference/utils/logging.md new file mode 100644 index 0000000..0694a9c --- /dev/null +++ b/docs/reference/utils/logging.md @@ -0,0 +1,3 @@ +# `cortex.utils.logging` + +::: cortex.utils.logging diff --git a/docs/reference/utils/loop.md b/docs/reference/utils/loop.md new file mode 100644 index 0000000..5b5a005 --- /dev/null +++ b/docs/reference/utils/loop.md @@ -0,0 +1,3 @@ +# `cortex.utils.loop` + +::: cortex.utils.loop diff --git a/docs/reference/utils/runtime.md b/docs/reference/utils/runtime.md new file mode 100644 index 0000000..9775b82 --- /dev/null +++ b/docs/reference/utils/runtime.md @@ -0,0 +1,3 @@ +# `cortex.utils.runtime` + +::: cortex.utils.runtime diff --git a/docs/reference/utils/serialization.md b/docs/reference/utils/serialization.md new file mode 100644 index 0000000..27890d6 --- /dev/null +++ b/docs/reference/utils/serialization.md @@ -0,0 +1,3 @@ +# `cortex.utils.serialization` + +::: cortex.utils.serialization diff --git a/docs/reference/utils/tracing.md b/docs/reference/utils/tracing.md new file mode 100644 index 0000000..37ebb74 --- /dev/null +++ b/docs/reference/utils/tracing.md @@ -0,0 +1,3 @@ +# `cortex.utils.tracing` + +::: cortex.utils.tracing diff --git a/docs/reference/zzz_extra.md b/docs/reference/zzz_extra.md new file mode 100644 index 0000000..0f22871 --- /dev/null +++ b/docs/reference/zzz_extra.md @@ -0,0 +1 @@ +extra diff --git a/docs/site/404.html b/docs/site/404.html deleted file mode 100644 index d021792..0000000 --- a/docs/site/404.html +++ /dev/null @@ -1,1502 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - Cortex - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - -
- - -
- -
- - - - - - - - - -
-
- - - -
-
-
- - - - - - - -
-
-
- - - -
-
- - - - - - -
- - - - -
-
-
- - - -
- -
- -

404 - Not found

- -
-
- - - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/site/assets/images/favicon.png b/docs/site/assets/images/favicon.png deleted file mode 100644 index 1cf13b9f9d978896599290a74f77d5dbe7d1655c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1870 zcmV-U2eJ5xP)Gc)JR9QMau)O=X#!i9;T z37kk-upj^(fsR36MHs_+1RCI)NNu9}lD0S{B^g8PN?Ww(5|~L#Ng*g{WsqleV}|#l zz8@ri&cTzw_h33bHI+12+kK6WN$h#n5cD8OQt`5kw6p~9H3()bUQ8OS4Q4HTQ=1Ol z_JAocz`fLbT2^{`8n~UAo=#AUOf=SOq4pYkt;XbC&f#7lb$*7=$na!mWCQ`dBQsO0 zLFBSPj*N?#u5&pf2t4XjEGH|=pPQ8xh7tpx;US5Cx_Ju;!O`ya-yF`)b%TEt5>eP1ZX~}sjjA%FJF?h7cX8=b!DZl<6%Cv z*G0uvvU+vmnpLZ2paivG-(cd*y3$hCIcsZcYOGh{$&)A6*XX&kXZd3G8m)G$Zz-LV z^GF3VAW^Mdv!)4OM8EgqRiz~*Cji;uzl2uC9^=8I84vNp;ltJ|q-*uQwGp2ma6cY7 z;`%`!9UXO@fr&Ebapfs34OmS9^u6$)bJxrucutf>`dKPKT%%*d3XlFVKunp9 zasduxjrjs>f8V=D|J=XNZp;_Zy^WgQ$9WDjgY=z@stwiEBm9u5*|34&1Na8BMjjgf3+SHcr`5~>oz1Y?SW^=K z^bTyO6>Gar#P_W2gEMwq)ot3; zREHn~U&Dp0l6YT0&k-wLwYjb?5zGK`W6S2v+K>AM(95m2C20L|3m~rN8dprPr@t)5lsk9Hu*W z?pS990s;Ez=+Rj{x7p``4>+c0G5^pYnB1^!TL=(?HLHZ+HicG{~4F1d^5Awl_2!1jICM-!9eoLhbbT^;yHcefyTAaqRcY zmuctDopPT!%k+}x%lZRKnzykr2}}XfG_ne?nRQO~?%hkzo;@RN{P6o`&mMUWBYMTe z6i8ChtjX&gXl`nvrU>jah)2iNM%JdjqoaeaU%yVn!^70x-flljp6Q5tK}5}&X8&&G zX3fpb3E(!rH=zVI_9Gjl45w@{(ITqngWFe7@9{mX;tO25Z_8 zQHEpI+FkTU#4xu>RkN>b3Tnc3UpWzPXWm#o55GKF09j^Mh~)K7{QqbO_~(@CVq! zS<8954|P8mXN2MRs86xZ&Q4EfM@JB94b=(YGuk)s&^jiSF=t3*oNK3`rD{H`yQ?d; ztE=laAUoZx5?RC8*WKOj`%LXEkgDd>&^Q4M^z`%u0rg-It=hLCVsq!Z%^6eB-OvOT zFZ28TN&cRmgU}Elrnk43)!>Z1FCPL2K$7}gwzIc48NX}#!A1BpJP?#v5wkNprhV** z?Cpalt1oH&{r!o3eSKc&ap)iz2BTn_VV`4>9M^b3;(YY}4>#ML6{~(4mH+?%07*qo IM6N<$f(jP3KmY&$ diff --git a/docs/site/assets/javascripts/LICENSE b/docs/site/assets/javascripts/LICENSE deleted file mode 100644 index baab16b..0000000 --- a/docs/site/assets/javascripts/LICENSE +++ /dev/null @@ -1,29 +0,0 @@ -------------------------------------------------------------------------------- -Third-Party licenses -------------------------------------------------------------------------------- - -Package: clipboard@2.0.11 -License: MIT -Copyright: Zeno Rocha - -------------------------------------------------------------------------------- - -Package: escape-html@1.0.3 -License: MIT -Copyright: 2012-2013 TJ Holowaychuk - 2015 Andreas Lubbe - 2015 Tiancheng "Timothy" Gu - -------------------------------------------------------------------------------- - -Package: focus-visible@5.2.1 -License: W3C -Copyright: WICG - -------------------------------------------------------------------------------- - -Package: rxjs@7.8.2 -License: Apache-2.0 -Copyright: 2015-2018 Google, Inc., - 2015-2018 Netflix, Inc., - 2015-2018 Microsoft Corp. and contributors diff --git a/docs/site/assets/javascripts/bundle.63456bd9.min.js b/docs/site/assets/javascripts/bundle.63456bd9.min.js deleted file mode 100644 index 38f0ece..0000000 --- a/docs/site/assets/javascripts/bundle.63456bd9.min.js +++ /dev/null @@ -1,3 +0,0 @@ -"use strict";(()=>{var xc=Object.create;var kn=Object.defineProperty,wc=Object.defineProperties,Ec=Object.getOwnPropertyDescriptor,Tc=Object.getOwnPropertyDescriptors,Sc=Object.getOwnPropertyNames,Dr=Object.getOwnPropertySymbols,Oc=Object.getPrototypeOf,An=Object.prototype.hasOwnProperty,Fo=Object.prototype.propertyIsEnumerable;var jo=(e,t,r)=>t in e?kn(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,H=(e,t)=>{for(var r in t||(t={}))An.call(t,r)&&jo(e,r,t[r]);if(Dr)for(var r of Dr(t))Fo.call(t,r)&&jo(e,r,t[r]);return e},He=(e,t)=>wc(e,Tc(t));var gr=(e,t)=>{var r={};for(var n in e)An.call(e,n)&&t.indexOf(n)<0&&(r[n]=e[n]);if(e!=null&&Dr)for(var n of Dr(e))t.indexOf(n)<0&&Fo.call(e,n)&&(r[n]=e[n]);return r};var Cn=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var Lc=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of Sc(t))!An.call(e,o)&&o!==r&&kn(e,o,{get:()=>t[o],enumerable:!(n=Ec(t,o))||n.enumerable});return e};var _r=(e,t,r)=>(r=e!=null?xc(Oc(e)):{},Lc(t||!e||!e.__esModule?kn(r,"default",{value:e,enumerable:!0}):r,e));var Uo=(e,t,r)=>new Promise((n,o)=>{var i=c=>{try{s(r.next(c))}catch(l){o(l)}},a=c=>{try{s(r.throw(c))}catch(l){o(l)}},s=c=>c.done?n(c.value):Promise.resolve(c.value).then(i,a);s((r=r.apply(e,t)).next())});var Do=Cn((Hn,No)=>{(function(e,t){typeof Hn=="object"&&typeof No!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(Hn,(function(){"use strict";function e(r){var n=!0,o=!1,i=null,a={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function s(_){return!!(_&&_!==document&&_.nodeName!=="HTML"&&_.nodeName!=="BODY"&&"classList"in _&&"contains"in _.classList)}function c(_){var de=_.type,be=_.tagName;return!!(be==="INPUT"&&a[de]&&!_.readOnly||be==="TEXTAREA"&&!_.readOnly||_.isContentEditable)}function l(_){_.classList.contains("focus-visible")||(_.classList.add("focus-visible"),_.setAttribute("data-focus-visible-added",""))}function u(_){_.hasAttribute("data-focus-visible-added")&&(_.classList.remove("focus-visible"),_.removeAttribute("data-focus-visible-added"))}function p(_){_.metaKey||_.altKey||_.ctrlKey||(s(r.activeElement)&&l(r.activeElement),n=!0)}function d(_){n=!1}function m(_){s(_.target)&&(n||c(_.target))&&l(_.target)}function h(_){s(_.target)&&(_.target.classList.contains("focus-visible")||_.target.hasAttribute("data-focus-visible-added"))&&(o=!0,window.clearTimeout(i),i=window.setTimeout(function(){o=!1},100),u(_.target))}function v(_){document.visibilityState==="hidden"&&(o&&(n=!0),x())}function x(){document.addEventListener("mousemove",E),document.addEventListener("mousedown",E),document.addEventListener("mouseup",E),document.addEventListener("pointermove",E),document.addEventListener("pointerdown",E),document.addEventListener("pointerup",E),document.addEventListener("touchmove",E),document.addEventListener("touchstart",E),document.addEventListener("touchend",E)}function w(){document.removeEventListener("mousemove",E),document.removeEventListener("mousedown",E),document.removeEventListener("mouseup",E),document.removeEventListener("pointermove",E),document.removeEventListener("pointerdown",E),document.removeEventListener("pointerup",E),document.removeEventListener("touchmove",E),document.removeEventListener("touchstart",E),document.removeEventListener("touchend",E)}function E(_){_.target.nodeName&&_.target.nodeName.toLowerCase()==="html"||(n=!1,w())}document.addEventListener("keydown",p,!0),document.addEventListener("mousedown",d,!0),document.addEventListener("pointerdown",d,!0),document.addEventListener("touchstart",d,!0),document.addEventListener("visibilitychange",v,!0),x(),r.addEventListener("focus",m,!0),r.addEventListener("blur",h,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)}))});var So=Cn((M0,vs)=>{"use strict";var Gu=/["'&<>]/;vs.exports=Ju;function Ju(e){var t=""+e,r=Gu.exec(t);if(!r)return t;var n,o="",i=0,a=0;for(i=r.index;i{(function(t,r){typeof jr=="object"&&typeof Lo=="object"?Lo.exports=r():typeof define=="function"&&define.amd?define([],r):typeof jr=="object"?jr.ClipboardJS=r():t.ClipboardJS=r()})(jr,function(){return(function(){var e={686:(function(n,o,i){"use strict";i.d(o,{default:function(){return vr}});var a=i(279),s=i.n(a),c=i(370),l=i.n(c),u=i(817),p=i.n(u);function d(B){try{return document.execCommand(B)}catch(C){return!1}}var m=function(C){var k=p()(C);return d("cut"),k},h=m;function v(B){var C=document.documentElement.getAttribute("dir")==="rtl",k=document.createElement("textarea");k.style.fontSize="12pt",k.style.border="0",k.style.padding="0",k.style.margin="0",k.style.position="absolute",k.style[C?"right":"left"]="-9999px";var D=window.pageYOffset||document.documentElement.scrollTop;return k.style.top="".concat(D,"px"),k.setAttribute("readonly",""),k.value=B,k}var x=function(C,k){var D=v(C);k.container.appendChild(D);var W=p()(D);return d("copy"),D.remove(),W},w=function(C){var k=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},D="";return typeof C=="string"?D=x(C,k):C instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(C==null?void 0:C.type)?D=x(C.value,k):(D=p()(C),d("copy")),D},E=w;function _(B){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?_=function(k){return typeof k}:_=function(k){return k&&typeof Symbol=="function"&&k.constructor===Symbol&&k!==Symbol.prototype?"symbol":typeof k},_(B)}var de=function(){var C=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},k=C.action,D=k===void 0?"copy":k,W=C.container,Z=C.target,We=C.text;if(D!=="copy"&&D!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(Z!==void 0)if(Z&&_(Z)==="object"&&Z.nodeType===1){if(D==="copy"&&Z.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(D==="cut"&&(Z.hasAttribute("readonly")||Z.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(We)return E(We,{container:W});if(Z)return D==="cut"?h(Z):E(Z,{container:W})},be=de;function M(B){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?M=function(k){return typeof k}:M=function(k){return k&&typeof Symbol=="function"&&k.constructor===Symbol&&k!==Symbol.prototype?"symbol":typeof k},M(B)}function O(B,C){if(!(B instanceof C))throw new TypeError("Cannot call a class as a function")}function N(B,C){for(var k=0;k0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof W.action=="function"?W.action:this.defaultAction,this.target=typeof W.target=="function"?W.target:this.defaultTarget,this.text=typeof W.text=="function"?W.text:this.defaultText,this.container=M(W.container)==="object"?W.container:document.body}},{key:"listenClick",value:function(W){var Z=this;this.listener=l()(W,"click",function(We){return Z.onClick(We)})}},{key:"onClick",value:function(W){var Z=W.delegateTarget||W.currentTarget,We=this.action(Z)||"copy",Gt=be({action:We,container:this.container,target:this.target(Z),text:this.text(Z)});this.emit(Gt?"success":"error",{action:We,text:Gt,trigger:Z,clearSelection:function(){Z&&Z.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(W){return Yt("action",W)}},{key:"defaultTarget",value:function(W){var Z=Yt("target",W);if(Z)return document.querySelector(Z)}},{key:"defaultText",value:function(W){return Yt("text",W)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(W){var Z=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return E(W,Z)}},{key:"cut",value:function(W){return h(W)}},{key:"isSupported",value:function(){var W=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],Z=typeof W=="string"?[W]:W,We=!!document.queryCommandSupported;return Z.forEach(function(Gt){We=We&&!!document.queryCommandSupported(Gt)}),We}}]),k})(s()),vr=Mt}),828:(function(n){var o=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function a(s,c){for(;s&&s.nodeType!==o;){if(typeof s.matches=="function"&&s.matches(c))return s;s=s.parentNode}}n.exports=a}),438:(function(n,o,i){var a=i(828);function s(u,p,d,m,h){var v=l.apply(this,arguments);return u.addEventListener(d,v,h),{destroy:function(){u.removeEventListener(d,v,h)}}}function c(u,p,d,m,h){return typeof u.addEventListener=="function"?s.apply(null,arguments):typeof d=="function"?s.bind(null,document).apply(null,arguments):(typeof u=="string"&&(u=document.querySelectorAll(u)),Array.prototype.map.call(u,function(v){return s(v,p,d,m,h)}))}function l(u,p,d,m){return function(h){h.delegateTarget=a(h.target,p),h.delegateTarget&&m.call(u,h)}}n.exports=c}),879:(function(n,o){o.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},o.nodeList=function(i){var a=Object.prototype.toString.call(i);return i!==void 0&&(a==="[object NodeList]"||a==="[object HTMLCollection]")&&"length"in i&&(i.length===0||o.node(i[0]))},o.string=function(i){return typeof i=="string"||i instanceof String},o.fn=function(i){var a=Object.prototype.toString.call(i);return a==="[object Function]"}}),370:(function(n,o,i){var a=i(879),s=i(438);function c(d,m,h){if(!d&&!m&&!h)throw new Error("Missing required arguments");if(!a.string(m))throw new TypeError("Second argument must be a String");if(!a.fn(h))throw new TypeError("Third argument must be a Function");if(a.node(d))return l(d,m,h);if(a.nodeList(d))return u(d,m,h);if(a.string(d))return p(d,m,h);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function l(d,m,h){return d.addEventListener(m,h),{destroy:function(){d.removeEventListener(m,h)}}}function u(d,m,h){return Array.prototype.forEach.call(d,function(v){v.addEventListener(m,h)}),{destroy:function(){Array.prototype.forEach.call(d,function(v){v.removeEventListener(m,h)})}}}function p(d,m,h){return s(document.body,d,m,h)}n.exports=c}),817:(function(n){function o(i){var a;if(i.nodeName==="SELECT")i.focus(),a=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var s=i.hasAttribute("readonly");s||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),s||i.removeAttribute("readonly"),a=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var c=window.getSelection(),l=document.createRange();l.selectNodeContents(i),c.removeAllRanges(),c.addRange(l),a=c.toString()}return a}n.exports=o}),279:(function(n){function o(){}o.prototype={on:function(i,a,s){var c=this.e||(this.e={});return(c[i]||(c[i]=[])).push({fn:a,ctx:s}),this},once:function(i,a,s){var c=this;function l(){c.off(i,l),a.apply(s,arguments)}return l._=a,this.on(i,l,s)},emit:function(i){var a=[].slice.call(arguments,1),s=((this.e||(this.e={}))[i]||[]).slice(),c=0,l=s.length;for(c;c0&&i[i.length-1])&&(l[0]===6||l[0]===2)){r=0;continue}if(l[0]===3&&(!i||l[1]>i[0]&&l[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function te(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var n=r.call(e),o,i=[],a;try{for(;(t===void 0||t-- >0)&&!(o=n.next()).done;)i.push(o.value)}catch(s){a={error:s}}finally{try{o&&!o.done&&(r=n.return)&&r.call(n)}finally{if(a)throw a.error}}return i}function ne(e,t,r){if(r||arguments.length===2)for(var n=0,o=t.length,i;n1||c(m,v)})},h&&(o[m]=h(o[m])))}function c(m,h){try{l(n[m](h))}catch(v){d(i[0][3],v)}}function l(m){m.value instanceof kt?Promise.resolve(m.value.v).then(u,p):d(i[0][2],m)}function u(m){c("next",m)}function p(m){c("throw",m)}function d(m,h){m(h),i.shift(),i.length&&c(i[0][0],i[0][1])}}function zo(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof $e=="function"?$e(e):e[Symbol.iterator](),r={},n("next"),n("throw"),n("return"),r[Symbol.asyncIterator]=function(){return this},r);function n(i){r[i]=e[i]&&function(a){return new Promise(function(s,c){a=e[i](a),o(s,c,a.done,a.value)})}}function o(i,a,s,c){Promise.resolve(c).then(function(l){i({value:l,done:s})},a)}}function F(e){return typeof e=="function"}function Jt(e){var t=function(n){Error.call(n),n.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var Vr=Jt(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: -`+r.map(function(n,o){return o+1+") "+n.toString()}).join(` - `):"",this.name="UnsubscriptionError",this.errors=r}});function ct(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var rt=(function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,n,o,i;if(!this.closed){this.closed=!0;var a=this._parentage;if(a)if(this._parentage=null,Array.isArray(a))try{for(var s=$e(a),c=s.next();!c.done;c=s.next()){var l=c.value;l.remove(this)}}catch(v){t={error:v}}finally{try{c&&!c.done&&(r=s.return)&&r.call(s)}finally{if(t)throw t.error}}else a.remove(this);var u=this.initialTeardown;if(F(u))try{u()}catch(v){i=v instanceof Vr?v.errors:[v]}var p=this._finalizers;if(p){this._finalizers=null;try{for(var d=$e(p),m=d.next();!m.done;m=d.next()){var h=m.value;try{qo(h)}catch(v){i=i!=null?i:[],v instanceof Vr?i=ne(ne([],te(i)),te(v.errors)):i.push(v)}}}catch(v){n={error:v}}finally{try{m&&!m.done&&(o=d.return)&&o.call(d)}finally{if(n)throw n.error}}}if(i)throw new Vr(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)qo(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&ct(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&ct(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=(function(){var t=new e;return t.closed=!0,t})(),e})();var Pn=rt.EMPTY;function zr(e){return e instanceof rt||e&&"closed"in e&&F(e.remove)&&F(e.add)&&F(e.unsubscribe)}function qo(e){F(e)?e():e.unsubscribe()}var Je={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var Xt={setTimeout:function(e,t){for(var r=[],n=2;n0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var n=this,o=this,i=o.hasError,a=o.isStopped,s=o.observers;return i||a?Pn:(this.currentObservers=null,s.push(r),new rt(function(){n.currentObservers=null,ct(s,r)}))},t.prototype._checkFinalizedStatuses=function(r){var n=this,o=n.hasError,i=n.thrownError,a=n.isStopped;o?r.error(i):a&&r.complete()},t.prototype.asObservable=function(){var r=new U;return r.source=this,r},t.create=function(r,n){return new Qo(r,n)},t})(U);var Qo=(function(e){ue(t,e);function t(r,n){var o=e.call(this)||this;return o.destination=r,o.source=n,o}return t.prototype.next=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.next)===null||o===void 0||o.call(n,r)},t.prototype.error=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.error)===null||o===void 0||o.call(n,r)},t.prototype.complete=function(){var r,n;(n=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||n===void 0||n.call(r)},t.prototype._subscribe=function(r){var n,o;return(o=(n=this.source)===null||n===void 0?void 0:n.subscribe(r))!==null&&o!==void 0?o:Pn},t})(I);var Un=(function(e){ue(t,e);function t(r){var n=e.call(this)||this;return n._value=r,n}return Object.defineProperty(t.prototype,"value",{get:function(){return this.getValue()},enumerable:!1,configurable:!0}),t.prototype._subscribe=function(r){var n=e.prototype._subscribe.call(this,r);return!n.closed&&r.next(this._value),n},t.prototype.getValue=function(){var r=this,n=r.hasError,o=r.thrownError,i=r._value;if(n)throw o;return this._throwIfClosed(),i},t.prototype.next=function(r){e.prototype.next.call(this,this._value=r)},t})(I);var xr={now:function(){return(xr.delegate||Date).now()},delegate:void 0};var wr=(function(e){ue(t,e);function t(r,n,o){r===void 0&&(r=1/0),n===void 0&&(n=1/0),o===void 0&&(o=xr);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=n,i._timestampProvider=o,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=n===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,n),i}return t.prototype.next=function(r){var n=this,o=n.isStopped,i=n._buffer,a=n._infiniteTimeWindow,s=n._timestampProvider,c=n._windowTime;o||(i.push(r),!a&&i.push(s.now()+c)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var n=this._innerSubscribe(r),o=this,i=o._infiniteTimeWindow,a=o._buffer,s=a.slice(),c=0;c0?e.prototype.schedule.call(this,r,n):(this.delay=n,this.state=r,this.scheduler.flush(this),this)},t.prototype.execute=function(r,n){return n>0||this.closed?e.prototype.execute.call(this,r,n):this._execute(r,n)},t.prototype.requestAsyncId=function(r,n,o){return o===void 0&&(o=0),o!=null&&o>0||o==null&&this.delay>0?e.prototype.requestAsyncId.call(this,r,n,o):(r.flush(this),0)},t})(tr);var ri=(function(e){ue(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t})(rr);var Wn=new ri(ti);var ni=(function(e){ue(t,e);function t(r,n){var o=e.call(this,r,n)||this;return o.scheduler=r,o.work=n,o}return t.prototype.requestAsyncId=function(r,n,o){return o===void 0&&(o=0),o!==null&&o>0?e.prototype.requestAsyncId.call(this,r,n,o):(r.actions.push(this),r._scheduled||(r._scheduled=er.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,n,o){var i;if(o===void 0&&(o=0),o!=null?o>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,n,o);var a=r.actions;n!=null&&n===r._scheduled&&((i=a[a.length-1])===null||i===void 0?void 0:i.id)!==n&&(er.cancelAnimationFrame(n),r._scheduled=void 0)},t})(tr);var oi=(function(e){ue(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var n;r?n=r.id:(n=this._scheduled,this._scheduled=void 0);var o=this.actions,i;r=r||o.shift();do if(i=r.execute(r.state,r.delay))break;while((r=o[0])&&r.id===n&&o.shift());if(this._active=!1,i){for(;(r=o[0])&&r.id===n&&o.shift();)r.unsubscribe();throw i}},t})(rr);var je=new oi(ni);var y=new U(function(e){return e.complete()});function Br(e){return e&&F(e.schedule)}function Vn(e){return e[e.length-1]}function _t(e){return F(Vn(e))?e.pop():void 0}function qe(e){return Br(Vn(e))?e.pop():void 0}function Yr(e,t){return typeof Vn(e)=="number"?e.pop():t}var nr=(function(e){return e&&typeof e.length=="number"&&typeof e!="function"});function Gr(e){return F(e==null?void 0:e.then)}function Jr(e){return F(e[Qt])}function Xr(e){return Symbol.asyncIterator&&F(e==null?void 0:e[Symbol.asyncIterator])}function Zr(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function Rc(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var Qr=Rc();function en(e){return F(e==null?void 0:e[Qr])}function tn(e){return Vo(this,arguments,function(){var r,n,o,i;return Wr(this,function(a){switch(a.label){case 0:r=e.getReader(),a.label=1;case 1:a.trys.push([1,,9,10]),a.label=2;case 2:return[4,kt(r.read())];case 3:return n=a.sent(),o=n.value,i=n.done,i?[4,kt(void 0)]:[3,5];case 4:return[2,a.sent()];case 5:return[4,kt(o)];case 6:return[4,a.sent()];case 7:return a.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function rn(e){return F(e==null?void 0:e.getReader)}function q(e){if(e instanceof U)return e;if(e!=null){if(Jr(e))return jc(e);if(nr(e))return Fc(e);if(Gr(e))return Uc(e);if(Xr(e))return ii(e);if(en(e))return Nc(e);if(rn(e))return Dc(e)}throw Zr(e)}function jc(e){return new U(function(t){var r=e[Qt]();if(F(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function Fc(e){return new U(function(t){for(var r=0;r=2;return function(n){return n.pipe(e?L(function(o,i){return e(o,i,n)}):Le,Me(1),r?ot(t):wi(function(){return new on}))}}function Gn(e){return e<=0?function(){return y}:S(function(t,r){var n=[];t.subscribe(T(r,function(o){n.push(o),e=2,!0))}function xe(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new I}:t,n=e.resetOnError,o=n===void 0?!0:n,i=e.resetOnComplete,a=i===void 0?!0:i,s=e.resetOnRefCountZero,c=s===void 0?!0:s;return function(l){var u,p,d,m=0,h=!1,v=!1,x=function(){p==null||p.unsubscribe(),p=void 0},w=function(){x(),u=d=void 0,h=v=!1},E=function(){var _=u;w(),_==null||_.unsubscribe()};return S(function(_,de){m++,!v&&!h&&x();var be=d=d!=null?d:r();de.add(function(){m--,m===0&&!v&&!h&&(p=Jn(E,c))}),be.subscribe(de),!u&&m>0&&(u=new Ct({next:function(M){return be.next(M)},error:function(M){v=!0,x(),p=Jn(w,o,M),be.error(M)},complete:function(){h=!0,x(),p=Jn(w,a),be.complete()}}),q(_).subscribe(u))})(l)}}function Jn(e,t){for(var r=[],n=2;ne.next(document)),e}function P(e,t=document){return Array.from(t.querySelectorAll(e))}function G(e,t=document){let r=we(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function we(e,t=document){return t.querySelector(e)||void 0}function xt(){var e,t,r,n;return(n=(r=(t=(e=document.activeElement)==null?void 0:e.shadowRoot)==null?void 0:t.activeElement)!=null?r:document.activeElement)!=null?n:void 0}var il=R(b(document.body,"focusin"),b(document.body,"focusout")).pipe(Be(1),J(void 0),f(()=>xt()||document.body),se(1));function ir(e){return il.pipe(f(t=>e.contains(t)),ie())}function Ft(e,t){let{matches:r}=matchMedia("(hover)");return j(()=>(r?R(b(e,"mouseenter").pipe(f(()=>!0)),b(e,"mouseleave").pipe(f(()=>!1))):R(b(e,"touchstart").pipe(f(()=>!0)),b(e,"touchend").pipe(f(()=>!1)),b(e,"touchcancel").pipe(f(()=>!1)))).pipe(t?Tr(o=>Ve(+!o*t)):Le,J(!0,e.matches(":hover"))))}function Oi(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)Oi(e,r)}function A(e,t,...r){let n=document.createElement(e);if(t)for(let o of Object.keys(t))typeof t[o]!="undefined"&&(typeof t[o]!="boolean"?n.setAttribute(o,t[o]):n.setAttribute(o,""));for(let o of r)Oi(n,o);return n}function Li(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function ar(e){let t=A("script",{src:e});return j(()=>(document.head.appendChild(t),R(b(t,"load"),b(t,"error").pipe(g(()=>zn(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(f(()=>{}),V(()=>document.head.removeChild(t)),Me(1))))}var Mi=new I,al=j(()=>typeof ResizeObserver=="undefined"?ar("https://unpkg.com/resize-observer-polyfill"):Y(void 0)).pipe(f(()=>new ResizeObserver(e=>e.forEach(t=>Mi.next(t)))),g(e=>R(Ke,Y(e)).pipe(V(()=>e.disconnect()))),se(1));function Ae(e){return{width:e.offsetWidth,height:e.offsetHeight}}function Re(e){let t=e;for(;t.clientWidth===0&&t.parentElement;)t=t.parentElement;return al.pipe($(r=>r.observe(t)),g(r=>Mi.pipe(L(n=>n.target===t),V(()=>r.unobserve(t)))),f(()=>Ae(e)),J(Ae(e)))}function Mr(e){return{width:e.scrollWidth,height:e.scrollHeight}}function ki(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}function Ai(e){let t=[],r=e.parentElement;for(;r;)(e.clientWidth>r.clientWidth||e.clientHeight>r.clientHeight)&&t.push(r),r=(e=r).parentElement;return t.length===0&&t.push(document.documentElement),t}function wt(e){return{x:e.offsetLeft,y:e.offsetTop}}function Ci(e){let t=e.getBoundingClientRect();return{x:t.x+window.scrollX,y:t.y+window.scrollY}}function Hi(e){return R(b(window,"load"),b(window,"resize")).pipe(Xe(0,je),f(()=>wt(e)),J(wt(e)))}function ln(e){return{x:e.scrollLeft,y:e.scrollTop}}function Ut(e){return R(b(e,"scroll"),b(window,"scroll"),b(window,"resize")).pipe(Xe(0,je),f(()=>ln(e)),J(ln(e)))}var $i=new I,sl=j(()=>Y(new IntersectionObserver(e=>{for(let t of e)$i.next(t)},{threshold:0}))).pipe(g(e=>R(Ke,Y(e)).pipe(V(()=>e.disconnect()))),se(1));function Et(e){return sl.pipe($(t=>t.observe(e)),g(t=>$i.pipe(L(({target:r})=>r===e),V(()=>t.unobserve(e)),f(({isIntersecting:r})=>r))))}var cl=Object.create,la=Object.defineProperty,ll=Object.getOwnPropertyDescriptor,ul=Object.getOwnPropertyNames,pl=Object.getPrototypeOf,fl=Object.prototype.hasOwnProperty,ml=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),dl=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of ul(t))!fl.call(e,o)&&o!==r&&la(e,o,{get:()=>t[o],enumerable:!(n=ll(t,o))||n.enumerable});return e},hl=(e,t,r)=>(r=e!=null?cl(pl(e)):{},dl(t||!e||!e.__esModule?la(r,"default",{value:e,enumerable:!0}):r,e)),vl=ml((e,t)=>{var r="Expected a function",n=NaN,o="[object Symbol]",i=/^\s+|\s+$/g,a=/^[-+]0x[0-9a-f]+$/i,s=/^0b[01]+$/i,c=/^0o[0-7]+$/i,l=parseInt,u=typeof global=="object"&&global&&global.Object===Object&&global,p=typeof self=="object"&&self&&self.Object===Object&&self,d=u||p||Function("return this")(),m=Object.prototype,h=m.toString,v=Math.max,x=Math.min,w=function(){return d.Date.now()};function E(O,N,ee){var le,ce,Ne,bt,De,st,tt=0,Yt=!1,Mt=!1,vr=!0;if(typeof O!="function")throw new TypeError(r);N=M(N)||0,_(ee)&&(Yt=!!ee.leading,Mt="maxWait"in ee,Ne=Mt?v(M(ee.maxWait)||0,N):Ne,vr="trailing"in ee?!!ee.trailing:vr);function B(Se){var gt=le,br=ce;return le=ce=void 0,tt=Se,bt=O.apply(br,gt),bt}function C(Se){return tt=Se,De=setTimeout(W,N),Yt?B(Se):bt}function k(Se){var gt=Se-st,br=Se-tt,Ro=N-gt;return Mt?x(Ro,Ne-br):Ro}function D(Se){var gt=Se-st,br=Se-tt;return st===void 0||gt>=N||gt<0||Mt&&br>=Ne}function W(){var Se=w();if(D(Se))return Z(Se);De=setTimeout(W,k(Se))}function Z(Se){return De=void 0,vr&&le?B(Se):(le=ce=void 0,bt)}function We(){De!==void 0&&clearTimeout(De),tt=0,le=st=ce=De=void 0}function Gt(){return De===void 0?bt:Z(w())}function Nr(){var Se=w(),gt=D(Se);if(le=arguments,ce=this,st=Se,gt){if(De===void 0)return C(st);if(Mt)return De=setTimeout(W,N),B(st)}return De===void 0&&(De=setTimeout(W,N)),bt}return Nr.cancel=We,Nr.flush=Gt,Nr}function _(O){var N=typeof O;return!!O&&(N=="object"||N=="function")}function de(O){return!!O&&typeof O=="object"}function be(O){return typeof O=="symbol"||de(O)&&h.call(O)==o}function M(O){if(typeof O=="number")return O;if(be(O))return n;if(_(O)){var N=typeof O.valueOf=="function"?O.valueOf():O;O=_(N)?N+"":N}if(typeof O!="string")return O===0?O:+O;O=O.replace(i,"");var ee=s.test(O);return ee||c.test(O)?l(O.slice(2),ee?2:8):a.test(O)?n:+O}t.exports=E}),yn,K,ua,pa,Nt,Pi,fa,ma,da,lo,to,ro,bl,Ar={},ha=[],gl=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,Pr=Array.isArray;function pt(e,t){for(var r in t)e[r]=t[r];return e}function uo(e){e&&e.parentNode&&e.parentNode.removeChild(e)}function Wt(e,t,r){var n,o,i,a={};for(i in t)i=="key"?n=t[i]:i=="ref"?o=t[i]:a[i]=t[i];if(arguments.length>2&&(a.children=arguments.length>3?yn.call(arguments,2):r),typeof e=="function"&&e.defaultProps!=null)for(i in e.defaultProps)a[i]===void 0&&(a[i]=e.defaultProps[i]);return fn(e,a,n,o,null)}function fn(e,t,r,n,o){var i={type:e,props:t,key:r,ref:n,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:o!=null?o:++ua,__i:-1,__u:0};return o==null&&K.vnode!=null&&K.vnode(i),i}function ft(e){return e.children}function at(e,t){this.props=e,this.context=t}function cr(e,t){if(t==null)return e.__?cr(e.__,e.__i+1):null;for(var r;ts&&Nt.sort(ma),e=Nt.shift(),s=Nt.length,e.__d&&(r=void 0,n=void 0,o=(n=(t=e).__v).__e,i=[],a=[],t.__P&&((r=pt({},n)).__v=n.__v+1,K.vnode&&K.vnode(r),po(t.__P,r,n,t.__n,t.__P.namespaceURI,32&n.__u?[o]:null,i,o!=null?o:cr(n),!!(32&n.__u),a),r.__v=n.__v,r.__.__k[r.__i]=r,_a(i,r,a),n.__e=n.__=null,r.__e!=o&&va(r)));vn.__r=0}function ba(e,t,r,n,o,i,a,s,c,l,u){var p,d,m,h,v,x,w,E=n&&n.__k||ha,_=t.length;for(c=_l(r,t,E,c,_),p=0;p<_;p++)(m=r.__k[p])!=null&&(d=m.__i==-1?Ar:E[m.__i]||Ar,m.__i=p,x=po(e,m,d,o,i,a,s,c,l,u),h=m.__e,m.ref&&d.ref!=m.ref&&(d.ref&&fo(d.ref,null,m),u.push(m.ref,m.__c||h,m)),v==null&&h!=null&&(v=h),(w=!!(4&m.__u))||d.__k===m.__k?c=ga(m,c,e,w):typeof m.type=="function"&&x!==void 0?c=x:h&&(c=h.nextSibling),m.__u&=-7);return r.__e=v,c}function _l(e,t,r,n,o){var i,a,s,c,l,u=r.length,p=u,d=0;for(e.__k=new Array(o),i=0;i0?fn(a.type,a.props,a.key,a.ref?a.ref:null,a.__v):a).__=e,a.__b=e.__b+1,s=null,(l=a.__i=yl(a,r,c,p))!=-1&&(p--,(s=r[l])&&(s.__u|=2)),s==null||s.__v==null?(l==-1&&(o>u?d--:oc?d--:d++,a.__u|=4))):e.__k[i]=null;if(p)for(i=0;i(u?1:0)){for(o=r-1,i=r+1;o>=0||i=0?o--:i++])!=null&&!(2&l.__u)&&s==l.key&&c==l.type)return a}return-1}function Ri(e,t,r){t[0]=="-"?e.setProperty(t,r!=null?r:""):e[t]=r==null?"":typeof r!="number"||gl.test(t)?r:r+"px"}function un(e,t,r,n,o){var i,a;e:if(t=="style")if(typeof r=="string")e.style.cssText=r;else{if(typeof n=="string"&&(e.style.cssText=n=""),n)for(t in n)r&&t in r||Ri(e.style,t,"");if(r)for(t in r)n&&r[t]==n[t]||Ri(e.style,t,r[t])}else if(t[0]=="o"&&t[1]=="n")i=t!=(t=t.replace(da,"$1")),a=t.toLowerCase(),t=a in e||t=="onFocusOut"||t=="onFocusIn"?a.slice(2):t.slice(2),e.l||(e.l={}),e.l[t+i]=r,r?n?r.u=n.u:(r.u=lo,e.addEventListener(t,i?ro:to,i)):e.removeEventListener(t,i?ro:to,i);else{if(o=="http://www.w3.org/2000/svg")t=t.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if(t!="width"&&t!="height"&&t!="href"&&t!="list"&&t!="form"&&t!="tabIndex"&&t!="download"&&t!="rowSpan"&&t!="colSpan"&&t!="role"&&t!="popover"&&t in e)try{e[t]=r!=null?r:"";break e}catch(s){}typeof r=="function"||(r==null||r===!1&&t[4]!="-"?e.removeAttribute(t):e.setAttribute(t,t=="popover"&&r==1?"":r))}}function ji(e){return function(t){if(this.l){var r=this.l[t.type+e];if(t.t==null)t.t=lo++;else if(t.t0?e:Pr(e)?e.map(ya):pt({},e)}function xl(e,t,r,n,o,i,a,s,c){var l,u,p,d,m,h,v,x=r.props,w=t.props,E=t.type;if(E=="svg"?o="http://www.w3.org/2000/svg":E=="math"?o="http://www.w3.org/1998/Math/MathML":o||(o="http://www.w3.org/1999/xhtml"),i!=null){for(l=0;l=r.__.length&&r.__.push({}),r.__[e]}function bn(e){return $r=1,Tl(Ta,e)}function Tl(e,t,r){var n=mo(Hr++,2);if(n.t=e,!n.__c&&(n.__=[r?r(t):Ta(void 0,t),function(s){var c=n.__N?n.__N[0]:n.__[0],l=n.t(c,s);c!==l&&(n.__N=[l,n.__[1]],n.__c.setState({}))}],n.__c=ve,!ve.__f)){var o=function(s,c,l){if(!n.__c.__H)return!0;var u=n.__c.__H.__.filter(function(d){return!!d.__c});if(u.every(function(d){return!d.__N}))return!i||i.call(this,s,c,l);var p=n.__c.props!==s;return u.forEach(function(d){if(d.__N){var m=d.__[0];d.__=d.__N,d.__N=void 0,m!==d.__[0]&&(p=!0)}}),i&&i.call(this,s,c,l)||p};ve.__f=!0;var i=ve.shouldComponentUpdate,a=ve.componentWillUpdate;ve.componentWillUpdate=function(s,c,l){if(this.__e){var u=i;i=void 0,o(s,c,l),i=u}a&&a.call(this,s,c,l)},ve.shouldComponentUpdate=o}return n.__N||n.__}function mt(e,t){var r=mo(Hr++,3);!Ee.__s&&Ea(r.__H,t)&&(r.__=e,r.u=t,ve.__H.__h.push(r))}function Vt(e){return $r=5,ur(function(){return{current:e}},[])}function ur(e,t){var r=mo(Hr++,7);return Ea(r.__H,t)&&(r.__=e(),r.__H=t,r.__h=e),r.__}function Sl(e,t){return $r=8,ur(function(){return e},t)}function Ol(){for(var e;e=wa.shift();)if(e.__P&&e.__H)try{e.__H.__h.forEach(mn),e.__H.__h.forEach(oo),e.__H.__h=[]}catch(t){e.__H.__h=[],Ee.__e(t,e.__v)}}Ee.__b=function(e){ve=null,Ui&&Ui(e)},Ee.__=function(e,t){e&&t.__k&&t.__k.__m&&(e.__m=t.__k.__m),zi&&zi(e,t)},Ee.__r=function(e){Ni&&Ni(e),Hr=0;var t=(ve=e.__c).__H;t&&(Zn===ve?(t.__h=[],ve.__h=[],t.__.forEach(function(r){r.__N&&(r.__=r.__N),r.u=r.__N=void 0})):(t.__h.forEach(mn),t.__h.forEach(oo),t.__h=[],Hr=0)),Zn=ve},Ee.diffed=function(e){Di&&Di(e);var t=e.__c;t&&t.__H&&(t.__H.__h.length&&(wa.push(t)!==1&&Fi===Ee.requestAnimationFrame||((Fi=Ee.requestAnimationFrame)||Ll)(Ol)),t.__H.__.forEach(function(r){r.u&&(r.__H=r.u),r.u=void 0})),Zn=ve=null},Ee.__c=function(e,t){t.some(function(r){try{r.__h.forEach(mn),r.__h=r.__h.filter(function(n){return!n.__||oo(n)})}catch(n){t.some(function(o){o.__h&&(o.__h=[])}),t=[],Ee.__e(n,r.__v)}}),Wi&&Wi(e,t)},Ee.unmount=function(e){Vi&&Vi(e);var t,r=e.__c;r&&r.__H&&(r.__H.__.forEach(function(n){try{mn(n)}catch(o){t=o}}),r.__H=void 0,t&&Ee.__e(t,r.__v))};var qi=typeof requestAnimationFrame=="function";function Ll(e){var t,r=function(){clearTimeout(n),qi&&cancelAnimationFrame(t),setTimeout(e)},n=setTimeout(r,35);qi&&(t=requestAnimationFrame(r))}function mn(e){var t=ve,r=e.__c;typeof r=="function"&&(e.__c=void 0,r()),ve=t}function oo(e){var t=ve;e.__c=e.__(),ve=t}function Ea(e,t){return!e||e.length!==t.length||t.some(function(r,n){return r!==e[n]})}function Ta(e,t){return typeof t=="function"?t(e):t}function Ml(e,t){for(var r in t)e[r]=t[r];return e}function Ki(e,t){for(var r in e)if(r!=="__source"&&!(r in t))return!0;for(var n in t)if(n!=="__source"&&e[n]!==t[n])return!0;return!1}function Bi(e,t){this.props=e,this.context=t}(Bi.prototype=new at).isPureReactComponent=!0,Bi.prototype.shouldComponentUpdate=function(e,t){return Ki(this.props,e)||Ki(this.state,t)};var Yi=K.__b;K.__b=function(e){e.type&&e.type.__f&&e.ref&&(e.props.ref=e.ref,e.ref=null),Yi&&Yi(e)};var Yx=typeof Symbol<"u"&&Symbol.for&&Symbol.for("react.forward_ref")||3911,kl=K.__e;K.__e=function(e,t,r,n){if(e.then){for(var o,i=t;i=i.__;)if((o=i.__c)&&o.__c)return t.__e==null&&(t.__e=r.__e,t.__k=r.__k),o.__c(e,t)}kl(e,t,r,n)};var Gi=K.unmount;function Sa(e,t,r){return e&&(e.__c&&e.__c.__H&&(e.__c.__H.__.forEach(function(n){typeof n.__c=="function"&&n.__c()}),e.__c.__H=null),(e=Ml({},e)).__c!=null&&(e.__c.__P===r&&(e.__c.__P=t),e.__c.__e=!0,e.__c=null),e.__k=e.__k&&e.__k.map(function(n){return Sa(n,t,r)})),e}function Oa(e,t,r){return e&&r&&(e.__v=null,e.__k=e.__k&&e.__k.map(function(n){return Oa(n,t,r)}),e.__c&&e.__c.__P===t&&(e.__e&&r.appendChild(e.__e),e.__c.__e=!0,e.__c.__P=r)),e}function Qn(){this.__u=0,this.o=null,this.__b=null}function La(e){var t=e.__.__c;return t&&t.__a&&t.__a(e)}function pn(){this.i=null,this.l=null}K.unmount=function(e){var t=e.__c;t&&t.__R&&t.__R(),t&&32&e.__u&&(e.type=null),Gi&&Gi(e)},(Qn.prototype=new at).__c=function(e,t){var r=t.__c,n=this;n.o==null&&(n.o=[]),n.o.push(r);var o=La(n.__v),i=!1,a=function(){i||(i=!0,r.__R=null,o?o(s):s())};r.__R=a;var s=function(){if(!--n.__u){if(n.state.__a){var c=n.state.__a;n.__v.__k[0]=Oa(c,c.__c.__P,c.__c.__O)}var l;for(n.setState({__a:n.__b=null});l=n.o.pop();)l.forceUpdate()}};n.__u++||32&t.__u||n.setState({__a:n.__b=n.__v.__k[0]}),e.then(a,a)},Qn.prototype.componentWillUnmount=function(){this.o=[]},Qn.prototype.render=function(e,t){if(this.__b){if(this.__v.__k){var r=document.createElement("div"),n=this.__v.__k[0].__c;this.__v.__k[0]=Sa(this.__b,r,n.__O=n.__P)}this.__b=null}var o=t.__a&&Wt(ft,null,e.fallback);return o&&(o.__u&=-33),[Wt(ft,null,t.__a?null:e.children),o]};var Ji=function(e,t,r){if(++r[1]===r[0]&&e.l.delete(t),e.props.revealOrder&&(e.props.revealOrder[0]!=="t"||!e.l.size))for(r=e.i;r;){for(;r.length>3;)r.pop()();if(r[1]Object.freeze({get current(){return t.current}}),[])}var Nl=typeof globalThis<"u"&&typeof navigator<"u"&&typeof document<"u";function Dl(e,...t){var r;(r=e==null?void 0:e.addEventListener)==null||r.call(e,...t)}function Wl(e,...t){var r;(r=e==null?void 0:e.removeEventListener)==null||r.call(e,...t)}var Vl=(e,t)=>Object.hasOwn(e,t),zl=()=>!0,ql=()=>!1;function Kl(e=!1){let t=Vt(e),r=Sl(()=>t.current,[]);return mt(()=>(t.current=!0,()=>{t.current=!1}),[]),r}function Bl(e,...t){let r=Kl(),n=ka(t[1]),o=ur(()=>function(...i){r()&&(typeof n.current=="function"?n.current.apply(this,i):typeof n.current.handleEvent=="function"&&n.current.handleEvent.apply(this,i))},[]);mt(()=>{let i=Yl(e)?e.current:e;if(!i)return;let a=t.slice(2);return Dl(i,t[0],o,...a),()=>{Wl(i,t[0],o,...a)}},[e,t[0]])}function Yl(e){return e!==null&&typeof e=="object"&&Vl(e,"current")}var Gl=e=>typeof e=="function"?e:typeof e=="string"?t=>t.key===e:e?zl:ql,Jl=Nl?globalThis:null;function Aa(e,t,r=[],n={}){let{event:o="keydown",target:i=Jl,eventOptions:a}=n,s=ka(t),c=ur(()=>{let l=Gl(e);return function(u){l(u)&&s.current.call(this,u)}},r);Bl(i,o,c,a)}function Ca(e){var t,r,n="";if(typeof e=="string"||typeof e=="number")n+=e;else if(typeof e=="object")if(Array.isArray(e)){var o=e.length;for(t=0;t1)St--;else{for(var e,t=!1;kr!==void 0;){var r=kr;for(kr=void 0,io++;r!==void 0;){var n=r.o;if(r.o=void 0,r.f&=-3,!(8&r.f)&&Pa(r))try{r.c()}catch(o){t||(e=o,t=!0)}r=n}}if(io=0,St--,t)throw e}}function Ql(e){if(St>0)return e();St++;try{return e()}finally{xn()}}var ae=void 0;function Ha(e){var t=ae;ae=void 0;try{return e()}finally{ae=t}}var kr=void 0,St=0,io=0,gn=0;function $a(e){if(ae!==void 0){var t=e.n;if(t===void 0||t.t!==ae)return t={i:0,S:e,p:ae.s,n:void 0,t:ae,e:void 0,x:void 0,r:t},ae.s!==void 0&&(ae.s.n=t),ae.s=t,e.n=t,32&ae.f&&e.S(t),t;if(t.i===-1)return t.i=0,t.n!==void 0&&(t.n.p=t.p,t.p!==void 0&&(t.p.n=t.n),t.p=ae.s,t.n=void 0,ae.s.n=t,ae.s=t),t}}function Ce(e,t){this.v=e,this.i=0,this.n=void 0,this.t=void 0,this.W=t==null?void 0:t.watched,this.Z=t==null?void 0:t.unwatched,this.name=t==null?void 0:t.name}Ce.prototype.brand=Zl;Ce.prototype.h=function(){return!0};Ce.prototype.S=function(e){var t=this,r=this.t;r!==e&&e.e===void 0&&(e.x=r,this.t=e,r!==void 0?r.e=e:Ha(function(){var n;(n=t.W)==null||n.call(t)}))};Ce.prototype.U=function(e){var t=this;if(this.t!==void 0){var r=e.e,n=e.x;r!==void 0&&(r.x=n,e.e=void 0),n!==void 0&&(n.e=r,e.x=void 0),e===this.t&&(this.t=n,n===void 0&&Ha(function(){var o;(o=t.Z)==null||o.call(t)}))}};Ce.prototype.subscribe=function(e){var t=this;return qt(function(){var r=t.value,n=ae;ae=void 0;try{e(r)}finally{ae=n}},{name:"sub"})};Ce.prototype.valueOf=function(){return this.value};Ce.prototype.toString=function(){return this.value+""};Ce.prototype.toJSON=function(){return this.value};Ce.prototype.peek=function(){var e=ae;ae=void 0;try{return this.value}finally{ae=e}};Object.defineProperty(Ce.prototype,"value",{get:function(){var e=$a(this);return e!==void 0&&(e.i=this.i),this.v},set:function(e){if(e!==this.v){if(io>100)throw new Error("Cycle detected");this.v=e,this.i++,gn++,St++;try{for(var t=this.t;t!==void 0;t=t.x)t.t.N()}finally{xn()}}}});function Ot(e,t){return new Ce(e,t)}function Pa(e){for(var t=e.s;t!==void 0;t=t.n)if(t.S.i!==t.i||!t.S.h()||t.S.i!==t.i)return!0;return!1}function Ia(e){for(var t=e.s;t!==void 0;t=t.n){var r=t.S.n;if(r!==void 0&&(t.r=r),t.S.n=t,t.i=-1,t.n===void 0){e.s=t;break}}}function Ra(e){for(var t=e.s,r=void 0;t!==void 0;){var n=t.p;t.i===-1?(t.S.U(t),n!==void 0&&(n.n=t.n),t.n!==void 0&&(t.n.p=n)):r=t,t.S.n=t.r,t.r!==void 0&&(t.r=void 0),t=n}e.s=r}function Kt(e,t){Ce.call(this,void 0),this.x=e,this.s=void 0,this.g=gn-1,this.f=4,this.W=t==null?void 0:t.watched,this.Z=t==null?void 0:t.unwatched,this.name=t==null?void 0:t.name}Kt.prototype=new Ce;Kt.prototype.h=function(){if(this.f&=-3,1&this.f)return!1;if((36&this.f)==32||(this.f&=-5,this.g===gn))return!0;if(this.g=gn,this.f|=1,this.i>0&&!Pa(this))return this.f&=-2,!0;var e=ae;try{Ia(this),ae=this;var t=this.x();(16&this.f||this.v!==t||this.i===0)&&(this.v=t,this.f&=-17,this.i++)}catch(r){this.v=r,this.f|=16,this.i++}return ae=e,Ra(this),this.f&=-2,!0};Kt.prototype.S=function(e){if(this.t===void 0){this.f|=36;for(var t=this.s;t!==void 0;t=t.n)t.S.S(t)}Ce.prototype.S.call(this,e)};Kt.prototype.U=function(e){if(this.t!==void 0&&(Ce.prototype.U.call(this,e),this.t===void 0)){this.f&=-33;for(var t=this.s;t!==void 0;t=t.n)t.S.U(t)}};Kt.prototype.N=function(){if(!(2&this.f)){this.f|=6;for(var e=this.t;e!==void 0;e=e.x)e.t.N()}};Object.defineProperty(Kt.prototype,"value",{get:function(){if(1&this.f)throw new Error("Cycle detected");var e=$a(this);if(this.h(),e!==void 0&&(e.i=this.i),16&this.f)throw this.v;return this.v}});function ta(e,t){return new Kt(e,t)}function ja(e){var t=e.u;if(e.u=void 0,typeof t=="function"){St++;var r=ae;ae=void 0;try{t()}catch(n){throw e.f&=-2,e.f|=8,ho(e),n}finally{ae=r,xn()}}}function ho(e){for(var t=e.s;t!==void 0;t=t.n)t.S.U(t);e.x=void 0,e.s=void 0,ja(e)}function eu(e){if(ae!==this)throw new Error("Out-of-order effect");Ra(this),ae=e,this.f&=-2,8&this.f&&ho(this),xn()}function pr(e,t){this.x=e,this.u=void 0,this.s=void 0,this.o=void 0,this.f=32,this.name=t==null?void 0:t.name}pr.prototype.c=function(){var e=this.S();try{if(8&this.f||this.x===void 0)return;var t=this.x();typeof t=="function"&&(this.u=t)}finally{e()}};pr.prototype.S=function(){if(1&this.f)throw new Error("Cycle detected");this.f|=1,this.f&=-9,ja(this),Ia(this),St++;var e=ae;return ae=this,eu.bind(this,e)};pr.prototype.N=function(){2&this.f||(this.f|=2,this.o=kr,kr=this)};pr.prototype.d=function(){this.f|=8,1&this.f||ho(this)};pr.prototype.dispose=function(){this.d()};function qt(e,t){var r=new pr(e,t);try{r.c()}catch(o){throw r.d(),o}var n=r.d.bind(r);return n[Symbol.dispose]=n,n}var Fa,vo,eo,Ua=[];qt(function(){Fa=this.N})();function fr(e,t){K[e]=t.bind(null,K[e]||function(){})}function _n(e){eo&&eo(),eo=e&&e.S()}function Na(e){var t=this,r=e.data,n=ru(r);n.value=r;var o=ur(function(){for(var s=t,c=t.__v;c=c.__;)if(c.__c){c.__c.__$f|=4;break}var l=ta(function(){var m=n.value.value;return m===0?0:m===!0?"":m||""}),u=ta(function(){return!Array.isArray(l.value)&&!pa(l.value)}),p=qt(function(){if(this.N=Da,u.value){var m=l.value;s.__v&&s.__v.__e&&s.__v.__e.nodeType===3&&(s.__v.__e.data=m)}}),d=t.__$u.d;return t.__$u.d=function(){p(),d.call(this)},[u,l]},[]),i=o[0],a=o[1];return i.value?a.peek():a.value}Na.displayName="ReactiveTextNode";Object.defineProperties(Ce.prototype,{constructor:{configurable:!0,value:void 0},type:{configurable:!0,value:Na},props:{configurable:!0,get:function(){return{data:this}}},__b:{configurable:!0,value:1}});fr("__b",function(e,t){if(typeof t.type=="function"&&typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.exitComponent(),typeof t.type=="string"){var r,n=t.props;for(var o in n)if(o!=="children"){var i=n[o];i instanceof Ce&&(r||(t.__np=r={}),r[o]=i,n[o]=i.peek())}}e(t)});fr("__r",function(e,t){if(typeof t.type=="function"&&typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.enterComponent(t),t.type!==ft){_n();var r,n=t.__c;n&&(n.__$f&=-2,(r=n.__$u)===void 0&&(n.__$u=r=(function(o){var i;return qt(function(){i=this}),i.c=function(){n.__$f|=1,n.setState({})},i})())),vo=n,_n(r)}e(t)});fr("__e",function(e,t,r,n){typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.exitComponent(),_n(),vo=void 0,e(t,r,n)});fr("diffed",function(e,t){typeof t.type=="function"&&typeof window<"u"&&window.__PREACT_SIGNALS_DEVTOOLS__&&window.__PREACT_SIGNALS_DEVTOOLS__.exitComponent(),_n(),vo=void 0;var r;if(typeof t.type=="string"&&(r=t.__e)){var n=t.__np,o=t.props;if(n){var i=r.U;if(i)for(var a in i){var s=i[a];s!==void 0&&!(a in n)&&(s.d(),i[a]=void 0)}else i={},r.U=i;for(var c in n){var l=i[c],u=n[c];l===void 0?(l=tu(r,c,u,o),i[c]=l):l.o(u,o)}}}e(t)});function tu(e,t,r,n){var o=t in e&&e.ownerSVGElement===void 0,i=Ot(r);return{o:function(a,s){i.value=a,n=s},d:qt(function(){this.N=Da;var a=i.value.value;n[t]!==a&&(n[t]=a,o?e[t]=a:a?e.setAttribute(t,a):e.removeAttribute(t))})}}fr("unmount",function(e,t){if(typeof t.type=="string"){var r=t.__e;if(r){var n=r.U;if(n){r.U=void 0;for(var o in n){var i=n[o];i&&i.d()}}}}else{var a=t.__c;if(a){var s=a.__$u;s&&(a.__$u=void 0,s.d())}}e(t)});fr("__h",function(e,t,r,n){(n<3||n===9)&&(t.__$f|=2),e(t,r,n)});at.prototype.shouldComponentUpdate=function(e,t){var r=this.__$u,n=r&&r.s!==void 0;for(var o in t)return!0;if(this.__f||typeof this.u=="boolean"&&this.u===!0){var i=2&this.__$f;if(!(n||i||4&this.__$f)||1&this.__$f)return!0}else if(!(n||4&this.__$f)||3&this.__$f)return!0;for(var a in e)if(a!=="__source"&&e[a]!==this.props[a])return!0;for(var s in this.props)if(!(s in e))return!0;return!1};function ru(e,t){return bn(function(){return Ot(e,t)})[0]}var nu=function(e){queueMicrotask(function(){queueMicrotask(e)})};function ou(){Ql(function(){for(var e;e=Ua.shift();)Fa.call(e)})}function Da(){Ua.push(this)===1&&(K.requestAnimationFrame||nu)(ou)}var ao=[0];for(let e=0;e<32;e++)ao.push(ao[e]|1<>>5]>>>e&1}set(e){this.data[e>>>5]|=1<<(e&31)}forEach(e){let t=this.size&31;for(let r=0;r{var r;return(r=t.tags)==null?void 0:r.length})&&(matchMedia("(max-width: 768px)").matches||Wa())}function Dt(){Qe.value=He(H({},Qe.value),{hideSearch:!Qe.value.hideSearch})}function Wa(){Qe.value=He(H({},Qe.value),{hideFilters:!Qe.value.hideFilters})}function dn(){return Qe.value.selectedItem}function so(e){Qe.value=He(H({},Qe.value),{selectedItem:e})}function su(){var e,t;return(t=(e=lr.value)==null?void 0:e.items)!=null?t:[]}function wn(){return typeof Oe.value.input=="string"?Oe.value.input:""}function Va(e){let t=za();e.length&&!t.length?Oe.value=He(H({},Oe.value),{page:void 0,input:e}):!e.length&&t.length?Oe.value=He(H({},Oe.value),{page:void 0,input:{type:"operator",data:{operator:"not",operands:[]}}}):Oe.value=He(H({},Oe.value),{page:void 0,input:e})}function cu(){typeof it.value.pagination.next<"u"&&(Oe.value=He(H({},Oe.value),{page:it.value.pagination.next}))}function lu(e){let t=Oe.value.filter.input;if("type"in t&&t.type==="operator"){for(let r of t.data.operands)if("type"in r&&r.type==="value"&&typeof r.data.value=="string"&&r.data.value===e)return!0}return!1}function za(){let e=Oe.value.filter.input,t=[];if("type"in e&&e.type==="operator")for(let r of e.data.operands)"type"in r&&r.type==="value"&&typeof r.data.value=="string"&&t.push(r.data.value);return t}function uu(e){let t=Oe.value.filter.input,r=[];if("type"in t&&t.type==="operator")for(let n of t.data.operands)"type"in n&&n.type==="value"&&typeof n.data.value=="string"&&r.push(n.data.value);if(r.includes(e)){let n=r.indexOf(e);n>-1&&r.splice(n,1)}else r.push(e);Oe.value=He(H({},Oe.value),{page:void 0,filter:He(H({},Oe.value.filter),{input:{type:"operator",data:{operator:"and",operands:r.map(n=>({type:"value",data:{field:"tags",value:n}}))}}})}),Va(wn())}function pu(){return it.value.items}function fu(){return it.value.total}function mu(){var e;for(let t of(e=it.value.aggregations)!=null?e:[])if(t.type==="term")return t.data.value;return[]}function sr(){return Qe.value.hideSearch}function du(){return Qe.value.hideFilters}function qa(){var e;return(e=Ka.value.highlight)!=null?e:!1}var Qe=Ot({hideSearch:!0,hideFilters:!0,selectedItem:0}),Ka=Ot({}),lr=Ot(),na=Ot(),Oe=Ot({input:"",filter:{input:{type:"operator",data:{operator:"and",operands:[]}},aggregation:{input:[{type:"term",data:{field:"tags"}}]}}}),it=Ot({items:[],query:{select:{documents:new ra(0),terms:new ra(0)},values:[]},pagination:{total:0}});function hu(e,t,r){for(let n=0;tr&&t(0,o,r,r=i);continue;case 62:e.charCodeAt(r+1)===47?t(2,--o,r,r=i+1):hu(e,r,n)?t(3,o,r,r=i+1):t(1,o++,r,r=i+1)}i>r&&t(0,o,r,i)}function bu(e,t=0,r=e.length){let n=++t;e:for(let l=0;n{let i=[],a=[],{onElement:s,onText:c=gu}=typeof r=="function"?{onElement:r}:r,l=0,u=0;return e(t,(p,d,m,h)=>{if(p===0)i[l++]=c(t,m,h),a[u++]={value:null,depth:d};else if(p&1&&(a[u++]={value:bu(t,m,h),depth:d}),p&2)for(let v=0;u>=0;v++){let{value:x,depth:w}=a[--u];if(w>d)continue;let E=i.slice(l-=v,l+v);i[l++]=s(x,E),u++;break}},n,o),i.slice(0,l)}}function yu(e){return e.replace(/[&<>]/g,t=>{switch(t.charCodeAt(0)){case 38:return"&";case 60:return"<";case 62:return">"}})}function hn(e){return e.replace(/&(amp|[lg]t);/g,t=>{switch(t.charCodeAt(1)){case 97:return"&";case 108:return"<";case 103:return">"}})}function xu(e,t){return{start:e.start+t,end:e.end+t,value:e.value}}function wu(e,t,r){return e.slice(t,r)}function Eu(e){let{onHighlight:t,onText:r=wu}=typeof e=="function"?{onHighlight:e}:e;return(n,o,i=0,a=n.length)=>{var l;let s=[],c=(l=o==null?void 0:o.ranges)!=null?l:[];for(let u=0,p=i;ua)break;let m=c[u].end;if(mi&&s.push(r(n,i,d));let{value:h}=c[u];s.push(t(n,{start:d,end:i=m,value:h}))}return i{let o=n.data;switch(o.type){case 1:na.value=!0;break;case 3:typeof o.data.pagination.prev<"u"?it.value=He(H({},it.value),{pagination:o.data.pagination,items:[...it.value.items,...o.data.items]}):(it.value=o.data,so(0));break}},qt(()=>{lr.value&&r.postMessage({type:0,data:lr.value})}),qt(()=>{na.value&&r.postMessage({type:2,data:Oe.value})})}var oa={container:"p",hidden:"m"};function ku(e){return z("div",{class:zt(oa.container,{[oa.hidden]:e.hidden}),onClick:()=>Dt()})}var ia={container:"r",disabled:"c"};function co(e){return z("button",{class:zt(ia.container,{[ia.disabled]:!e.onClick}),onClick:e.onClick,children:e.children})}var aa=e=>e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase(),Au=e=>e.replace(/^([A-Z])|[\s-_]+(\w)/g,(t,r,n)=>n?n.toUpperCase():r.toLowerCase()),sa=e=>{let t=Au(e);return t.charAt(0).toUpperCase()+t.slice(1)},Cu=(...e)=>e.filter((t,r,n)=>!!t&&t.trim()!==""&&n.indexOf(t)===r).join(" ").trim(),Hu={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"},$u=c=>{var l=c,{color:e="currentColor",size:t=24,strokeWidth:r=2,absoluteStrokeWidth:n,children:o,iconNode:i,class:a=""}=l,s=gr(l,["color","size","strokeWidth","absoluteStrokeWidth","children","iconNode","class"]);return Wt("svg",H(He(H({},Hu),{width:String(t),height:t,stroke:e,"stroke-width":n?Number(r)*24/Number(t):r,class:["lucide",a].join(" ")}),s),[...i.map(([u,p])=>Wt(u,p)),...Cr(o)])},bo=(e,t)=>{let r=a=>{var s=a,{class:n="",children:o}=s,i=gr(s,["class","children"]);return Wt($u,He(H({},i),{iconNode:t,class:Cu(`lucide-${aa(sa(e))}`,`lucide-${aa(e)}`,n)}),o)};return r.displayName=sa(e),r},Pu=bo("corner-down-left",[["path",{d:"M20 4v7a4 4 0 0 1-4 4H4",key:"6o5b7l"}],["path",{d:"m9 10-5 5 5 5",key:"1kshq7"}]]),Iu=bo("list-filter",[["path",{d:"M2 5h20",key:"1fs1ex"}],["path",{d:"M6 12h12",key:"8npq4p"}],["path",{d:"M9 19h6",key:"456am0"}]]),Ru=bo("search",[["path",{d:"m21 21-4.34-4.34",key:"14j7rj"}],["circle",{cx:"11",cy:"11",r:"8",key:"4ej97u"}]]),Gx=hl(vl(),1);function ju({threshold:e=0,root:t=null,rootMargin:r="0%",freezeOnceVisible:n=!1,initialIsIntersecting:o=!1,onChange:i}={}){var a;let[s,c]=bn(null),[l,u]=bn(()=>({isIntersecting:o,entry:void 0})),p=Vt();p.current=i;let d=((a=l.entry)==null?void 0:a.isIntersecting)&&n;mt(()=>{if(!s||!("IntersectionObserver"in window)||d)return;let v,x=new IntersectionObserver(w=>{let E=Array.isArray(x.thresholds)?x.thresholds:[x.thresholds];w.forEach(_=>{let de=_.isIntersecting&&E.some(be=>_.intersectionRatio>=be);u({isIntersecting:de,entry:_}),p.current&&p.current(de,_),de&&n&&v&&(v(),v=void 0)})},{threshold:e,root:t,rootMargin:r});return x.observe(s),()=>{x.disconnect()}},[s,JSON.stringify(e),t,r,d,n]);let m=Vt(null);mt(()=>{var v;!s&&(v=l.entry)!=null&&v.target&&!n&&!d&&m.current!==l.entry.target&&(m.current=l.entry.target,u({isIntersecting:o,entry:void 0}))},[s,l.entry,n,d,o]);let h=[c,!!l.isIntersecting,l.entry];return h.ref=h[0],h.isIntersecting=h[1],h.entry=h[2],h}var lt={container:"n",hidden:"l",content:"u",pop:"d",badge:"y",sidebar:"i",controls:"w",results:"k",loadmore:"z"};function Fu(e){let{isIntersecting:t,ref:r}=ju({threshold:0});mt(()=>{t&&cu()},[t]);let n=Vt(null);mt(()=>{n.current&&typeof Oe.value.page>"u"&&n.current.scrollTo({top:0,behavior:"smooth"})},[Oe.value]);let o=za();return z("div",{class:zt(lt.container,{[lt.hidden]:e.hidden}),children:[z("div",{class:lt.content,children:[z("div",{class:lt.controls,children:[z(co,{onClick:Dt,children:z(Ru,{})}),z(Nu,{focus:!e.hidden}),z(co,{onClick:Wa,children:[z(Iu,{}),o.length>0&&z("span",{class:lt.badge,children:o.length})]})]}),z("div",{class:lt.results,ref:n,children:[z(Du,{keyboard:!e.hidden}),z("div",{class:lt.loadmore,ref:r})]})]}),z("div",{class:zt(lt.sidebar,{[lt.hidden]:du()}),children:z(Uu,{})})]})}var Tt={container:"X",list:"j",heading:"F",title:"I",item:"o",active:"g",value:"R",count:"q"};function Uu(e){let t=mu();return t.sort((r,n)=>n.node.count-r.node.count),z("div",{class:Tt.container,children:[z("h3",{class:Tt.heading,children:"Filters"}),z("h4",{class:Tt.title,children:"Tags"}),z("ol",{class:Tt.list,children:t.map(r=>z("li",{class:zt(Tt.item,{[Tt.active]:lu(r.node.value)}),onClick:()=>uu(r.node.value),children:[z("span",{class:Tt.value,children:r.node.value}),z("span",{class:Tt.count,children:r.node.count})]}))})]})}var ca={container:"f"};function Nu(e){let t=Vt(null);return mt(()=>{var r,n;e.focus?(r=t.current)==null||r.focus():(n=t.current)==null||n.blur()},[e.focus]),z("div",{class:ca.container,children:z("input",{ref:t,type:"text",class:ca.content,value:hn(wn()),onInput:r=>Va(yu(r.currentTarget.value)),autocapitalize:"off",autocomplete:"off",autocorrect:"off",placeholder:"Search",spellcheck:!1,role:"combobox"})})}var ut={container:"b",heading:"A",item:"a",active:"h",wrapper:"B",actions:"s",title:"x",path:"t"};function Ga(){let[e,t]=bn(!1);return mt(()=>{let r=()=>t(!0),n=()=>t(!1);return document.addEventListener("compositionstart",r),document.addEventListener("compositionend",n),()=>{document.removeEventListener("compositionstart",r),document.removeEventListener("compositionend",n)}},[]),e}function Du(e){var s;let t=su(),r=pu(),n=dn(),o=Vt([]),i=Ga();mt(()=>{let c=o.current[n];c&&c.scrollIntoView({block:"center",behavior:"smooth"})},[n]),Aa(e.keyboard,c=>{if(i)return;let l=dn();c.key==="ArrowDown"?(c.preventDefault(),so(Math.min(l+1,r.length-1))):c.key==="ArrowUp"&&(c.preventDefault(),so(Math.max(l-1,0)))},[e.keyboard,i]);let a=(s=fu())!=null?s:0;return z(ft,{children:[r.length>0&&z("h3",{class:ut.heading,children:[z("span",{class:ut.bubble,children:new Intl.NumberFormat("en-US").format(a)})," ","results"]}),z("ol",{class:ut.container,children:r.map((c,l)=>{var m;let u=Ba(t[c.id].title,c.matches.find(({field:h})=>h==="title")),p=Mu((m=t[c.id].path)!=null?m:[],c.matches.find(({field:h})=>h==="path")),d=t[c.id].location;if(qa()){let h=encodeURIComponent(wn()),[v,x]=d.split("#",2);d=`${v}?h=${h.replace(/%20/g,"+")}`,typeof x<"u"&&(d+=`#${x}`)}return z("li",{children:z("a",{ref:h=>{o.current[l]=h},href:d,onClick:()=>Dt(),class:zt(ut.item,{[ut.active]:l===dn()}),children:[z("div",{class:ut.wrapper,children:[z("h2",{class:ut.title,children:u}),z("menu",{class:ut.path,children:p.map(h=>z("li",{children:h}))})]}),z("nav",{class:ut.actions,children:z(co,{children:z(Pu,{})})})]})})})})]})}var Wu={container:"e"};function Vu(e){let t=Ga();return Aa(!0,r=>{var n,o,i,a,s;if(!t)if((r.metaKey||r.ctrlKey)&&r.key==="k")r.preventDefault(),Dt();else if((r.metaKey||r.ctrlKey)&&r.key==="j")document.body.classList.toggle("dark");else if(r.key==="Enter"&&!sr()){r.preventDefault();let c=dn(),l=(o=(n=it.value)==null?void 0:n.items[c])==null?void 0:o.id;if((a=(i=lr.value)==null?void 0:i.items[l])!=null&&a.location){Dt();let u=(s=lr.value)==null?void 0:s.items[l].location;if(qa()){let p=encodeURIComponent(wn()),[d,m]=u.split("#",2);u=`${d}?h=${p.replace(/%20/g,"+")}`,typeof m<"u"&&(u+=`#${m}`)}window.location.href=u}}else r.key==="Escape"&&!sr()&&(r.preventDefault(),Dt())},[t]),z("div",{class:Wu.container,children:[z(ku,{hidden:sr()}),z(Fu,{hidden:sr()})]})}function Ja(e,t){au(e),El(z(Vu,{}),t)}function go(){Dt()}function zu(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function qu(){return R(b(window,"compositionstart").pipe(f(()=>!0)),b(window,"compositionend").pipe(f(()=>!1))).pipe(J(!1))}function Xa(){let e=b(window,"keydown").pipe(f(t=>({mode:sr()?"global":"search",type:t.key,meta:t.ctrlKey||t.metaKey,claim(){t.preventDefault(),t.stopPropagation()}})),L(({mode:t,type:r})=>{if(t==="global"){let n=xt();if(typeof n!="undefined")return!zu(n,r)}return!0}),xe());return qu().pipe(g(t=>t?y:e))}function Ye(){return new URL(location.href)}function dt(e,t=!1){if(X("navigation.instant")&&!t){let r=A("a",{href:e.href});document.body.appendChild(r),r.click(),r.remove()}else location.href=e.href}function Za(){return new I}function Qa(){return location.hash.slice(1)}function es(e){let t=A("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function _o(e){return R(b(window,"hashchange"),e).pipe(f(Qa),J(Qa()),L(t=>t.length>0),f(decodeURIComponent),se(1))}function ts(e){return _o(e).pipe(f(t=>we(`[id="${t}"]`)),L(t=>typeof t!="undefined"))}function Ir(e){let t=matchMedia(e);return an(r=>t.addListener(()=>r(t.matches))).pipe(J(t.matches))}function rs(){let e=matchMedia("print");return R(b(window,"beforeprint").pipe(f(()=>!0)),b(window,"afterprint").pipe(f(()=>!1))).pipe(J(e.matches))}function yo(e,t){return e.pipe(g(r=>r?t():y))}function xo(e,t){return new U(r=>{let n=new XMLHttpRequest;return n.open("GET",`${e}`),n.responseType="blob",n.addEventListener("load",()=>{n.status>=200&&n.status<300?(r.next(n.response),r.complete()):r.error(new Error(n.statusText))}),n.addEventListener("error",()=>{r.error(new Error("Network error"))}),n.addEventListener("abort",()=>{r.complete()}),typeof(t==null?void 0:t.progress$)!="undefined"&&(n.addEventListener("progress",o=>{var i;if(o.lengthComputable)t.progress$.next(o.loaded/o.total*100);else{let a=(i=n.getResponseHeader("Content-Length"))!=null?i:0;t.progress$.next(o.loaded/+a*100)}}),t.progress$.next(5)),n.send(),()=>n.abort()})}function et(e,t){return xo(e,t).pipe(g(r=>r.text()),f(r=>JSON.parse(r)),se(1))}function En(e,t){let r=new DOMParser;return xo(e,t).pipe(g(n=>n.text()),f(n=>r.parseFromString(n,"text/html")),se(1))}function ns(e,t){let r=new DOMParser;return xo(e,t).pipe(g(n=>n.text()),f(n=>r.parseFromString(n,"text/xml")),se(1))}var wo={drawer:G("[data-md-toggle=drawer]"),search:G("[data-md-toggle=search]")};function Eo(e,t){wo[e].checked!==t&&wo[e].click()}function Tn(e){let t=wo[e];return b(t,"change").pipe(f(()=>t.checked),J(t.checked))}function os(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function is(){return R(b(window,"scroll",{passive:!0}),b(window,"resize",{passive:!0})).pipe(f(os),J(os()))}function as(){return{width:innerWidth,height:innerHeight}}function ss(){return b(window,"resize",{passive:!0}).pipe(f(as),J(as()))}function cs(){return re([is(),ss()]).pipe(f(([e,t])=>({offset:e,size:t})),se(1))}function Sn(e,{viewport$:t,header$:r}){let n=t.pipe(fe("size")),o=re([n,r]).pipe(f(()=>wt(e)));return re([r,t,o]).pipe(f(([{height:i},{offset:a,size:s},{x:c,y:l}])=>({offset:{x:a.x-c,y:a.y-l+i},size:s})))}var Ku=G("#__config"),mr=JSON.parse(Ku.textContent);mr.base=`${new URL(mr.base,Ye())}`;function Ue(){return mr}function X(e){return mr.features.includes(e)}function Bt(e,t){return typeof t!="undefined"?mr.translations[e].replace("#",t.toString()):mr.translations[e]}function ht(e,t=document){return G(`[data-md-component=${e}]`,t)}function Te(e,t=document){return P(`[data-md-component=${e}]`,t)}function Bu(e){let t=G(".md-typeset > :first-child",e);return b(t,"click",{once:!0}).pipe(f(()=>G(".md-typeset",e)),f(r=>({hash:__md_hash(r.innerHTML)})))}function ls(e){if(!X("announce.dismiss")||!e.childElementCount)return y;if(!e.hidden){let t=G(".md-typeset",e);__md_hash(t.innerHTML)===__md_get("__announce")&&(e.hidden=!0)}return j(()=>{let t=new I;return t.subscribe(({hash:r})=>{e.hidden=!0,__md_set("__announce",r)}),Bu(e).pipe($(r=>t.next(r)),V(()=>t.complete()),f(r=>H({ref:e},r)))})}function Yu(e,{target$:t}){return t.pipe(f(r=>({hidden:r!==e})))}function us(e,t){let r=new I;return r.subscribe(({hidden:n})=>{e.hidden=n}),Yu(e,t).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))}function To(e,t){return t==="inline"?A("div",{class:"md-tooltip md-tooltip--inline",id:e,role:"tooltip"},A("div",{class:"md-tooltip__inner md-typeset"})):A("div",{class:"md-tooltip",id:e,role:"tooltip"},A("div",{class:"md-tooltip__inner md-typeset"}))}function On(...e){return A("div",{class:"md-tooltip2",role:"dialog"},A("div",{class:"md-tooltip2__inner md-typeset"},e))}function ps(...e){return A("div",{class:"md-tooltip2",role:"tooltip"},A("div",{class:"md-tooltip2__inner md-typeset"},e))}function fs(e,t){if(t=t?`${t}_annotation_${e}`:void 0,t){let r=t?`#${t}`:void 0;return A("aside",{class:"md-annotation",tabIndex:0},To(t),A("a",{href:r,class:"md-annotation__index",tabIndex:-1},A("span",{"data-md-annotation-id":e})))}else return A("aside",{class:"md-annotation",tabIndex:0},To(t),A("span",{class:"md-annotation__index",tabIndex:-1},A("span",{"data-md-annotation-id":e})))}function ms(e){return A("button",{class:"md-code__button",title:Bt("clipboard.copy"),"data-clipboard-target":`#${e} > code`,"data-md-type":"copy"})}function ds(){return A("button",{class:"md-code__button",title:"Toggle line selection","data-md-type":"select"})}function hs(){return A("nav",{class:"md-code__nav"})}var Xu=_r(So());function bs(e){return A("ul",{class:"md-source__facts"},Object.entries(e).map(([t,r])=>A("li",{class:`md-source__fact md-source__fact--${t}`},typeof r=="number"?Li(r):r)))}function Oo(e){let t=`tabbed-control tabbed-control--${e}`;return A("div",{class:t,hidden:!0},A("button",{class:"tabbed-button",tabIndex:-1,"aria-hidden":"true"}))}function gs(e){return A("div",{class:"md-typeset__scrollwrap"},A("div",{class:"md-typeset__table"},e))}function Zu(e){var n;let t=Ue(),r=new URL(`../${e.version}/`,t.base);return A("li",{class:"md-version__item"},A("a",{href:`${r}`,class:"md-version__link"},e.title,((n=t.version)==null?void 0:n.alias)&&e.aliases.length>0&&A("span",{class:"md-version__alias"},e.aliases[0])))}function _s(e,t){var n;let r=Ue();return e=e.filter(o=>{var i;return!((i=o.properties)!=null&&i.hidden)}),A("div",{class:"md-version"},A("button",{class:"md-version__current","aria-label":Bt("select.version")},t.title,((n=r.version)==null?void 0:n.alias)&&t.aliases.length>0&&A("span",{class:"md-version__alias"},t.aliases[0])),A("ul",{class:"md-version__list"},e.map(Zu)))}var Qu=0;function ep(e,t=250){let r=re([ir(e),Ft(e,t)]).pipe(f(([o,i])=>o||i),ie()),n=j(()=>Ai(e)).pipe(oe(Ut),Lr(1),Ze(r),f(()=>Ci(e)));return r.pipe(Sr(o=>o),g(()=>re([r,n])),f(([o,i])=>({active:o,offset:i})),xe())}function Rr(e,t,r=250){let{content$:n,viewport$:o}=t,i=`__tooltip2_${Qu++}`;return j(()=>{let a=new I,s=new Un(!1);a.pipe(he(),ye(!1)).subscribe(s);let c=s.pipe(Tr(u=>Ve(+!u*250,Wn)),ie(),g(u=>u?n:y),$(u=>u.id=i),xe());re([a.pipe(f(({active:u})=>u)),c.pipe(g(u=>Ft(u,250)),J(!1))]).pipe(f(u=>u.some(p=>p))).subscribe(s);let l=s.pipe(L(u=>u),pe(c,o),f(([u,p,{size:d}])=>{let m=e.getBoundingClientRect(),h=m.width/2;if(p.role==="tooltip")return{x:h,y:8+m.height};if(m.y>=d.height/2){let{height:v}=Ae(p);return{x:h,y:-16-v}}else return{x:h,y:16+m.height}}));return re([c,a,l]).subscribe(([u,{offset:p},d])=>{u.style.setProperty("--md-tooltip-host-x",`${p.x}px`),u.style.setProperty("--md-tooltip-host-y",`${p.y}px`),u.style.setProperty("--md-tooltip-x",`${d.x}px`),u.style.setProperty("--md-tooltip-y",`${d.y}px`),u.classList.toggle("md-tooltip2--top",d.y<0),u.classList.toggle("md-tooltip2--bottom",d.y>=0)}),s.pipe(L(u=>u),pe(c,(u,p)=>p),L(u=>u.role==="tooltip")).subscribe(u=>{let p=Ae(G(":scope > *",u));u.style.setProperty("--md-tooltip-width",`${p.width}px`),u.style.setProperty("--md-tooltip-tail","0px")}),s.pipe(ie(),Ie(je),pe(c)).subscribe(([u,p])=>{p.classList.toggle("md-tooltip2--active",u)}),re([s.pipe(L(u=>u)),c]).subscribe(([u,p])=>{p.role==="dialog"?(e.setAttribute("aria-controls",i),e.setAttribute("aria-haspopup","dialog")):e.setAttribute("aria-describedby",i)}),s.pipe(L(u=>!u)).subscribe(()=>{e.removeAttribute("aria-controls"),e.removeAttribute("aria-describedby"),e.removeAttribute("aria-haspopup")}),ep(e,r).pipe($(u=>a.next(u)),V(()=>a.complete()),f(u=>H({ref:e},u)))})}function Ge(e,{viewport$:t},r=document.body){return Rr(e,{content$:new U(n=>{let o=e.title,i=ps(o);return n.next(i),e.removeAttribute("title"),r.append(i),()=>{i.remove(),e.setAttribute("title",o)}}),viewport$:t},0)}function tp(e,t){let r=j(()=>re([Hi(e),Ut(t)])).pipe(f(([{x:n,y:o},i])=>{let{width:a,height:s}=Ae(e);return{x:n-i.x+a/2,y:o-i.y+s/2}}));return ir(e).pipe(g(n=>r.pipe(f(o=>({active:n,offset:o})),Me(+!n||1/0))))}function ys(e,t,{target$:r}){let[n,o]=Array.from(e.children);return j(()=>{let i=new I,a=i.pipe(he(),ye(!0));return i.subscribe({next({offset:s}){e.style.setProperty("--md-tooltip-x",`${s.x}px`),e.style.setProperty("--md-tooltip-y",`${s.y}px`)},complete(){e.style.removeProperty("--md-tooltip-x"),e.style.removeProperty("--md-tooltip-y")}}),Et(e).pipe(Q(a)).subscribe(s=>{e.toggleAttribute("data-md-visible",s)}),R(i.pipe(L(({active:s})=>s)),i.pipe(Be(250),L(({active:s})=>!s))).subscribe({next({active:s}){s?e.prepend(n):n.remove()},complete(){e.prepend(n)}}),i.pipe(Xe(16,je)).subscribe(({active:s})=>{n.classList.toggle("md-tooltip--active",s)}),i.pipe(Lr(125,je),L(()=>!!e.offsetParent),f(()=>e.offsetParent.getBoundingClientRect()),f(({x:s})=>s)).subscribe({next(s){s?e.style.setProperty("--md-tooltip-0",`${-s}px`):e.style.removeProperty("--md-tooltip-0")},complete(){e.style.removeProperty("--md-tooltip-0")}}),b(o,"click").pipe(Q(a),L(s=>!(s.metaKey||s.ctrlKey))).subscribe(s=>{s.stopPropagation(),s.preventDefault()}),b(o,"mousedown").pipe(Q(a),pe(i)).subscribe(([s,{active:c}])=>{var l;if(s.button!==0||s.metaKey||s.ctrlKey)s.preventDefault();else if(c){s.preventDefault();let u=e.parentElement.closest(".md-annotation");u instanceof HTMLElement?u.focus():(l=xt())==null||l.blur()}}),r.pipe(Q(a),L(s=>s===n),It(125)).subscribe(()=>e.focus()),tp(e,t).pipe($(s=>i.next(s)),V(()=>i.complete()),f(s=>H({ref:e},s)))})}function rp(e){let t=Ue();if(e.tagName!=="CODE")return[e];let r=[".c",".c1",".cm"];if(t.annotate){let n=e.closest("[class|=language]");if(n)for(let o of Array.from(n.classList)){if(!o.startsWith("language-"))continue;let[,i]=o.split("-");i in t.annotate&&r.push(...t.annotate[i])}}return P(r.join(", "),e)}function np(e){let t=[];for(let r of rp(e)){let n=[],o=document.createNodeIterator(r,NodeFilter.SHOW_TEXT);for(let i=o.nextNode();i;i=o.nextNode())n.push(i);for(let i of n){let a;for(;a=/(\(\d+\))(!)?/.exec(i.textContent);){let[,s,c]=a;if(typeof c=="undefined"){let l=i.splitText(a.index);i=l.splitText(s.length),t.push(l)}else{i.textContent=s,t.push(i);break}}}}return t}function xs(e,t){t.append(...Array.from(e.childNodes))}function Ln(e,t,{target$:r,print$:n}){let o=t.closest("[id]"),i=o==null?void 0:o.id,a=new Map;for(let s of np(t)){let[,c]=s.textContent.match(/\((\d+)\)/);we(`:scope > li:nth-child(${c})`,e)&&(a.set(c,fs(c,i)),s.replaceWith(a.get(c)))}return a.size===0?y:j(()=>{let s=new I,c=s.pipe(he(),ye(!0)),l=[];for(let[u,p]of a)l.push([G(".md-typeset",p),G(`:scope > li:nth-child(${u})`,e)]);return n.pipe(Q(c)).subscribe(u=>{e.hidden=!u,e.classList.toggle("md-annotation-list",u);for(let[p,d]of l)u?xs(p,d):xs(d,p)}),R(...[...a].map(([,u])=>ys(u,t,{target$:r}))).pipe(V(()=>s.complete()),xe())})}function ws(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return ws(t)}}function Es(e,t){return j(()=>{let r=ws(e);return typeof r!="undefined"?Ln(r,e,t):y})}var Ss=_r(Mo());var op=0,Ts=R(b(window,"keydown").pipe(f(()=>!0)),R(b(window,"keyup"),b(window,"contextmenu")).pipe(f(()=>!1))).pipe(J(!1),se(1));function Os(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return Os(t)}}function ip(e){return Re(e).pipe(f(({width:t})=>({scrollable:Mr(e).width>t})),fe("scrollable"))}function Ls(e,t){let{matches:r}=matchMedia("(hover)"),n=j(()=>{let o=new I,i=o.pipe(Gn(1));o.subscribe(({scrollable:m})=>{m&&r?e.setAttribute("tabindex","0"):e.removeAttribute("tabindex")});let a=[],s=e.closest("pre"),c=s.closest("[id]"),l=c?c.id:op++;s.id=`__code_${l}`;let u=[],p=e.closest(".highlight");if(p instanceof HTMLElement){let m=Os(p);if(typeof m!="undefined"&&(p.classList.contains("annotate")||X("content.code.annotate"))){let h=Ln(m,e,t);u.push(Re(p).pipe(Q(i),f(({width:v,height:x})=>v&&x),ie(),g(v=>v?h:y)))}}let d=P(":scope > span[id]",e);if(d.length&&(e.classList.add("md-code__content"),e.closest(".select")||X("content.code.select")&&!e.closest(".no-select"))){let m=+d[0].id.split("-").pop(),h=ds();a.push(h),X("content.tooltips")&&u.push(Ge(h,{viewport$}));let v=b(h,"click").pipe(Or(M=>!M,!1),$(()=>h.blur()),xe());v.subscribe(M=>{h.classList.toggle("md-code__button--active",M)});let x=me(d).pipe(oe(M=>Ft(M).pipe(f(O=>[M,O]))));v.pipe(g(M=>M?x:y)).subscribe(([M,O])=>{let N=we(".hll.select",M);if(N&&!O)N.replaceWith(...Array.from(N.childNodes));else if(!N&&O){let ee=document.createElement("span");ee.className="hll select",ee.append(...Array.from(M.childNodes).slice(1)),M.append(ee)}});let w=me(d).pipe(oe(M=>b(M,"mousedown").pipe($(O=>O.preventDefault()),f(()=>M)))),E=v.pipe(g(M=>M?w:y),pe(Ts),f(([M,O])=>{var ee;let N=d.indexOf(M)+m;if(O===!1)return[N,N];{let le=P(".hll",e).map(ce=>d.indexOf(ce.parentElement)+m);return(ee=window.getSelection())==null||ee.removeAllRanges(),[Math.min(N,...le),Math.max(N,...le)]}})),_=_o(y).pipe(L(M=>M.startsWith(`__codelineno-${l}-`)));_.subscribe(M=>{let[,,O]=M.split("-"),N=O.split(":").map(le=>+le-m+1);N.length===1&&N.push(N[0]);for(let le of P(".hll:not(.select)",e))le.replaceWith(...Array.from(le.childNodes));let ee=d.slice(N[0]-1,N[1]);for(let le of ee){let ce=document.createElement("span");ce.className="hll",ce.append(...Array.from(le.childNodes).slice(1)),le.append(ce)}}),_.pipe(Me(1),Ie(ge)).subscribe(M=>{if(M.includes(":")){let O=document.getElementById(M.split(":")[0]);O&&setTimeout(()=>{let N=O,ee=-64;for(;N!==document.body;)ee+=N.offsetTop,N=N.offsetParent;window.scrollTo({top:ee})},1)}});let be=me(P('a[href^="#__codelineno"]',p)).pipe(oe(M=>b(M,"click").pipe($(O=>O.preventDefault()),f(()=>M)))).pipe(Q(i),pe(Ts),f(([M,O])=>{let ee=+G(`[id="${M.hash.slice(1)}"]`).parentElement.id.split("-").pop();if(O===!1)return[ee,ee];{let le=P(".hll",e).map(ce=>+ce.parentElement.id.split("-").pop());return[Math.min(ee,...le),Math.max(ee,...le)]}}));R(E,be).subscribe(M=>{let O=`#__codelineno-${l}-`;M[0]===M[1]?O+=M[0]:O+=`${M[0]}:${M[1]}`,history.replaceState({},"",O),window.dispatchEvent(new HashChangeEvent("hashchange",{newURL:window.location.origin+window.location.pathname+O,oldURL:window.location.href}))})}if(Ss.default.isSupported()&&(e.closest(".copy")||X("content.code.copy")&&!e.closest(".no-copy"))){let m=ms(s.id);a.push(m),X("content.tooltips")&&u.push(Ge(m,{viewport$}))}if(a.length){let m=hs();m.append(...a),s.insertBefore(m,e)}return ip(e).pipe($(m=>o.next(m)),V(()=>o.complete()),f(m=>H({ref:e},m)),Rt(R(...u).pipe(Q(i))))});return X("content.lazy")?Et(e).pipe(L(o=>o),Me(1),g(()=>n)):n}function ap(e,{target$:t,print$:r}){let n=!0;return R(t.pipe(f(o=>o.closest("details:not([open])")),L(o=>e===o),f(()=>({action:"open",reveal:!0}))),r.pipe(L(o=>o||!n),$(()=>n=e.open),f(o=>({action:o?"open":"close"}))))}function Ms(e,t){return j(()=>{let r=new I;return r.subscribe(({action:n,reveal:o})=>{e.toggleAttribute("open",n==="open"),o&&e.scrollIntoView()}),ap(e,t).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))})}var ks=0,As=new Map;function sp(e){let t=document.createElement("h3");t.innerHTML=e.innerHTML;let r=[t],n=e.nextElementSibling;for(;n&&!(n instanceof HTMLHeadingElement);)r.push(n.cloneNode(!0)),n=n.nextElementSibling;return r}function cp(e,t){for(let r of P("[href], [src]",e))for(let n of["href","src"]){let o=r.getAttribute(n);if(o&&!/^(?:[a-z]+:)?\/\//i.test(o)){r[n]=new URL(r.getAttribute(n),t).toString();break}}for(let r of P("[name^=__], [for]",e))for(let n of["id","for","name"]){let o=r.getAttribute(n);o&&r.setAttribute(n,`${o}$preview_${ks}`)}return ks++,Y(e)}function lp(e){let t=As.get(e.toString());return t?Y(t):En(e).pipe(g(r=>cp(r,e)),f(r=>(As.set(e.toString(),r),r)))}function Cs(e,t){let{sitemap$:r}=t;if(!(e instanceof HTMLAnchorElement))return y;if(!(X("navigation.instant.preview")||e.hasAttribute("data-preview")))return y;e.removeAttribute("title");let n=re([ir(e),Ft(e).pipe(ke(1))]).pipe(f(([i,a])=>i||a),ie(),L(i=>i));return $t([r,n]).pipe(g(([i])=>{let a=new URL(e.href);return a.search=a.hash="",i.has(`${a}`)?Y(a):y}),g(i=>lp(i)),g(i=>{let a=e.hash?`article [id="${decodeURIComponent(e.hash.slice(1))}"]`:"article h1",s=we(a,i);return typeof s=="undefined"?y:Y(sp(s))})).pipe(g(i=>{let a=new U(s=>{let c=On(...i);return s.next(c),document.body.append(c),()=>c.remove()});return Rr(e,H({content$:a},t))}))}var Hs=".node circle,.node ellipse,.node path,.node polygon,.node rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}marker{fill:var(--md-mermaid-edge-color)!important}.edgeLabel .label rect{fill:#0000}.flowchartTitleText{fill:var(--md-mermaid-label-fg-color)}.label{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.label foreignObject{line-height:normal;overflow:visible}.label div .edgeLabel{color:var(--md-mermaid-label-fg-color)}.edgeLabel,.edgeLabel p,.label div .edgeLabel{background-color:var(--md-mermaid-label-bg-color)}.edgeLabel,.edgeLabel p{fill:var(--md-mermaid-label-bg-color);color:var(--md-mermaid-edge-color)}.edgePath .path,.flowchart-link{stroke:var(--md-mermaid-edge-color)}.edgePath .arrowheadPath{fill:var(--md-mermaid-edge-color);stroke:none}.cluster rect{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}.cluster span{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}g #flowchart-circleEnd,g #flowchart-circleStart,g #flowchart-crossEnd,g #flowchart-crossStart,g #flowchart-pointEnd,g #flowchart-pointStart{stroke:none}.classDiagramTitleText{fill:var(--md-mermaid-label-fg-color)}g.classGroup line,g.classGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.classGroup text{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.classLabel .box{fill:var(--md-mermaid-label-bg-color);background-color:var(--md-mermaid-label-bg-color);opacity:1}.classLabel .label{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.node .divider{stroke:var(--md-mermaid-node-fg-color)}.relation{stroke:var(--md-mermaid-edge-color)}.cardinality{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.cardinality text{fill:inherit!important}defs marker.marker.composition.class path,defs marker.marker.dependency.class path,defs marker.marker.extension.class path{fill:var(--md-mermaid-edge-color)!important;stroke:var(--md-mermaid-edge-color)!important}defs marker.marker.aggregation.class path{fill:var(--md-mermaid-label-bg-color)!important;stroke:var(--md-mermaid-edge-color)!important}.statediagramTitleText{fill:var(--md-mermaid-label-fg-color)}g.stateGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.stateGroup .state-title{fill:var(--md-mermaid-label-fg-color)!important;font-family:var(--md-mermaid-font-family)}g.stateGroup .composit{fill:var(--md-mermaid-label-bg-color)}.nodeLabel,.nodeLabel p{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}a .nodeLabel{text-decoration:underline}.node circle.state-end,.node circle.state-start,.start-state{fill:var(--md-mermaid-edge-color);stroke:none}.end-state-inner,.end-state-outer{fill:var(--md-mermaid-edge-color)}.end-state-inner,.node circle.state-end{stroke:var(--md-mermaid-label-bg-color)}.transition{stroke:var(--md-mermaid-edge-color)}[id^=state-fork] rect,[id^=state-join] rect{fill:var(--md-mermaid-edge-color)!important;stroke:none!important}.statediagram-cluster.statediagram-cluster .inner{fill:var(--md-default-bg-color)}.statediagram-cluster rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.statediagram-state rect.divider{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}defs #statediagram-barbEnd{stroke:var(--md-mermaid-edge-color)}[id^=entity] path,[id^=entity] rect{fill:var(--md-default-bg-color)}.relationshipLine{stroke:var(--md-mermaid-edge-color)}defs .marker.oneOrMore.er *,defs .marker.onlyOne.er *,defs .marker.zeroOrMore.er *,defs .marker.zeroOrOne.er *{stroke:var(--md-mermaid-edge-color)!important}text:not([class]):last-child{fill:var(--md-mermaid-label-fg-color)}.actor{fill:var(--md-mermaid-sequence-actor-bg-color);stroke:var(--md-mermaid-sequence-actor-border-color)}text.actor>tspan{fill:var(--md-mermaid-sequence-actor-fg-color);font-family:var(--md-mermaid-font-family)}line{stroke:var(--md-mermaid-sequence-actor-line-color)}.actor-man circle,.actor-man line{fill:var(--md-mermaid-sequence-actorman-bg-color);stroke:var(--md-mermaid-sequence-actorman-line-color)}.messageLine0,.messageLine1{stroke:var(--md-mermaid-sequence-message-line-color)}.note{fill:var(--md-mermaid-sequence-note-bg-color);stroke:var(--md-mermaid-sequence-note-border-color)}.loopText,.loopText>tspan,.messageText,.noteText>tspan{stroke:none;font-family:var(--md-mermaid-font-family)!important}.messageText{fill:var(--md-mermaid-sequence-message-fg-color)}.loopText,.loopText>tspan{fill:var(--md-mermaid-sequence-loop-fg-color)}.noteText>tspan{fill:var(--md-mermaid-sequence-note-fg-color)}#arrowhead path{fill:var(--md-mermaid-sequence-message-line-color);stroke:none}.loopLine{fill:var(--md-mermaid-sequence-loop-bg-color);stroke:var(--md-mermaid-sequence-loop-border-color)}.labelBox{fill:var(--md-mermaid-sequence-label-bg-color);stroke:none}.labelText,.labelText>span{fill:var(--md-mermaid-sequence-label-fg-color);font-family:var(--md-mermaid-font-family)}.sequenceNumber{fill:var(--md-mermaid-sequence-number-fg-color)}rect.rect{fill:var(--md-mermaid-sequence-box-bg-color);stroke:none}rect.rect+text.text{fill:var(--md-mermaid-sequence-box-fg-color)}defs #sequencenumber{fill:var(--md-mermaid-sequence-number-bg-color)!important}";var ko,pp=0;function fp(){return typeof mermaid=="undefined"||mermaid instanceof Element?ar("https://unpkg.com/mermaid@11/dist/mermaid.min.js"):Y(void 0)}function $s(e){return e.classList.remove("mermaid"),ko||(ko=fp().pipe($(()=>mermaid.initialize({startOnLoad:!1,themeCSS:Hs,sequence:{actorFontSize:"16px",messageFontSize:"16px",noteFontSize:"16px"}})),f(()=>{}),se(1))),ko.subscribe(()=>Uo(null,null,function*(){e.classList.add("mermaid");let t=`__mermaid_${pp++}`,r=A("div",{class:"mermaid"}),n=e.textContent,{svg:o,fn:i}=yield mermaid.render(t,n),a=r.attachShadow({mode:"closed"});a.innerHTML=o,e.replaceWith(r),i==null||i(a)})),ko.pipe(f(()=>({ref:e})))}var Ps=A("table");function Is(e){return e.replaceWith(Ps),Ps.replaceWith(gs(e)),Y({ref:e})}function mp(e){let t=e.find(r=>r.checked)||e[0];return R(...e.map(r=>b(r,"change").pipe(f(()=>G(`label[for="${r.id}"]`))))).pipe(J(G(`label[for="${t.id}"]`)),f(r=>({active:r})))}function Rs(e,{viewport$:t,target$:r}){let n=G(".tabbed-labels",e),o=P(":scope > input",e),i=Oo("prev");e.append(i);let a=Oo("next");return e.append(a),j(()=>{let s=new I,c=s.pipe(he(),ye(!0));re([s,Re(e),Et(e)]).pipe(Q(c),Xe(1,je)).subscribe({next([{active:l},u]){let p=wt(l),{width:d}=Ae(l);e.style.setProperty("--md-indicator-x",`${p.x}px`),e.style.setProperty("--md-indicator-width",`${d}px`);let m=ln(n);(p.xm.x+u.width)&&n.scrollTo({left:Math.max(0,p.x-16),behavior:"smooth"})},complete(){e.style.removeProperty("--md-indicator-x"),e.style.removeProperty("--md-indicator-width")}}),re([Ut(n),Re(n)]).pipe(Q(c)).subscribe(([l,u])=>{let p=Mr(n);i.hidden=l.x<16,a.hidden=l.x>p.width-u.width-16}),R(b(i,"click").pipe(f(()=>-1)),b(a,"click").pipe(f(()=>1))).pipe(Q(c)).subscribe(l=>{let{width:u}=Ae(n);n.scrollBy({left:u*l,behavior:"smooth"})}),r.pipe(Q(c),L(l=>o.includes(l))).subscribe(l=>l.click()),n.classList.add("tabbed-labels--linked");for(let l of o){let u=G(`label[for="${l.id}"]`);u.replaceChildren(A("a",{href:`#${u.htmlFor}`,tabIndex:-1},...Array.from(u.childNodes))),b(u.firstElementChild,"click").pipe(Q(c),L(p=>!(p.metaKey||p.ctrlKey)),$(p=>{p.preventDefault(),p.stopPropagation()})).subscribe(()=>{history.replaceState({},"",`#${u.htmlFor}`),u.click()})}return X("content.tabs.link")&&s.pipe(ke(1),pe(t)).subscribe(([{active:l},{offset:u}])=>{let p=l.innerText.trim();if(l.hasAttribute("data-md-switching"))l.removeAttribute("data-md-switching");else{let d=e.offsetTop-u.y;for(let h of P("[data-tabs]"))for(let v of P(":scope > input",h)){let x=G(`label[for="${v.id}"]`);if(x!==l&&x.innerText.trim()===p){x.setAttribute("data-md-switching",""),v.click();break}}window.scrollTo({top:e.offsetTop-d});let m=__md_get("__tabs")||[];__md_set("__tabs",[...new Set([p,...m])])}}),s.pipe(Q(c)).subscribe(()=>{for(let l of P("audio, video",e))l.offsetWidth&&l.autoplay?l.play().catch(()=>{}):l.pause()}),mp(o).pipe($(l=>s.next(l)),V(()=>s.complete()),f(l=>H({ref:e},l)))}).pipe(Ht(ge))}function js(e,t){let{viewport$:r,target$:n,print$:o}=t;return R(...P(".annotate:not(.highlight)",e).map(i=>Es(i,{target$:n,print$:o})),...P("pre:not(.mermaid) > code",e).map(i=>Ls(i,{target$:n,print$:o})),...P("a",e).map(i=>Cs(i,t)),...P("pre.mermaid",e).map(i=>$s(i)),...P("table:not([class])",e).map(i=>Is(i)),...P("details",e).map(i=>Ms(i,{target$:n,print$:o})),...P("[data-tabs]",e).map(i=>Rs(i,{viewport$:r,target$:n})),...P("[title]:not([data-preview])",e).filter(()=>X("content.tooltips")).map(i=>Ge(i,{viewport$:r})),...P(".footnote-ref",e).filter(()=>X("content.footnote.tooltips")).map(i=>Rr(i,{content$:new U(a=>{let s=new URL(i.href).hash.slice(1),c=Array.from(document.getElementById(s).cloneNode(!0).children),l=On(...c);return a.next(l),document.body.append(l),()=>l.remove()}),viewport$:r})))}function dp(e,{alert$:t}){return t.pipe(g(r=>R(Y(!0),Y(!1).pipe(It(2e3))).pipe(f(n=>({message:r,active:n})))))}function Fs(e,t){let r=G(".md-typeset",e);return j(()=>{let n=new I;return n.subscribe(({message:o,active:i})=>{e.classList.toggle("md-dialog--active",i),r.textContent=o}),dp(e,t).pipe($(o=>n.next(o)),V(()=>n.complete()),f(o=>H({ref:e},o)))})}function hp({viewport$:e}){if(!X("header.autohide"))return Y(!1);let t=e.pipe(f(({offset:{y:o}})=>o),Pt(2,1),f(([o,i])=>[oMath.abs(i-o.y)>100),f(([,[o]])=>o),ie()),n=Tn("search");return re([e,n]).pipe(f(([{offset:o},i])=>o.y>400&&!i),ie(),g(o=>o?r:Y(!1)),J(!1))}function Us(e,t){return j(()=>re([Re(e),hp(t)])).pipe(f(([{height:r},n])=>({height:r,hidden:n})),ie((r,n)=>r.height===n.height&&r.hidden===n.hidden),se(1))}function Ns(e,{viewport$:t,header$:r,main$:n}){return j(()=>{let o=new I,i=o.pipe(he(),ye(!0));o.pipe(fe("active"),Ze(r)).subscribe(([{active:s},{hidden:c}])=>{e.classList.toggle("md-header--shadow",s&&!c),e.hidden=c});let a=me(P("[title]",e)).pipe(L(()=>X("content.tooltips")),oe(s=>Ge(s,{viewport$:t})));return n.subscribe(o),r.pipe(Q(i),f(s=>H({ref:e},s)),Rt(a.pipe(Q(i))))})}function vp(e,{viewport$:t,header$:r}){return Sn(e,{viewport$:t,header$:r}).pipe(f(({offset:{y:n}})=>{let{height:o}=Ae(e);return{active:o>0&&n>=o}}),fe("active"))}function Ds(e,t){return j(()=>{let r=new I;r.subscribe({next({active:o}){e.classList.toggle("md-header__title--active",o)},complete(){e.classList.remove("md-header__title--active")}});let n=we(".md-content h1");return typeof n=="undefined"?y:vp(n,t).pipe($(o=>r.next(o)),V(()=>r.complete()),f(o=>H({ref:e},o)))})}function Ws(e,{viewport$:t,header$:r}){let n=r.pipe(f(({height:i})=>i),ie()),o=n.pipe(g(()=>Re(e).pipe(f(({height:i})=>({top:e.offsetTop,bottom:e.offsetTop+i})),fe("bottom"))));return re([n,o,t]).pipe(f(([i,{top:a,bottom:s},{offset:{y:c},size:{height:l}}])=>(l=Math.max(0,l-Math.max(0,a-c,i)-Math.max(0,l+c-s)),{offset:a-i,height:l,active:a-i<=c})),ie((i,a)=>i.offset===a.offset&&i.height===a.height&&i.active===a.active))}function bp(e){let t=__md_get("__palette")||{index:e.findIndex(n=>matchMedia(n.getAttribute("data-md-color-media")).matches)},r=Math.max(0,Math.min(t.index,e.length-1));return Y(...e).pipe(oe(n=>b(n,"change").pipe(f(()=>n))),J(e[r]),f(n=>({index:e.indexOf(n),color:{media:n.getAttribute("data-md-color-media"),scheme:n.getAttribute("data-md-color-scheme"),primary:n.getAttribute("data-md-color-primary"),accent:n.getAttribute("data-md-color-accent")}})),se(1))}function Vs(e){let t=P("input",e),r=A("meta",{name:"theme-color"});document.head.appendChild(r);let n=A("meta",{name:"color-scheme"});document.head.appendChild(n);let o=Ir("(prefers-color-scheme: light)");return j(()=>{let i=new I;return i.subscribe(a=>{if(document.body.setAttribute("data-md-color-switching",""),a.color.media==="(prefers-color-scheme)"){let s=matchMedia("(prefers-color-scheme: light)"),c=document.querySelector(s.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");a.color.scheme=c.getAttribute("data-md-color-scheme"),a.color.primary=c.getAttribute("data-md-color-primary"),a.color.accent=c.getAttribute("data-md-color-accent")}for(let[s,c]of Object.entries(a.color))document.body.setAttribute(`data-md-color-${s}`,c);for(let s=0;sa.key==="Enter"),pe(i,(a,s)=>s)).subscribe(({index:a})=>{a=(a+1)%t.length,t[a].click(),t[a].focus()}),i.pipe(f(()=>{let a=ht("header"),s=window.getComputedStyle(a);return n.content=s.colorScheme,s.backgroundColor.match(/\d+/g).map(c=>(+c).toString(16).padStart(2,"0")).join("")})).subscribe(a=>r.content=`#${a}`),i.pipe(Ie(ge)).subscribe(()=>{document.body.removeAttribute("data-md-color-switching")}),bp(t).pipe(Q(o.pipe(ke(1))),jt(),$(a=>i.next(a)),V(()=>i.complete()),f(a=>H({ref:e},a)))})}function zs(e,{progress$:t}){return j(()=>{let r=new I;return r.subscribe(({value:n})=>{e.style.setProperty("--md-progress-value",`${n}`)}),t.pipe($(n=>r.next({value:n})),V(()=>r.complete()),f(n=>({ref:e,value:n})))})}var qs='.v u{text-decoration:underline!important;text-decoration-style:wavy!important;text-decoration-thickness:1px!important}.p{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background-color:rgba(var(--color-backdrop)/var(--alpha-lighter));cursor:pointer;height:100%;pointer-events:auto;position:absolute;transition:opacity .25s;width:100%}.p.m{opacity:0;pointer-events:none;transition:opacity .35s}.r{align-items:center;background-color:initial;border:none;border-radius:var(--space-2);cursor:pointer;display:flex;flex-shrink:0;font-family:var(--font-family);height:36px;justify-content:center;outline:none;padding:0;position:relative;transition:background-color .25s,color .25s;width:36px;z-index:1}.r svg{stroke:rgb(var(--color-foreground));height:18px;opacity:.5;width:18px}.r:before{background-color:rgb(var(--color-background-subtle));border-radius:var(--border-radius-2);content:"";inset:0;opacity:0;position:absolute;transform:scale(.75);transition:transform 125ms,opacity 125ms;z-index:0}.r:hover:before{opacity:1;transform:scale(1)}.r.c{cursor:auto}.r.c:before{display:none}.n{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background-color:rgba(var(--color-background)/var(--alpha-light));border-radius:var(--space-3);box-shadow:0 0 60px #0000000d;display:flex;height:480px;overflow:hidden;pointer-events:auto;position:absolute;transition:transform .25s cubic-bezier(.16,1,.3,1),opacity .25s;width:640px}.n.l{opacity:0;pointer-events:none;transform:scale(1.1);transition:transform .25s .15s,opacity .15s}@media (max-width:680px){.n{border-radius:0;height:100%;width:100%}}.u{display:flex;flex-basis:min-content;flex-direction:column;flex-grow:1;flex-shrink:0}@keyframes d{0%{transform:scale(0)}50%{transform:scale(1.2)}to{transform:scale(1)}}.y{animation:d .25s ease-in-out;background:var(--color-highlight);border-radius:100%;color:#fff;font-size:8px;font-weight:700;height:12px;padding-top:1px;position:absolute;right:4px;top:4px;width:12px}.i{background-color:rgb(var(--color-background-subtle)/var(--alpha-lighter));flex-shrink:0;overflow:scroll;position:relative;transition:width .35s cubic-bezier(.16,1,.3,1),opacity .25s;width:200px}.i>*{transform:translate(0);transition:transform .25s cubic-bezier(.16,1,.3,1)}.i.l{opacity:0;width:0}.i.l>*{transform:translate(-48px)}@media (max-width:680px){.i{-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);background-color:rgba(var(--color-background-subtle)/var(--alpha-light));box-shadow:0 0 60px #00000026;height:100%;position:absolute;right:0;top:0}}.w{border-bottom:1px solid rgb(var(--color-foreground)/var(--alpha-lightest));display:flex;gap:var(--space-1);padding:var(--space-2)}.k{-webkit-overflow-scrolling:touch;overflow:auto;overscroll-behavior:contain}.z{padding:8px 10px}.X{color:rgb(var(--color-foreground)/var(--alpha-light));padding:var(--space-2);position:absolute;width:200px}.X,.j{display:flex;flex-direction:column}.j{gap:2px;list-style:none;padding:0}.F,.j{margin:0}.F{font-size:16px;font-weight:400}.F,.I{padding:8px}.I{font-size:14px;margin:4px 0 0;opacity:.5}.I,.o{font-size:12px}.o{cursor:pointer;display:flex;padding:4px 8px;position:relative}.o:before{background-color:var(--color-highlight-transparent);border-radius:var(--space-1);content:"";inset:0;opacity:0;position:absolute;transform:scale(.75);transition:transform 125ms,opacity 125ms;z-index:0}.o.g:before,.o:hover:before{opacity:1;transform:scale(1)}.o.g,.o:hover{color:var(--color-highlight)}.R{flex-grow:1}.R,.q{position:relative}.q{font-weight:700}.f{flex-grow:1}.f input{background:#0000;border:none;color:rgb(var(--color-foreground));font-family:var(--font-family);font-size:16px;height:100%;letter-spacing:-.25px;outline:none;width:100%}.b{color:rgb(var(--color-foreground)/var(--alpha-light));display:flex;flex-direction:column;gap:2px;line-height:1.3;list-style:none;margin:var(--space-2);margin-top:0;padding:0}.A,.b li{margin:0}.A{color:rgb(var(--color-foreground)/var(--alpha-lighter));font-size:12px;margin-top:var(--space-2);padding:0 18px}.a{border-radius:var(--space-2);color:inherit;cursor:pointer;display:flex;flex-direction:row;flex-grow:1;padding:8px 10px;position:relative;text-decoration:none}.a:before{background-color:rgb(var(--color-background-subtle));border-radius:var(--border-radius-2);content:"";display:block;inset:0;opacity:0;position:absolute;transform:scale(.9);transition:transform 125ms,opacity 125ms;z-index:0}@media (pointer:fine){.a.h:before,.a:hover:before{opacity:1;transform:scale(1)}}.a mark{background:#0000;color:var(--color-highlight)}.a u{background-color:var(--color-highlight-transparent);border-radius:2px;box-shadow:0 0 0 1px var(--color-highlight-transparent);text-decoration:none}.B{flex-grow:1}.s{margin-right:-8px;opacity:0;position:relative;transform:translate(-2px);transition:transform 125ms,opacity 125ms;z-index:0}@media (pointer:fine){.h>.s,:hover>.s{opacity:1;transform:none}}.x{font-size:14px;margin:0;position:relative}.x code{background:rgb(var(--color-background-subtle));border-radius:var(--space-1);font-size:13px;padding:2px 4px}.t{color:rgb(var(--color-foreground)/var(--alpha-lighter));display:inline-flex;flex-wrap:wrap;font-size:12px;gap:var(--space-1);list-style:none;margin:0;padding:0;position:relative}.t li{white-space:nowrap}.t li:after{content:"/";display:inline;margin-left:var(--space-1)}.t li:last-child:after{content:"";display:none}.e{--space-1:4px;--space-2:calc(var(--space-1)*2);--space-3:calc(var(--space-2)*2);--space-4:calc(var(--space-3)*2);--space-5:calc(var(--space-4)*2);--alpha-light:.7;--alpha-lighter:.54;--alpha-lightest:.1;--color-highlight:var(--md-accent-fg-color,#526cfe);--color-highlight-transparent:var(--md-accent-fg-color--transparent,#526cfe1a);--border-radius-1:var(--space-1);--border-radius-2:var(--space-2);--border-radius-3:calc(var(--space-1) + var(--space-2));--font-family:var(--md-text-font-family,Inter,Roboto Flex,system-ui,sans-serif);--font-size:16px;--line-height:1.5;--letter-spacing:-.5px;-webkit-font-smoothing:antialiased;align-items:center;display:flex;font-family:var(--font-family);font-size:var(--font-size);height:100vh;justify-content:center;letter-spacing:var(--letter-spacing);line-height:var(--line-height);pointer-events:none;position:absolute;width:100vw}@media (pointer:coarse){.e{height:-webkit-fill-available}}.e *,.e :after,.e :before{box-sizing:border-box}';function Ks(e,{index$:t}){let r=Ue(),n=document.createElement("div");document.body.appendChild(n),n.style.position="fixed",n.style.height="100%",n.style.top="0",n.style.zIndex="4";let o=n.attachShadow({mode:"open"});o.appendChild(A("style",{},qs.toString()));try{Ya(r.search,{highlight:r.features.includes("search.highlight")}),me(t).subscribe(i=>{for(let a of i.items)a.location=new URL(a.location,r.base).toString();Ja(i,o)}),b(e,"click").subscribe(()=>{go()}),Tn("search").pipe(ke(1)).subscribe(()=>go())}catch(i){e.hidden=!0;let a=G("label[for=__search]");a.hidden=!0}return Ke}var Bs=_r(So());function Ys(e,{index$:t,location$:r}){return re([t,r.pipe(J(Ye()),L(n=>!!n.searchParams.get("h")))]).pipe(f(([n,o])=>_p(n.config)(o.searchParams.get("h"))),f(n=>{var a;let o=new Map,i=document.createNodeIterator(e,NodeFilter.SHOW_TEXT);for(let s=i.nextNode();s;s=i.nextNode())if((a=s.parentElement)!=null&&a.offsetHeight){let c=s.textContent,l=n(c);l.length>c.length&&o.set(s,l)}for(let[s,c]of o){let{childNodes:l}=A("span",null,c);s.replaceWith(...Array.from(l))}return{ref:e,nodes:o}}))}function _p(e){let t=e.separator.split("|").map(o=>o.replace(/(\(\?[!=<][^)]+\))/g,"").length===0?"\uFFFD":o).join("|"),r=new RegExp(t,"img"),n=(o,i,a)=>`${i}${a}`;return o=>{o=o.replace(/\s+/g," ").replace(/&/g,"&").trim();let i=new RegExp(`(^|${e.separator}|)(${o.split(r).map(a=>a.replace(/[|\\{}()[\]^$+*?.-]/g,"\\$&")).filter(a=>a.length>0).join("|")})`,"img");return a=>(0,Bs.default)(a).replace(i,n).replace(/<\/mark>(\s+)]*>/img,"$1")}}function yp(e,{viewport$:t,main$:r}){let n=e.closest(".md-grid"),o=n.offsetTop-n.parentElement.offsetTop;return re([r,t]).pipe(f(([{offset:i,height:a},{offset:{y:s}}])=>(a=a+Math.min(o,Math.max(0,s-i))-o,{height:a,locked:s>=i+o})),ie((i,a)=>i.height===a.height&&i.locked===a.locked))}function Ao(e,n){var o=n,{header$:t}=o,r=gr(o,["header$"]);let i=G(".md-sidebar__scrollwrap",e),{y:a}=wt(i);return j(()=>{let s=new I,c=s.pipe(he(),ye(!0)),l=s.pipe(Xe(0,je));return l.pipe(pe(t)).subscribe({next([{height:u},{height:p}]){i.style.height=`${u-2*a}px`,e.style.top=`${p}px`},complete(){i.style.height="",e.style.top=""}}),l.pipe(Sr()).subscribe(()=>{for(let u of P(".md-nav__link--active[href]",e)){if(!u.clientHeight)continue;let p=u.closest(".md-sidebar__scrollwrap");if(typeof p!="undefined"){let d=u.offsetTop-p.offsetTop,{height:m}=Ae(p);p.scrollTo({top:d-m/2})}}}),me(P("label[tabindex]",e)).pipe(oe(u=>b(u,"click").pipe(Ie(ge),f(()=>u),Q(c)))).subscribe(u=>{let p=G(`[id="${u.htmlFor}"]`);G(`[aria-labelledby="${u.id}"]`).setAttribute("aria-expanded",`${p.checked}`)}),X("content.tooltips")&&me(P("abbr[title]",e)).pipe(oe(u=>Ge(u,{viewport$})),Q(c)).subscribe(),yp(e,r).pipe($(u=>s.next(u)),V(()=>s.complete()),f(u=>H({ref:e},u)))})}function Gs(e,t){if(typeof t!="undefined"){let r=`https://api.github.com/repos/${e}/${t}`;return $t(et(`${r}/releases/latest`).pipe(_e(()=>y),f(n=>({version:n.tag_name})),ot({})),et(r).pipe(_e(()=>y),f(n=>({stars:n.stargazers_count,forks:n.forks_count})),ot({}))).pipe(f(([n,o])=>H(H({},n),o)))}else{let r=`https://api.github.com/users/${e}`;return et(r).pipe(f(n=>({repositories:n.public_repos})),ot({}))}}function Js(e,t){let r=`https://${e}/api/v4/projects/${encodeURIComponent(t)}`;return $t(et(`${r}/releases/permalink/latest`).pipe(_e(()=>y),f(({tag_name:n})=>({version:n})),ot({})),et(r).pipe(_e(()=>y),f(({star_count:n,forks_count:o})=>({stars:n,forks:o})),ot({}))).pipe(f(([n,o])=>H(H({},n),o)))}function Xs(e){let t=e.match(/^.+github\.com\/([^/]+)\/?([^/]+)?/i);if(t){let[,r,n]=t;return Gs(r,n)}if(t=e.match(/^.+?([^/]*gitlab[^/]+)\/(.+?)\/?$/i),t){let[,r,n]=t;return Js(r,n)}return y}var xp;function wp(e){return xp||(xp=j(()=>{let t=__md_get("__source",sessionStorage);if(t)return Y(t);if(Te("consent").length){let n=__md_get("__consent");if(!(n&&n.github))return y}return Xs(e.href).pipe($(n=>__md_set("__source",n,sessionStorage)))}).pipe(_e(()=>y),L(t=>Object.keys(t).length>0),f(t=>({facts:t})),se(1)))}function Zs(e){let t=G(":scope > :last-child",e);return j(()=>{let r=new I;return r.subscribe(({facts:n})=>{t.appendChild(bs(n)),t.classList.add("md-source__repository--active")}),wp(e).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))})}function Ep(e,{viewport$:t,header$:r}){return Re(document.body).pipe(g(()=>Sn(e,{header$:r,viewport$:t})),f(({offset:{y:n}})=>({hidden:n>=10})),fe("hidden"))}function Qs(e,t){return j(()=>{let r=new I;return r.subscribe({next({hidden:n}){e.hidden=n},complete(){e.hidden=!1}}),(X("navigation.tabs.sticky")?Y({hidden:!1}):Ep(e,t)).pipe($(n=>r.next(n)),V(()=>r.complete()),f(n=>H({ref:e},n)))})}function Tp(e,{viewport$:t,header$:r}){let n=new Map,o=P(".md-nav__link",e);for(let s of o){let c=decodeURIComponent(s.hash.substring(1)),l=we(`[id="${c}"]`);typeof l!="undefined"&&n.set(s,l)}let i=r.pipe(fe("height"),f(({height:s})=>{let c=ht("main"),l=G(":scope > :first-child",c);return s+.9*(l.offsetTop-c.offsetTop)}),xe());return Re(document.body).pipe(fe("height"),g(s=>j(()=>{let c=[];return Y([...n].reduce((l,[u,p])=>{for(;c.length&&n.get(c[c.length-1]).tagName>=p.tagName;)c.pop();let d=p.offsetTop;for(;!d&&p.parentElement;)p=p.parentElement,d=p.offsetTop;let m=p.offsetParent;for(;m;m=m.offsetParent)d+=m.offsetTop;return l.set([...c=[...c,u]].reverse(),d)},new Map))}).pipe(f(c=>new Map([...c].sort(([,l],[,u])=>l-u))),Ze(i),g(([c,l])=>t.pipe(Or(([u,p],{offset:{y:d},size:m})=>{let h=d+m.height>=Math.floor(s.height);for(;p.length;){let[,v]=p[0];if(v-l=d&&!h)p=[u.pop(),...p];else break}return[u,p]},[[],[...c]]),ie((u,p)=>u[0]===p[0]&&u[1]===p[1])))))).pipe(f(([s,c])=>({prev:s.map(([l])=>l),next:c.map(([l])=>l)})),J({prev:[],next:[]}),Pt(2,1),f(([s,c])=>s.prev.length{let i=new I,a=i.pipe(he(),ye(!0));i.subscribe(({prev:c,next:l})=>{for(let[u]of l)u.classList.remove("md-nav__link--passed"),u.classList.remove("md-nav__link--active");for(let[u,[p]]of c.entries())p.classList.add("md-nav__link--passed"),p.classList.toggle("md-nav__link--active",u===c.length-1)});let s=we(".md-sidebar--secondary");if(typeof s!="undefined"&&b(document.body,"click").subscribe(c=>{let l=c.target;if(!s.contains(l)){let u=we(".md-nav__toggle",s);typeof u!="undefined"&&(u.checked=!1)}}),X("toc.follow")){let c=R(t.pipe(Be(1),f(()=>{})),t.pipe(Be(250),f(()=>"smooth")));i.pipe(L(({prev:l})=>l.length>0),Ze(n.pipe(Ie(ge))),pe(c)).subscribe(([[{prev:l}],u])=>{let[p]=l[l.length-1];if(p.offsetHeight){let d=ki(p);if(typeof d!="undefined"){let m=p.offsetTop-d.offsetTop,{height:h}=Ae(d);d.scrollTo({top:m-h/2,behavior:u})}}})}return X("navigation.tracking")&&t.pipe(Q(a),fe("offset"),Be(250),ke(1),Q(o.pipe(ke(1))),jt({delay:250}),pe(i)).subscribe(([,{prev:c}])=>{let l=Ye(),u=c[c.length-1];if(u&&u.length){let[p]=u,{hash:d}=new URL(p.href);l.hash!==d&&(l.hash=d,history.replaceState({},"",`${l}`))}else l.hash="",history.replaceState({},"",`${l}`)}),Tp(e,{viewport$:t,header$:r}).pipe($(c=>i.next(c)),V(()=>i.complete()),f(c=>H({ref:e},c)))})}function Sp(e,{viewport$:t,main$:r,target$:n}){let o=t.pipe(f(({offset:{y:a}})=>a),Pt(2,1),f(([a,s])=>a>s&&s>0),ie()),i=r.pipe(f(({active:a})=>a));return re([i,o]).pipe(f(([a,s])=>!(a&&s)),ie(),Q(n.pipe(ke(1))),ye(!0),jt({delay:250}),f(a=>({hidden:a})))}function tc(e,{viewport$:t,header$:r,main$:n,target$:o}){let i=new I,a=i.pipe(he(),ye(!0));return i.subscribe({next({hidden:s}){e.hidden=s,s?(e.setAttribute("tabindex","-1"),e.blur()):e.removeAttribute("tabindex")},complete(){e.style.top="",e.hidden=!0,e.removeAttribute("tabindex")}}),r.pipe(Q(a),fe("height")).subscribe(({height:s})=>{e.style.top=`${s+16}px`}),b(e,"click").subscribe(s=>{s.preventDefault(),window.scrollTo({top:0})}),Sp(e,{viewport$:t,main$:n,target$:o}).pipe($(s=>i.next(s)),V(()=>i.complete()),f(s=>H({ref:e},s)))}function rc(e,t){return e.protocol=t.protocol,e.hostname=t.hostname,t.port&&(e.port=t.port),e}function Op(e,t){let r=new Map;for(let n of P("url",e)){let o=G("loc",n),i=[rc(new URL(o.textContent),t)];r.set(`${i[0]}`,i);for(let a of P("[rel=alternate]",n)){let s=a.getAttribute("href");s!=null&&i.push(rc(new URL(s),t))}}return r}function dr(e){return ns(new URL("sitemap.xml",e)).pipe(f(t=>Op(t,new URL(e))),_e(()=>Y(new Map)),xe())}function nc({document$:e}){let t=new Map;e.pipe(g(()=>P("link[rel=alternate]")),f(r=>new URL(r.href)),L(r=>!t.has(r.toString())),oe(r=>dr(r).pipe(f(n=>[r,n]),_e(()=>y)))).subscribe(([r,n])=>{t.set(r.toString().replace(/\/$/,""),n)}),b(document.body,"click").pipe(L(r=>!r.metaKey&&!r.ctrlKey),g(r=>{if(r.target instanceof Element){let n=r.target.closest("a");if(n&&!n.target){let o=[...t].find(([p])=>n.href.startsWith(`${p}/`));if(typeof o=="undefined")return y;let[i,a]=o,s=Ye();if(s.href.startsWith(i))return y;let c=Ue(),l=s.href.replace(c.base,"");l=`${i}/${l}`;let u=a.has(l.split("#")[0])?new URL(l,c.base):new URL(i);return r.preventDefault(),Y(u)}}return y})).subscribe(r=>dt(r,!0))}var Co=_r(Mo());function Lp(e){e.setAttribute("data-md-copying","");let t=e.closest("[data-copy]"),r=t?t.getAttribute("data-copy"):e.innerText;return e.removeAttribute("data-md-copying"),r.trimEnd()}function oc({alert$:e}){Co.default.isSupported()&&new U(t=>{new Co.default("[data-clipboard-target], [data-clipboard-text]",{text:r=>r.getAttribute("data-clipboard-text")||Lp(G(r.getAttribute("data-clipboard-target")))}).on("success",r=>t.next(r))}).pipe($(t=>{t.trigger.focus()}),f(()=>Bt("clipboard.copied"))).subscribe(e)}function ic(e,t){if(!(e.target instanceof Element))return y;let r=e.target.closest("a");if(r===null)return y;if(r.target||e.metaKey||e.ctrlKey)return y;let n=new URL(r.href);return n.search=n.hash="",t.has(`${n}`)?(e.preventDefault(),Y(r)):y}function ac(e){let t=new Map;for(let r of P(":scope > *",e.head))t.set(r.outerHTML,r);return t}function sc(e){for(let t of P("[href], [src]",e))for(let r of["href","src"]){let n=t.getAttribute(r);if(n&&!/^(?:[a-z]+:)?\/\//i.test(n)){t[r]=t[r];break}}return Y(e)}function Mp(e){for(let n of["[data-md-component=announce]","[data-md-component=container]","[data-md-component=header-topic]","[data-md-component=outdated]","[data-md-component=logo]","[data-md-component=skip]",...X("navigation.tabs.sticky")?["[data-md-component=tabs]"]:[]]){let o=we(n),i=we(n,e);typeof o!="undefined"&&typeof i!="undefined"&&o.replaceWith(i)}let t=ac(document);for(let[n,o]of ac(e))t.has(n)?t.delete(n):document.head.appendChild(o);for(let n of t.values()){let o=n.getAttribute("name");o!=="theme-color"&&o!=="color-scheme"&&n.remove()}let r=ht("container");return nt(P("script",r)).pipe(g(n=>{let o=e.createElement("script");if(n.src){for(let i of n.getAttributeNames())o.setAttribute(i,n.getAttribute(i));return n.replaceWith(o),new U(i=>{o.onload=()=>i.complete()})}else return o.textContent=n.textContent,n.replaceWith(o),y}),he(),ye(document))}function cc({sitemap$:e,location$:t,viewport$:r,progress$:n}){if(location.protocol==="file:")return Ke;Y(document).subscribe(sc);let o=b(document.body,"click").pipe(Ze(e),g(([s,c])=>ic(s,c)),f(({href:s})=>new URL(s)),xe()),i=b(window,"popstate").pipe(f(Ye),xe());o.pipe(pe(r)).subscribe(([s,{offset:c}])=>{history.replaceState(c,""),history.pushState(null,"",s)}),R(o,i).subscribe(t);let a=t.pipe(fe("pathname"),g(s=>En(s,{progress$:n}).pipe(_e(()=>(dt(s,!0),y)))),g(sc),g(Mp),xe());return R(a.pipe(pe(t,(s,c)=>c)),a.pipe(g(()=>t),fe("hash")),t.pipe(ie((s,c)=>s.pathname===c.pathname&&s.hash===c.hash),g(()=>o),$(()=>history.back()))).subscribe(s=>{var c,l;history.state!==null||!s.hash?window.scrollTo(0,(l=(c=history.state)==null?void 0:c.y)!=null?l:0):(history.scrollRestoration="auto",es(s.hash),history.scrollRestoration="manual")}),t.subscribe(()=>{history.scrollRestoration="manual"}),b(window,"beforeunload").subscribe(()=>{history.scrollRestoration="auto"}),r.pipe(fe("offset"),Be(100)).subscribe(({offset:s})=>{history.replaceState(s,"")}),X("navigation.instant.prefetch")&&R(b(document.body,"mousemove"),b(document.body,"focusin")).pipe(Ze(e),g(([s,c])=>ic(s,c)),Be(25),Yn(({href:s})=>s),cn(s=>{let c=document.createElement("link");return c.rel="prefetch",c.href=s.toString(),document.head.appendChild(c),b(c,"load").pipe(f(()=>c),Me(1))})).subscribe(s=>s.remove()),a}function lc(e){var u;let{selectedVersionSitemap:t,selectedVersionBaseURL:r,currentLocation:n,currentBaseURL:o}=e,i=(u=Ho(o))==null?void 0:u.pathname;if(i===void 0)return;let a=kp(n.pathname,i);if(a===void 0)return;let s=Cp(t.keys());if(!t.has(s))return;let c=Ho(a,s);if(!c||!t.has(c.href))return;let l=Ho(a,r);if(l)return l.hash=n.hash,l.search=n.search,l}function Ho(e,t){try{return new URL(e,t)}catch(r){return}}function kp(e,t){if(e.startsWith(t))return e.slice(t.length)}function Ap(e,t){let r=Math.min(e.length,t.length),n;for(n=0;ny)),n=r.pipe(f(o=>{let[,i]=t.base.match(/([^/]+)\/?$/);return o.find(({version:a,aliases:s})=>a===i||s.includes(i))||o[0]}));r.pipe(f(o=>new Map(o.map(i=>[`${new URL(`../${i.version}/`,t.base)}`,i]))),g(o=>b(document.body,"click").pipe(L(i=>!i.metaKey&&!i.ctrlKey),pe(n),g(([i,a])=>{if(i.target instanceof Element){let s=i.target.closest("a");if(s&&!s.target&&o.has(s.href)){let c=s.href;return!i.target.closest(".md-version")&&o.get(c)===a?y:(i.preventDefault(),Y(new URL(c)))}}return y}),g(i=>dr(i).pipe(f(a=>{var s;return(s=lc({selectedVersionSitemap:a,selectedVersionBaseURL:i,currentLocation:Ye(),currentBaseURL:t.base}))!=null?s:i})))))).subscribe(o=>dt(o,!0)),re([r,n]).subscribe(([o,i])=>{G(".md-header__topic").appendChild(_s(o,i))}),e.pipe(g(()=>n)).subscribe(o=>{var s;let i=new URL(t.base),a=__md_get("__outdated",sessionStorage,i);if(a===null){a=!0;let c=((s=t.version)==null?void 0:s.default)||"latest";Array.isArray(c)||(c=[c]);e:for(let l of c)for(let u of o.aliases.concat(o.version))if(new RegExp(l,"i").test(u)){a=!1;break e}__md_set("__outdated",a,sessionStorage,i)}if(a)for(let c of Te("outdated"))c.hidden=!1})}function pc({document$:e,viewport$:t}){e.pipe(g(()=>P(".md-ellipsis")),oe(r=>Et(r).pipe(Q(e.pipe(ke(1))),L(n=>n),f(()=>r),Me(1))),L(r=>r.offsetWidth{let n=r.innerText,o=r.closest("a")||r;return o.title=n,X("content.tooltips")?Ge(o,{viewport$:t}).pipe(Q(e.pipe(ke(1))),V(()=>o.removeAttribute("title"))):y})).subscribe(),X("content.tooltips")&&e.pipe(g(()=>P(".md-status")),oe(r=>Ge(r,{viewport$:t}))).subscribe()}function fc({document$:e,tablet$:t}){e.pipe(g(()=>P(".md-toggle--indeterminate")),$(r=>{r.indeterminate=!0,r.checked=!1}),oe(r=>b(r,"change").pipe(Xn(()=>r.classList.contains("md-toggle--indeterminate")),f(()=>r))),pe(t)).subscribe(([r,n])=>{r.classList.remove("md-toggle--indeterminate"),n&&(r.checked=!1)})}function Hp(){return/(iPad|iPhone|iPod)/.test(navigator.userAgent)}function mc({document$:e}){e.pipe(g(()=>P("[data-md-scrollfix]")),$(t=>t.removeAttribute("data-md-scrollfix")),L(Hp),oe(t=>b(t,"touchstart").pipe(f(()=>t)))).subscribe(t=>{let r=t.scrollTop;r===0?t.scrollTop=1:r+t.offsetHeight===t.scrollHeight&&(t.scrollTop=r-1)})}Object.entries||(Object.entries=function(e){let t=[];for(let r of Object.keys(e))t.push([r,e[r]]);return t});Object.values||(Object.values=function(e){let t=[];for(let r of Object.keys(e))t.push(e[r]);return t});typeof Element!="undefined"&&(Element.prototype.scrollTo||(Element.prototype.scrollTo=function(e,t){typeof e=="object"?(this.scrollLeft=e.left,this.scrollTop=e.top):(this.scrollLeft=e,this.scrollTop=t)}),Element.prototype.replaceWith||(Element.prototype.replaceWith=function(...e){let t=this.parentNode;if(t){e.length===0&&t.removeChild(this);for(let r=e.length-1;r>=0;r--){let n=e[r];typeof n=="string"?n=document.createTextNode(n):n.parentNode&&n.parentNode.removeChild(n),r?t.insertBefore(this.previousSibling,n):t.replaceChild(n,this)}}}));function $p(){return location.protocol==="file:"?ar(`${new URL("search.js",Mn.base)}`).pipe(f(()=>__index),_e(()=>Ke),se(1)):et(new URL("search.json",Mn.base))}document.documentElement.classList.remove("no-js");document.documentElement.classList.add("js");var vt=Si(),Ur=Za(),hr=ts(Ur),hc=Xa(),ze=cs(),$o=Ir("(min-width: 60em)"),vc=Ir("(min-width: 76.25em)"),bc=rs(),Mn=Ue(),gc=we(".md-search")?$p():Ke,Po=new I;oc({alert$:Po});nc({document$:vt});var Io=new I,_c=dr(Mn.base);X("navigation.instant")&&cc({sitemap$:_c,location$:Ur,viewport$:ze,progress$:Io}).subscribe(vt);var dc;((dc=Mn.version)==null?void 0:dc.provider)==="mike"&&uc({document$:vt});R(Ur,hr).pipe(It(125)).subscribe(()=>{Eo("drawer",!1),Eo("search",!1)});hc.pipe(L(({mode:e,meta:t})=>e==="global"&&!t)).subscribe(e=>{switch(e.type){case",":case"p":let t=document.querySelector("link[rel=prev]");t instanceof HTMLLinkElement&&dt(t);break;case".":case"n":let r=document.querySelector("link[rel=next]");r instanceof HTMLLinkElement&&dt(r);break;case"/":let n=document.querySelector("[data-md-component=search] button");n instanceof HTMLButtonElement&&n.click();break;case"Enter":let o=xt();o instanceof HTMLLabelElement&&o.click()}});pc({viewport$:ze,document$:vt});fc({document$:vt,tablet$:$o});mc({document$:vt});var Lt=Us(ht("header"),{viewport$:ze}),Fr=vt.pipe(f(()=>ht("main")),g(e=>Ws(e,{viewport$:ze,header$:Lt})),se(1)),Pp=R(...Te("consent").map(e=>us(e,{target$:hr})),...Te("dialog").map(e=>Fs(e,{alert$:Po})),...Te("palette").map(e=>Vs(e)),...Te("progress").map(e=>zs(e,{progress$:Io})),...Te("search").map(e=>Ks(e,{index$:gc})),...Te("source").map(e=>Zs(e))),Ip=j(()=>R(...Te("announce").map(e=>ls(e)),...Te("content").map(e=>js(e,{sitemap$:_c,viewport$:ze,target$:hr,print$:bc})),...Te("content").map(e=>X("search.highlight")?Ys(e,{index$:gc,location$:Ur}):y),...Te("header").map(e=>Ns(e,{viewport$:ze,header$:Lt,main$:Fr})),...Te("header-title").map(e=>Ds(e,{viewport$:ze,header$:Lt})),...Te("sidebar").map(e=>e.getAttribute("data-md-type")==="navigation"?yo(vc,()=>Ao(e,{viewport$:ze,header$:Lt,main$:Fr})):yo($o,()=>Ao(e,{viewport$:ze,header$:Lt,main$:Fr}))),...Te("tabs").map(e=>Qs(e,{viewport$:ze,header$:Lt})),...Te("toc").map(e=>ec(e,{viewport$:ze,header$:Lt,main$:Fr,target$:hr})),...Te("top").map(e=>tc(e,{viewport$:ze,header$:Lt,main$:Fr,target$:hr})))),yc=vt.pipe(g(()=>Ip),Rt(Pp),se(1));yc.subscribe();window.document$=vt;window.location$=Ur;window.target$=hr;window.keyboard$=hc;window.viewport$=ze;window.tablet$=$o;window.screen$=vc;window.print$=bc;window.alert$=Po;window.progress$=Io;window.component$=yc;})(); diff --git a/docs/site/assets/javascripts/workers/search.e2d2d235.min.js b/docs/site/assets/javascripts/workers/search.e2d2d235.min.js deleted file mode 100644 index a56d589..0000000 --- a/docs/site/assets/javascripts/workers/search.e2d2d235.min.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(()=>{var vt=Object.create;var K=Object.defineProperty,wt=Object.defineProperties,bt=Object.getOwnPropertyDescriptor,Tt=Object.getOwnPropertyDescriptors,Mt=Object.getOwnPropertyNames,W=Object.getOwnPropertySymbols,kt=Object.getPrototypeOf,Y=Object.prototype.hasOwnProperty,Et=Object.prototype.propertyIsEnumerable;var B=(t,e,n)=>e in t?K(t,e,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[e]=n,R=(t,e)=>{for(var n in e||(e={}))Y.call(e,n)&&B(t,n,e[n]);if(W)for(var n of W(e))Et.call(e,n)&&B(t,n,e[n]);return t},Q=(t,e)=>wt(t,Tt(e));var Ft=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports);var Rt=(t,e,n,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let l of Mt(e))!Y.call(t,l)&&l!==n&&K(t,l,{get:()=>e[l],enumerable:!(r=bt(e,l))||r.enumerable});return t};var qt=(t,e,n)=>(n=t!=null?vt(kt(t)):{},Rt(e||!t||!t.__esModule?K(n,"default",{value:t,enumerable:!0}):n,t));var L=(t,e,n)=>B(t,typeof e!="symbol"?e+"":e,n);var E=(t,e,n)=>new Promise((r,l)=>{var o=u=>{try{s(n.next(u))}catch(i){l(i)}},a=u=>{try{s(n.throw(u))}catch(i){l(i)}},s=u=>u.done?r(u.value):Promise.resolve(u.value).then(o,a);s((n=n.apply(t,e)).next())});var xt=Ft(mt=>{"use strict";function C(t,e,n={}){return{name:t,from:e,meta:n}}function H(t,e){let n=[{value:t,depth:0}];for(let r=0,l=-1,o=0;r>=0;){let{value:a,depth:s}=n[r];if(l<=s&&a.type==="operator"&&a.data.operands.length>0)for(let u=a.data.operands.length;u>0;)n[++r]={value:a.data.operands[--u],depth:s+1};else{let u=e(a,o++,s);if(typeof u<"u")return u;--r}l=s}}var P=class extends Error{constructor(t,e){super(e),this.code=t}};function $(t,e){let n=zt(t);for(let r=0;r{let{matches:r}=n;for(let l=0;l{let l=e.get(n);return typeof l>"u"&&e.set(n,l=t(n,...r)),l}}function D(t,e){return Object.defineProperty(e,"name",{value:t}),e}function tt(t){return E(this,null,function*(){let e=[];if(typeof t.plugins<"u")for(let n=0;n32)throw new RangeError("Bit format exceeds 32 bits");return t}function nt(t,e,n){let r=N(t),l=N(e),o=typeof n<"u"?N(n):32-r-l;return St({d:r,f:l,x:o})}var T=[0];for(let t=0;t<32;t++)T.push(T[t]|1<=n&&t{e+=r*r}),Math.sqrt(e)}function Pt(t,e){t instanceof J?t.data.forEach((n,r)=>{e(n,r)}):t.forEach((n,r)=>{e({start:n,end:n+1,value:1},r)})}var O=class{constructor(t,e=jt(Math.ceil(t/32))){this.size=t,this.data=e}get(t){return this.data[t>>>5]>>>t&1}set(t){this.data[t>>>5]|=1<<(t&31)}forEach(t){let e=this.size&31;for(let n=0;n>>0;for(let n=0;n0;l++){let{value:o,depth:a}=n[--r],s=e(o,l,a);if(typeof s<"u")return s;for(let u=o.children.length;u>0;)n[r++]={value:o.children[--u],depth:a+1}}}function Vt(t,e){return E(this,null,function*(){let{fields:n,plugins:r=[]}=e,l=nt(t.length,n.length),o=[];for(let u=0;u"u")continue;let f=u<{var m;return(m=g.onFilterInput)==null?void 0:m.call(g,p,f,l)},d);let c=o[i],h=k();d=Array.isArray(d)?d:[d];for(let p=0;p{let v=c.index.get(m.node);typeof v>"u"&&c.index.set(m.node,v=k());let w=c.terms.length;for(let b=0;b{var i;return(i=u.onFilterStore)==null?void 0:i.call(u,s,e,t)}),s})}function Ut(t,e,n,r={}){let l=[];if(e<0||e>=t.count.fields)return l;let o=t.shards[e],a=new Map,{count:s=1/0,depth:u=1/0}=r;for(let i=0;iu)continue;let p=a.get(d);typeof p>"u"&&a.set(d,p={node:f,children:[]});let g=l;h>0&&(g=a.get(o.terms[c]).children),g.length=t.count.fields)return{documents:r,terms:l};let o=t.shards[n];return e.forEach(a=>{let{occurrences:s}=o.terms[a];for(let u=0;u>>t.space.x>>>t.space.f;r.set(i)}l[n].set(a)}),{documents:r,terms:l}}function Bt(t){let{documents:e,terms:n}=U(t);A(e,1);for(let r=0;rnew O(e.length))}}function Kt(t,e,n){let{compiler:r,fields:l,plugins:o=[]}=n,{input:a,scope:s,abort:u=!1}=z(o,(f,c)=>{var h;return(h=c.onFilterQuery)==null?void 0:h.call(c,f,t,n)},e),i={items:[],query:{select:U(t),values:[]}},d=new Map;if(u===!1){let f=r(n),{select:c,values:h}=f(a,t);typeof s<"u"&&V(c.documents,s);let p=new Map;i.query={select:c,values:h},c.terms.forEach((g,m)=>{g.forEach(y=>{let x=t.shards[m],{occurrences:v}=x.terms[y];for(let w=0;w>>t.space.x,F=b>>>t.space.f;if(!c.documents.get(F))continue;let S=p.get(b);typeof S>"u"&&p.set(b,S=new j(k()));let yt=M&T[t.space.x];S.add(yt,y)}})}),c.documents.forEach(g=>{let m={id:g,matches:[]};i.items.push(m),d.set(g,m)}),p.forEach((g,m)=>{let y=m>>>t.space.f,x=m&T[t.space.f];d.get(y).matches.push({id:m,field:l[x].name,value:{filter:g},score:0})})}return z(o,(f,c)=>{var h;return(h=c.onFilterResult)==null?void 0:h.call(c,f,t,n)},i)}function st(t){let{fields:e}=t;return(n,r)=>{if(It(n))return n;let l=[Bt(r)],o=[],a=0;return H(n,({type:s,data:u})=>{switch(s){case"value":let i=e.findIndex(({name:c})=>c===u.field);if(i===-1){l[a++]=U(r);break}let d=u.value;if(typeof d!="object"){let c=new j(k()),h=r.shards[i],p=h.index.get(d);if(typeof p<"u")for(let g=0;gf+1&&a--;){I(l[f].documents,l[a].documents);for(let c=0;cf+1&&a--;){V(l[f].documents,l[a].documents);for(let c=0;cf+1&&a--;)lt(l[f].documents,l[a].documents)}}}),{select:l[0],values:o}}}function Lt(t){return{name:t.name,data:t.data,onFilterOptions:t.onFilterOptions,onFilterInput:t.onFilterInput,onFilterStore:t.onFilterStore,onFilterQuery:t.onFilterQuery,onFilterResult:t.onFilterResult}}function ot(t){return typeof t=="object"&&t!==null&&"type"in t&&"data"in t}function Nt(t){return typeof t=="object"&&t!==null&&"select"in t&&"values"in t}function Gt(t){return t.normalize("NFKD").toLowerCase()}function Ht(t,e){let n=Math.min(t.length,e.length);for(let r=0;r65535)){let o=e(l=t.codePointAt(n),n);if(typeof o<"u")return o}}function ut(t,e,n=0,r=t.length){let l=k();return Jt(t,o=>{l.push(o);let a=e(String.fromCodePoint(...l),l.length);if(typeof a<"u")return a},n,r)}function Wt(t,e,n=0,r=t.length){let l=n;for(let o=0;ln&&e(n,n=l);continue;case 62:n=l+1}l>n&&e(n,l)}function it(t,e,n,r=0){return Wt(t,(l,o)=>e(t,(a,s)=>{r=n({value:t.slice(a,s),index:r,start:a,end:s})},l,o)),r}function Yt(t,e,n,r=0){for(let l=0,o=0;l(a.start+=o,a.end+=o,n(a)),r);return r}function Zt(t){let e=new RegExp(t,"gu");return(n,r,l=0,o=n.length)=>{var u;e.lastIndex=l;let a,s=0;do{a=e.exec(n);let i=(u=a==null?void 0:a.index)!=null?u:o;l"u")continue;let p=f<{var y;return(y=m.onTextInput)==null?void 0:y.call(m,g,p,a)},h),h=Array.isArray(h)?h:[h],Yt(h,n,g=>{let m=z(o,(y,x)=>{var v;return(v=x.onTextTokens)==null?void 0:v.call(x,y)},[g]);for(let y=0;y"u"?s.set(x,[p<{var c;return(c=f.onTextStore)==null?void 0:c.call(f,i,e,t)}),i})}function Xt(t,e,n){let{documents:r,terms:l}=_(t);return n<0||n>=t.count.fields?{documents:r,terms:l}:(e.forEach(o=>{let{occurrences:a}=t.terms[o];for(let s=0;s>>t.space.x;if((u&T[t.space.f])!==n)continue;let i=u>>>t.space.f;r.set(i)}l.set(o)}),{documents:r,terms:l})}function te(t,e){let{documents:n,terms:r}=_(t),l=t.space.f+t.space.x;return e.forEach(o=>{let{occurrences:a}=t.terms[o];for(let s=0;s>>l);r.set(o)}),{documents:n,terms:r}}function _(t){return{documents:new O(t.count.documents),terms:new O(t.terms.length)}}function ee(t,e,n){let{compiler:r,fields:l,plugins:o=[]}=n,{input:a,scope:s,abort:u=!1}=z(o,(f,c)=>{var h;return(h=c.onTextQuery)==null?void 0:h.call(c,f,t,n)},e),i={items:[],query:{select:_(t),values:[]}},d=new Map;if(u===!1){let f=r(n),{select:c,values:h}=f(a,t);typeof s<"u"&&V(c.documents,s);let p=new O(l.length),g=new Map;i.query={select:c,values:h},c.terms.forEach(m=>{A(p,0);for(let x=0;x>>t.space.x,M=w>>>t.space.f;if(!c.documents.get(M))continue;let b=w&T[t.space.f];if(!p.get(b))continue;let F=g.get(w);typeof F>"u"&&g.set(w,F=new j(k()));let S=v&T[t.space.x];F.add(S,m)}}),c.documents.forEach(m=>{let y={id:m,matches:[]};i.items.push(y),d.set(m,y)}),g.forEach((m,y)=>{let x=y>>>t.space.f,v=y&T[t.space.f];d.get(x).matches.push({id:y,field:l[v].name,value:{text:m},score:0})})}return z(o,(f,c)=>{var h;return(h=c.onTextResult)==null?void 0:h.call(c,f,t,n)},i)}function ne(t,e=10){return t.length>1?1+t[t.length-1]-t[0]:e}function re(t,e,n,r=10){let l=[];t.value.text.forEach((s,u)=>{for(let i=0;is.index-u.index);let o=l.slice(0,1),a=0;for(let s=0;sr||i.value===u.value)d=o.map(({index:f})=>f),o=[l[s+1]];else{for(let f=0;fi.index-u.index){let h=o.splice(f+1);d=o.map(({index:p})=>p),o=[...h,l[s+1]]}else d=o.map(({index:h})=>h),o=[l[s+1]];break}}typeof d>"u"&&o.push(l[s+1])}if(typeof d<"u"){let f=n(d,a++);if(typeof f<"u")return f}}if(o.length)return n(o.map(({index:s})=>s),a)}function le(t){let{transform:e,parser:n,fields:r}=t,l=n(t);return(o,a)=>{if(Nt(o))return o;typeof o=="string"&&(o=l(o));let s=[_(a)],u=[],i=0;return H(o,({type:d,data:f})=>{switch(d){case"value":let c=f.value;if(typeof c=="string"){let p=new j(k()),g=a.index.get(e(c));typeof g<"u"&&p.add(g,1),c=p}if(f.field==="*")s[i++]=te(a,c);else{let p=r.findIndex(({name:g})=>g===f.field);s[i++]=Xt(a,c,p)}u.push(Q(R({},f),{value:c}));break;case"operator":let h=i-f.operands.length;switch(f.operator){case"or":for(;i>h+1&&i--;)I(s[h].documents,s[i].documents),I(s[h].terms,s[i].terms);break;case"and":for(;i>h+1&&i--;)V(s[h].documents,s[i].documents),I(s[h].terms,s[i].terms);break;case"not":for(at(s[h].documents),A(s[h].terms,0);i>h+1&&i--;)lt(s[h].documents,s[i].documents)}}}),{select:s[0],values:u}}}function ft(t,e){return H(t,(n,r,l)=>{if(n.type!=="value")return;let o=e(n.data,r,l);if(typeof o<"u")return o})}function ct(t){if(t.length===0)return[];let e=[],n=[];for(let o=0;oo.index-a.index);let r=new Set([n[0].value]),l=n[0].index;for(let o=1;o{t[i].start>l||t[i].end{e.push({start:l,end:o,value:n})})}return new J(ct(e))}function ae(t,e="or",n){let{separator:r}=t;return n!=null||(n=l=>({field:"*",value:l.value})),l=>{let o=[];return it(l,r,a=>{let s=n(a);typeof s<"u"&&o.push({type:"value",data:s})}),{type:"operator",data:{operator:e,operands:o}}}}function se(t,e){return E(this,null,function*(){let n=yield tt(e),r=yield At(n,(o,a)=>{var s;return(s=a.onTextOptions)==null?void 0:s.call(a,o,t)},Q(R({},e),{plugins:n})),l=yield $t(t,r);return D("text",o=>{if(o.type!=="text")throw new P("unsupported");return{type:o.type,data:ee(l,o.data,r)}})})}function q(t){return{name:t.name,data:t.data,onTextOptions:t.onTextOptions,onTextInput:t.onTextInput,onTextTokens:t.onTextTokens,onTextStore:t.onTextStore,onTextQuery:t.onTextQuery,onTextResult:t.onTextResult}}function oe(t){let{handlers:e}=t,n,r=new Map;return Lt({name:"aggregation",onFilterStore(l,o){for(let a=0;a"u")continue;let u=!0;o.documents.forEach(i=>{u=!1}),u&&A(o.documents,1),l.aggregations.push(s(a,o))}}})}function ue(t={}){let{empty:e=!1,limit:n}=t;return(r,{fields:l})=>{let o=r.space.f+r.space.x;return D("term",({type:a,data:s},{documents:u})=>{if(a!=="term")throw new P("unsupported");let i=l.findIndex(({name:f})=>f===s.field),d=Ut(r,i,f=>{let c=0,{occurrences:h}=f;for(let p=0;p>>o)&&c++;if(!(e===!1&&c===0))return{value:f.value,count:c}},R(R({},n),s.limit));return{type:a,data:{field:s.field,value:d}}})}}function ie(t,e="prefix"){return{type:e,data:t}}function fe(t){return typeof t=="object"&&"type"in t&&typeof t.type=="string"&&"data"in t&&typeof t.data=="string"}function ce(t,e={}){var u;let{prefix:n=2,filter:r=[]}=e,l=t.terms,o=new Map,a=Ot(l.length),s=k();for(let i=0;i{var p;return o.set(c,(p=o.get(c))!=null?p:i),h===n||void 0});let f=i?l[i-1]:"";a[i]=Ht(f,d)}for(let i=0;ii-d),{terms:l,index:o,cover:a,exact:s}}function de(t,e){let n="",r=-1,l=-1;if(ut(e,s=>{let u=t.index.get(s);if(typeof u>"u")return!0;n=s,r=u}),r!==-1)for(let s=n.length;ss>r&&sa),index:e.index},{prefix:t.prefix,filter:(l=t.filter)==null?void 0:l.map(r)}))},onTextQuery(e,n,r){let{transform:l,parser:o}=r;if(typeof e.input=="string")e.input=o(r)(e.input);else if(!ot(e.input))return;ft(e.input,a=>{var u;let s=a.value;if(fe(s))s=l(s.data);else return;a.value=(u=de(this.data,s))!=null?u:s})}})}function pe(t){let e=Q(R({},t),{plugins:[]}),n,r,l;return q({name:"filter",onTextOptions(a,s){return E(this,null,function*(){e.plugins=yield tt(t),l=yield Vt(s,e)})},onTextQuery(a){typeof a.filter<"u"&&(n=a.filter,r!=null||(r=st(e)),n.input=r(n.input,l),a.scope=n.input.select.documents)},onTextResult(a){if(typeof n<"u"){let s=!0;a.query.select.documents.forEach(i=>{s=!1}),s||(n.scope=a.query.select.documents);let u=Kt(l,n,e);a.aggregations=u.aggregations,n=void 0}}})}function ht(t,e){let n=[],r=t/e>>>0,l=t%e,o=0;if(r)for(let a=0;as);r.sort((a,s)=>t.terms[a].length-t.terms[s].length||t.terms[a].localeCompare(t.terms[s]));let l=0;for(let a=0;al&&(l=t.terms[a].length);let o=[];for(let a=0;a"u"?u[d].set(f,[r[a]]):c.push(r[a])}}return{index:o,terms:t.terms,idxmp:r}}var ye=[["id","di","rr"],["dr","rd"],["dd"]];function pt(t,e,n=2){if(t.lengthn)return;let a,s,u,i=n+1;for(let d of ye[o]){for(u=a=s=0;an)break;switch(d[u-1]){case"d":a++;break;case"i":s++;break;case"r":a++,s++;break}}else a++,s++;u+=r-a+(l-s),u"u")continue;let a=me(e,o,2);for(let s=0;s"u"))for(let c of f){let h=t.terms[c].length,p=n(t.terms[c],e);typeof p<"u"&&r.add(c,(h-p)/h)}}}}if(r.data.length)return r}function we(t={}){return q({name:"fuzzy",onTextStore(e){var n;(n=this.data)!=null||(this.data=xe({terms:e.terms.map(({value:r})=>r)},t))},onTextQuery(e,n,r){let{transform:l,parser:o}=r;if(typeof e.input=="string")e.input=o(r)(e.input);else if(!ot(e.input))return;ft(e.input,a=>{var u;let s=a.value;if(typeof s=="string")s=l(s);else return;n.index.get(s)||(a.value=(u=ve(this.data,s))!=null?u:s)})}})}function be(){return{tables:new Map}}function Te(t,e={}){let{count:n}=e;return D("term",r=>{let l=dt(r);return(o,a)=>{let s=[];return o.value.text.forEach((u,i)=>{let d=a[u]>>>10,f=a[u]&T[10];for(let p=0;pu.start-i.start),{ranges:ct(s).slice(0,n)}}})}function Me(t){let e,n;return q({name:"highlight",data:be(),onTextInput(r,l){let{tables:o}=this.data;o.set(l,n=k())},onTextTokens(r){for(let l=0;l{let s=l.get(a.id);if(a.value.highlight)return;let u=o(a,s);a.value=Q(R({},a.value),{highlight:u})})}})}function ke(){return{directives:[]}}function gt(...t){return(e,n)=>{for(let r=0;r{if(r!=="match")throw new P("unsupported");let o=Fe(t),a=gt(...e.map(s=>s(n)));return $(n,({matches:s})=>{s.sort(o)}),(s,u)=>{let i=Math.min(s.matches.length,u.matches.length);for(let d=0,f=0;dr*(l.get(a.field)-l.get(s.field))}function Re(t,e={}){let n=dt(t.query),r=G(re),l=G(ne);return(o,a)=>{let s=r(o,n,f=>f),u=r(a,n,f=>f);if(s.length!==u.length)return u.length-s.length;let i=l(s),d=l(u);return i!==d?i-d:s[0]!==u[0]?s[0]-u[0]:0}}function qe(t){let e=new Map;return q({name:"order",data:ke(),onTextOptions(r,l){return E(this,null,function*(){for(let o=0;o"u")throw new P("unknown");o.push(u(r,s))}r.items.sort(gt(...o))}})}function ze(t){let e=t.handler();return q({name:"pagination",onTextQuery(n){return e.onQuery(n,t)},onTextResult(n){return e.onResult(n,t)}})}function Ae(t){let{id:e,size:n=10,from:r=0}=t;if(r-n>=0)return{id:e,size:n,from:r-n}}function Qe(t,e){let{id:n,size:r=10,from:l=0}=t;if(l+rE(null,null,function*(){let e=t.data;switch(e.type){case 0:Z=yield se(e.data.items,{separator:Zt(e.data.config.separator),transform:G(Gt),parser:r=>ae(r,"and",l=>({field:"*",value:ie(l.value),range:{start:l.start,end:l.end,value:l.index}})),compiler:le,fields:[C("title",r=>r.title,{weight:3}),C("text",r=>r.text),C("path",r=>r.path,{weight:2})],plugins:[he(),we(),pe({compiler:st,fields:[C("tags",r=>r.tags)],plugins:[oe({handlers:[ue()]})]}),qe({handlers:[r=>Ee({fields:r.fields,comparators:[Re]})],defaults:{order:[{type:"match",data:{field:"*"}}]}}),()=>q({onTextResult(r){r.total=r.items.length}}),ze({handler:Se,size:10}),Me({handler:r=>Te(r)}),()=>q({onTextResult(r){let{query:l}=r,o=l.values.map(({range:a,value:s})=>{var i,d;let u=!1;return s.forEach((f,c)=>{!u&&c<1&&(u=!0)}),u?-1:((i=a==null?void 0:a.end)!=null?i:0)-((d=a==null?void 0:a.start)!=null?d:0)});X(r,a=>{var s;(s=a.value.highlight)==null||s.ranges.forEach(u=>{u.value=o[u.value]})})}})]}),self.postMessage({type:1});break;case 2:let n=Z({type:"text",data:e.data});self.postMessage({type:3,data:n.data});break}})});var _e=qt(xt());})(); diff --git a/docs/site/assets/stylesheets/classic/main.96fc3bb8.min.css b/docs/site/assets/stylesheets/classic/main.96fc3bb8.min.css deleted file mode 100644 index d880a52..0000000 --- a/docs/site/assets/stylesheets/classic/main.96fc3bb8.min.css +++ /dev/null @@ -1 +0,0 @@ -@charset "UTF-8";html{-webkit-text-size-adjust:none;-moz-text-size-adjust:none;text-size-adjust:none;box-sizing:border-box}*,:after,:before{box-sizing:inherit}@media (prefers-reduced-motion){*,:after,:before{transition:none!important}}body{margin:0}a,button,input,label{-webkit-tap-highlight-color:transparent}a{color:inherit;text-decoration:none}hr{border:0;box-sizing:initial;display:block;height:.05rem;overflow:visible;padding:0}small{font-size:80%}sub,sup{line-height:1em}img{border-style:none}table{border-collapse:initial;border-spacing:0}td,th{font-weight:400;vertical-align:top}button{background:#0000;border:0;font-family:inherit;font-size:inherit;margin:0;padding:0}input{border:0;outline:none}:root{--md-primary-fg-color:#4051b5;--md-primary-fg-color--light:#5d6cc0;--md-primary-fg-color--dark:#303fa1;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3;--md-accent-fg-color:#526cfe;--md-accent-fg-color--transparent:#526cfe1a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-scheme=default]{color-scheme:light}[data-md-color-scheme=default] img[src$="#gh-dark-mode-only"],[data-md-color-scheme=default] img[src$="#only-dark"]{display:none}:root,[data-md-color-scheme=default]{--md-hue:225deg;--md-default-fg-color:#000000de;--md-default-fg-color--light:#0000008a;--md-default-fg-color--lighter:#00000052;--md-default-fg-color--lightest:#00000012;--md-default-bg-color:#fff;--md-default-bg-color--light:#ffffffb3;--md-default-bg-color--lighter:#ffffff4d;--md-default-bg-color--lightest:#ffffff1f;--md-code-fg-color:#36464e;--md-code-bg-color:#f5f5f5;--md-code-bg-color--light:#f5f5f5b3;--md-code-bg-color--lighter:#f5f5f54d;--md-code-hl-color:#4287ff;--md-code-hl-color--light:#4287ff1a;--md-code-hl-number-color:#d52a2a;--md-code-hl-special-color:#db1457;--md-code-hl-function-color:#a846b9;--md-code-hl-constant-color:#6e59d9;--md-code-hl-keyword-color:#3f6ec6;--md-code-hl-string-color:#1c7d4d;--md-code-hl-name-color:var(--md-code-fg-color);--md-code-hl-operator-color:var(--md-default-fg-color--light);--md-code-hl-punctuation-color:var(--md-default-fg-color--light);--md-code-hl-comment-color:var(--md-default-fg-color--light);--md-code-hl-generic-color:var(--md-default-fg-color--light);--md-code-hl-variable-color:var(--md-default-fg-color--light);--md-typeset-color:var(--md-default-fg-color);--md-typeset-a-color:var(--md-primary-fg-color);--md-typeset-del-color:#f5503d26;--md-typeset-ins-color:#0bd57026;--md-typeset-kbd-color:#fafafa;--md-typeset-kbd-accent-color:#fff;--md-typeset-kbd-border-color:#b8b8b8;--md-typeset-mark-color:#ffff0080;--md-typeset-table-color:#0000001f;--md-typeset-table-color--light:rgba(0,0,0,.035);--md-admonition-fg-color:var(--md-default-fg-color);--md-admonition-bg-color:var(--md-default-bg-color);--md-warning-fg-color:#000000de;--md-warning-bg-color:#ff9;--md-footer-fg-color:#fff;--md-footer-fg-color--light:#ffffffb3;--md-footer-fg-color--lighter:#ffffff73;--md-footer-bg-color:#000000de;--md-footer-bg-color--dark:#00000052;--md-shadow-z1:0 0.2rem 0.5rem #0000000d,0 0 0.05rem #0000001a;--md-shadow-z2:0 0.2rem 0.5rem #0000001a,0 0 0.05rem #00000040;--md-shadow-z3:0 0.2rem 0.5rem #0003,0 0 0.05rem #00000059;--color-foreground:0 0 0;--color-background:255 255 255;--color-background-subtle:240 240 240;--color-backdrop:255 255 255}.md-icon svg{fill:currentcolor;display:block;height:1.2rem;width:1.2rem}.md-icon svg.lucide{fill:#0000;stroke:currentcolor}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;--md-text-font-family:var(--md-text-font,_),-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;--md-code-font-family:var(--md-code-font,_),SFMono-Regular,Consolas,Menlo,monospace}aside,body,input{font-feature-settings:"kern","liga";color:var(--md-typeset-color);font-family:var(--md-text-font-family)}code,kbd,pre{font-feature-settings:"kern";font-family:var(--md-code-font-family)}:root{--md-typeset-table-sort-icon:url('data:image/svg+xml;charset=utf-8,');--md-typeset-table-sort-icon--asc:url('data:image/svg+xml;charset=utf-8,');--md-typeset-table-sort-icon--desc:url('data:image/svg+xml;charset=utf-8,')}.md-typeset{-webkit-print-color-adjust:exact;color-adjust:exact;font-size:.8rem;line-height:1.6;overflow-wrap:break-word}@media print{.md-typeset{font-size:.68rem}}.md-typeset blockquote,.md-typeset dl,.md-typeset figure,.md-typeset ol,.md-typeset pre,.md-typeset ul{margin-bottom:1em;margin-top:1em}.md-typeset h1{color:var(--md-default-fg-color--light);font-size:2em;line-height:1.3;margin:0 0 1.25em}.md-typeset h1,.md-typeset h2{font-weight:300;letter-spacing:-.01em}.md-typeset h2{font-size:1.5625em;line-height:1.4;margin:1.6em 0 .64em}.md-typeset h3{font-size:1.25em;font-weight:400;letter-spacing:-.01em;line-height:1.5;margin:1.6em 0 .8em}.md-typeset h2+h3{margin-top:.8em}.md-typeset h4{font-weight:700;letter-spacing:-.01em;margin:1em 0}.md-typeset h5,.md-typeset h6{color:var(--md-default-fg-color--light);font-size:.8em;font-weight:700;letter-spacing:-.01em;margin:1.25em 0}.md-typeset h5{text-transform:uppercase}.md-typeset h5 code{text-transform:none}.md-typeset hr{border-bottom:.05rem solid var(--md-default-fg-color--lightest);display:flow-root;margin:1.5em 0}.md-typeset a{color:var(--md-typeset-a-color);word-break:break-word}.md-typeset a,.md-typeset a:before{transition:color 125ms}.md-typeset a:focus,.md-typeset a:hover{color:var(--md-accent-fg-color)}.md-typeset a:focus code,.md-typeset a:hover code{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-typeset a code{color:var(--md-typeset-a-color)}.md-typeset a.focus-visible{outline-color:var(--md-accent-fg-color);outline-offset:.2rem}.md-typeset code,.md-typeset kbd,.md-typeset pre{color:var(--md-code-fg-color);direction:ltr;font-variant-ligatures:none;transition:background-color 125ms}@media print{.md-typeset code,.md-typeset kbd,.md-typeset pre{white-space:pre-wrap}}.md-typeset code{background-color:var(--md-code-bg-color);border-radius:.1rem;-webkit-box-decoration-break:clone;box-decoration-break:clone;font-size:.85em;padding:0 .2941176471em;transition:color 125ms,background-color 125ms;word-break:break-word}.md-typeset code:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}.md-typeset pre{display:flow-root;line-height:1.4;position:relative}.md-typeset pre>code{-webkit-box-decoration-break:slice;box-decoration-break:slice;box-shadow:none;display:block;margin:0;outline-color:var(--md-accent-fg-color);overflow:auto;padding:.7720588235em 1.1764705882em;scrollbar-color:var(--md-default-fg-color--lighter) #0000;scrollbar-width:thin;touch-action:auto;word-break:normal}.md-typeset pre>code:hover{scrollbar-color:var(--md-accent-fg-color) #0000}.md-typeset pre>code::-webkit-scrollbar{height:.2rem;width:.2rem}.md-typeset pre>code::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-typeset pre>code::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}.md-typeset kbd{background-color:var(--md-typeset-kbd-color);border-radius:.1rem;box-shadow:0 .1rem 0 .05rem var(--md-typeset-kbd-border-color),0 .1rem 0 var(--md-typeset-kbd-border-color),0 -.1rem .2rem var(--md-typeset-kbd-accent-color) inset;color:var(--md-default-fg-color);display:inline-block;font-size:.75em;padding:0 .6666666667em;vertical-align:text-top;word-break:break-word}.md-typeset mark{background-color:var(--md-typeset-mark-color);-webkit-box-decoration-break:clone;box-decoration-break:clone;color:inherit;word-break:break-word}.md-typeset abbr{cursor:help;text-decoration:none}.md-typeset [data-preview],.md-typeset abbr{border-bottom:.05rem dotted var(--md-default-fg-color--light)}.md-typeset small{opacity:.75}[dir=ltr] .md-typeset sub,[dir=ltr] .md-typeset sup{margin-left:.078125em}[dir=rtl] .md-typeset sub,[dir=rtl] .md-typeset sup{margin-right:.078125em}[dir=ltr] .md-typeset blockquote{padding-left:.6rem}[dir=rtl] .md-typeset blockquote{padding-right:.6rem}[dir=ltr] .md-typeset blockquote{border-left:.2rem solid var(--md-default-fg-color--lighter)}[dir=rtl] .md-typeset blockquote{border-right:.2rem solid var(--md-default-fg-color--lighter)}.md-typeset blockquote{color:var(--md-default-fg-color--light);margin-left:0;margin-right:0}.md-typeset ul{list-style-type:disc}.md-typeset ul[type]{list-style-type:revert-layer}[dir=ltr] .md-typeset ol,[dir=ltr] .md-typeset ul{margin-left:.625em}[dir=rtl] .md-typeset ol,[dir=rtl] .md-typeset ul{margin-right:.625em}.md-typeset ol,.md-typeset ul{padding:0}.md-typeset ol:not([hidden]),.md-typeset ul:not([hidden]){display:flow-root}.md-typeset ol ol,.md-typeset ul ol{list-style-type:lower-alpha}.md-typeset ol ol ol,.md-typeset ul ol ol{list-style-type:lower-roman}.md-typeset ol ol ol ol,.md-typeset ul ol ol ol{list-style-type:upper-alpha}.md-typeset ol ol ol ol ol,.md-typeset ul ol ol ol ol{list-style-type:upper-roman}.md-typeset ol[type],.md-typeset ul[type]{list-style-type:revert-layer}[dir=ltr] .md-typeset ol li,[dir=ltr] .md-typeset ul li{margin-left:1.25em}[dir=rtl] .md-typeset ol li,[dir=rtl] .md-typeset ul li{margin-right:1.25em}.md-typeset ol li,.md-typeset ul li{margin-bottom:.5em}.md-typeset ol li blockquote,.md-typeset ol li p,.md-typeset ul li blockquote,.md-typeset ul li p{margin:.5em 0}.md-typeset ol li:last-child,.md-typeset ul li:last-child{margin-bottom:0}[dir=ltr] .md-typeset ol li ol,[dir=ltr] .md-typeset ol li ul,[dir=ltr] .md-typeset ul li ol,[dir=ltr] .md-typeset ul li ul{margin-left:.625em}[dir=rtl] .md-typeset ol li ol,[dir=rtl] .md-typeset ol li ul,[dir=rtl] .md-typeset ul li ol,[dir=rtl] .md-typeset ul li ul{margin-right:.625em}.md-typeset ol li ol,.md-typeset ol li ul,.md-typeset ul li ol,.md-typeset ul li ul{margin-bottom:.5em;margin-top:.5em}[dir=ltr] .md-typeset dd{margin-left:1.875em}[dir=rtl] .md-typeset dd{margin-right:1.875em}.md-typeset dd{margin-bottom:1.5em;margin-top:1em}.md-typeset img,.md-typeset svg,.md-typeset video{height:auto;max-width:100%}.md-typeset img[align=left]{margin:1em 1em 1em 0}.md-typeset img[align=right]{margin:1em 0 1em 1em}.md-typeset img[align]:only-child{margin-top:0}.md-typeset figure{display:flow-root;margin:1em auto;max-width:100%;text-align:center;width:fit-content}.md-typeset figure img{display:block;margin:0 auto}.md-typeset figcaption{font-style:italic;margin:1em auto;max-width:24rem}.md-typeset iframe{max-width:100%}.md-typeset table:not([class]){background-color:var(--md-default-bg-color);border:.05rem solid var(--md-typeset-table-color);border-radius:.1rem;display:inline-block;font-size:.64rem;max-width:100%;overflow:auto;touch-action:auto}@media print{.md-typeset table:not([class]){display:table}}.md-typeset table:not([class])+*{margin-top:1.5em}.md-typeset table:not([class]) td>:first-child,.md-typeset table:not([class]) th>:first-child{margin-top:0}.md-typeset table:not([class]) td>:last-child,.md-typeset table:not([class]) th>:last-child{margin-bottom:0}.md-typeset table:not([class]) td:not([align]),.md-typeset table:not([class]) th:not([align]){text-align:left}[dir=rtl] .md-typeset table:not([class]) td:not([align]),[dir=rtl] .md-typeset table:not([class]) th:not([align]){text-align:right}.md-typeset table:not([class]) th{font-weight:700;min-width:5rem;padding:.9375em 1.25em;vertical-align:top}.md-typeset table:not([class]) td{border-top:.05rem solid var(--md-typeset-table-color);padding:.9375em 1.25em;vertical-align:top}.md-typeset table:not([class]) tbody tr{transition:background-color 125ms}.md-typeset table:not([class]) tbody tr:hover{background-color:var(--md-typeset-table-color--light);box-shadow:0 .05rem 0 var(--md-default-bg-color) inset}.md-typeset table:not([class]) a{word-break:normal}.md-typeset table th[role=columnheader]{cursor:pointer}[dir=ltr] .md-typeset table th[role=columnheader]:after{margin-left:.5em}[dir=rtl] .md-typeset table th[role=columnheader]:after{margin-right:.5em}.md-typeset table th[role=columnheader]:after{content:"";display:inline-block;height:1.2em;-webkit-mask-image:var(--md-typeset-table-sort-icon);mask-image:var(--md-typeset-table-sort-icon);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:background-color 125ms;vertical-align:text-bottom;width:1.2em}.md-typeset table th[role=columnheader]:hover:after{background-color:var(--md-default-fg-color--lighter)}.md-typeset table th[role=columnheader][aria-sort=ascending]:after{background-color:var(--md-default-fg-color--light);-webkit-mask-image:var(--md-typeset-table-sort-icon--asc);mask-image:var(--md-typeset-table-sort-icon--asc)}.md-typeset table th[role=columnheader][aria-sort=descending]:after{background-color:var(--md-default-fg-color--light);-webkit-mask-image:var(--md-typeset-table-sort-icon--desc);mask-image:var(--md-typeset-table-sort-icon--desc)}.md-typeset__scrollwrap{margin:1em -.8rem;overflow-x:auto;touch-action:auto}.md-typeset__table{display:inline-block;margin-bottom:.5em;padding:0 .8rem}@media print{.md-typeset__table{display:block}}html .md-typeset__table table{display:table;margin:0;overflow:hidden;width:100%}@media screen and (max-width:44.984375em){.md-content__inner>pre{margin:1em -.8rem}.md-content__inner>pre code{border-radius:0}}.md-typeset .md-author{border-radius:100%;display:block;flex-shrink:0;height:1.6rem;overflow:hidden;position:relative;transition:color 125ms,transform 125ms;width:1.6rem}.md-typeset .md-author img{display:block}.md-typeset .md-author--more{background:var(--md-default-fg-color--lightest);color:var(--md-default-fg-color--lighter);font-size:.6rem;font-weight:700;line-height:1.6rem;text-align:center}.md-typeset .md-author--long{height:2.4rem;width:2.4rem}.md-typeset a.md-author{transform:scale(1)}.md-typeset a.md-author img{border-radius:100%;filter:grayscale(100%) opacity(75%);transition:filter 125ms}.md-typeset a.md-author:focus,.md-typeset a.md-author:hover{transform:scale(1.1);z-index:1}.md-typeset a.md-author:focus img,.md-typeset a.md-author:hover img{filter:grayscale(0)}.md-banner{background-color:var(--md-footer-bg-color);color:var(--md-footer-fg-color);overflow:auto}@media print{.md-banner{display:none}}.md-banner--warning{background-color:var(--md-warning-bg-color);color:var(--md-warning-fg-color)}.md-banner__inner{font-size:.7rem;margin:.6rem auto;padding:0 .8rem}[dir=ltr] .md-banner__button{float:right}[dir=rtl] .md-banner__button{float:left}.md-banner__button{color:inherit;cursor:pointer;transition:opacity .25s}.no-js .md-banner__button{display:none}.md-banner__button:hover{opacity:.7}html{scrollbar-gutter:stable;font-size:125%;height:100%;overflow-x:hidden}@media screen and (min-width:100em){html{font-size:137.5%}}@media screen and (min-width:125em){html{font-size:150%}}body{background-color:var(--md-default-bg-color);display:flex;flex-direction:column;font-size:.5rem;min-height:100%;position:relative;width:100%}@media print{body{display:block}}@media screen and (max-width:59.984375em){body[data-md-scrolllock]{position:fixed}}.md-grid{margin-left:auto;margin-right:auto;max-width:61rem}.md-container{display:flex;flex-direction:column;flex-grow:1}@media print{.md-container{display:block}}.md-main{flex-grow:1}.md-main__inner{display:flex;height:100%;margin-top:1.5rem}.md-ellipsis{overflow:hidden;text-overflow:ellipsis}.md-toggle{display:none}.md-option{height:0;opacity:0;position:absolute;width:0}.md-option:checked+label:not([hidden]){display:block}.md-option.focus-visible+label{outline-color:var(--md-accent-fg-color);outline-style:auto}.md-skip{background-color:var(--md-default-fg-color);border-radius:.1rem;color:var(--md-default-bg-color);font-size:.64rem;margin:.5rem;opacity:0;outline-color:var(--md-accent-fg-color);padding:.3rem .5rem;position:fixed;transform:translateY(.4rem);z-index:-1}.md-skip:focus{opacity:1;transform:translateY(0);transition:transform .25s cubic-bezier(.4,0,.2,1),opacity 175ms 75ms;z-index:10}@page{margin:25mm}:root{--md-clipboard-icon:url('data:image/svg+xml;charset=utf-8,')}.md-clipboard{border-radius:.1rem;color:var(--md-default-fg-color--lightest);cursor:pointer;height:1.5em;outline-color:var(--md-accent-fg-color);outline-offset:.1rem;transition:color .25s;width:1.5em;z-index:1}@media print{.md-clipboard{display:none}}.md-clipboard:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}:hover>.md-clipboard{color:var(--md-default-fg-color--light)}.md-clipboard:focus,.md-clipboard:hover{color:var(--md-accent-fg-color)}.md-clipboard:after{background-color:currentcolor;content:"";display:block;height:1.125em;margin:0 auto;-webkit-mask-image:var(--md-clipboard-icon);mask-image:var(--md-clipboard-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:1.125em}.md-clipboard--inline{cursor:pointer}.md-clipboard--inline code{transition:color .25s,background-color .25s}.md-clipboard--inline:focus code,.md-clipboard--inline:hover code{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}:root{--md-code-select-icon:url('data:image/svg+xml;charset=utf-8,');--md-code-copy-icon:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .md-code__content{display:grid}.md-code__nav{background-color:var(--md-code-bg-color--lighter);border-radius:.1rem;display:flex;gap:.2rem;padding:.2rem;position:absolute;right:.25em;top:.25em;transition:background-color .25s;z-index:1}:hover>.md-code__nav{background-color:var(--md-code-bg-color--light)}.md-code__button{color:var(--md-default-fg-color--lightest);cursor:pointer;display:block;height:1.5em;outline-color:var(--md-accent-fg-color);outline-offset:.1rem;transition:color .25s;width:1.5em}:hover>*>.md-code__button{color:var(--md-default-fg-color--light)}.md-code__button.focus-visible,.md-code__button:hover{color:var(--md-accent-fg-color)}.md-code__button--active{color:var(--md-default-fg-color)!important}.md-code__button:after{background-color:currentcolor;content:"";display:block;height:1.125em;margin:0 auto;-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:1.125em}.md-code__button[data-md-type=select]:after{-webkit-mask-image:var(--md-code-select-icon);mask-image:var(--md-code-select-icon)}.md-code__button[data-md-type=copy]:after{-webkit-mask-image:var(--md-code-copy-icon);mask-image:var(--md-code-copy-icon)}@keyframes consent{0%{opacity:0;transform:translateY(100%)}to{opacity:1;transform:translateY(0)}}@keyframes overlay{0%{opacity:0}to{opacity:1}}.md-consent__overlay{animation:overlay .25s both;-webkit-backdrop-filter:blur(.1rem);backdrop-filter:blur(.1rem);background-color:#0000008a;height:100%;opacity:1;position:fixed;top:0;width:100%;z-index:5}.md-consent__inner{animation:consent .5s cubic-bezier(.1,.7,.1,1) both;background-color:var(--md-default-bg-color);border:0;border-radius:.1rem;bottom:0;box-shadow:0 0 .2rem #0000001a,0 .2rem .4rem #0003;max-height:100%;overflow:auto;padding:0;position:fixed;width:100%;z-index:5}.md-consent__form{padding:.8rem}.md-consent__settings{display:none;margin:1em 0}input:checked+.md-consent__settings{display:block}.md-consent__controls{margin-bottom:.8rem}.md-typeset .md-consent__controls .md-button{display:inline}@media screen and (max-width:44.984375em){.md-typeset .md-consent__controls .md-button{display:block;margin-top:.4rem;text-align:center;width:100%}}.md-consent label{cursor:pointer}.md-content{flex-grow:1;min-width:0}.md-content__inner{margin:0 .8rem 1.2rem;padding-top:.6rem}@media screen and (min-width:76.25em){[dir=ltr] .md-sidebar--primary:not([hidden])~.md-content>.md-content__inner{margin-left:1.2rem}[dir=ltr] .md-sidebar--secondary:not([hidden])~.md-content>.md-content__inner,[dir=rtl] .md-sidebar--primary:not([hidden])~.md-content>.md-content__inner{margin-right:1.2rem}[dir=rtl] .md-sidebar--secondary:not([hidden])~.md-content>.md-content__inner{margin-left:1.2rem}}.md-content__inner:before{content:"";display:block;height:.4rem}.md-content__inner>:last-child{margin-bottom:0}[dir=ltr] .md-content__button{float:right}[dir=rtl] .md-content__button{float:left}[dir=ltr] .md-content__button{margin-left:.4rem}[dir=rtl] .md-content__button{margin-right:.4rem}.md-content__button{margin:.4rem 0;padding:0}@media print{.md-content__button{display:none}}.md-typeset .md-content__button{color:var(--md-default-fg-color--lighter)}.md-content__button svg{display:inline;vertical-align:top}[dir=rtl] .md-content__button svg{transform:scaleX(-1)}.md-content__button svg.lucide{fill:#0000;stroke:currentcolor}[dir=ltr] .md-dialog{right:.8rem}[dir=rtl] .md-dialog{left:.8rem}.md-dialog{background-color:var(--md-default-fg-color);border-radius:.1rem;bottom:.8rem;box-shadow:var(--md-shadow-z3);min-width:11.1rem;opacity:0;padding:.4rem .6rem;pointer-events:none;position:fixed;transform:translateY(100%);transition:transform 0ms .4s,opacity .4s;z-index:4}@media print{.md-dialog{display:none}}.md-dialog--active{opacity:1;pointer-events:auto;transform:translateY(0);transition:transform .4s cubic-bezier(.075,.85,.175,1),opacity .4s}.md-dialog__inner{color:var(--md-default-bg-color);font-size:.7rem}.md-feedback{margin:2em 0 1em;text-align:center}.md-feedback fieldset{border:none;margin:0;padding:0}.md-feedback__title{font-weight:700;margin:1em auto}.md-feedback__inner{position:relative}.md-feedback__list{display:flex;flex-wrap:wrap;place-content:baseline center;position:relative}.md-feedback__list:hover .md-icon:not(:disabled){color:var(--md-default-fg-color--lighter)}:disabled .md-feedback__list{min-height:1.8rem}.md-feedback__icon{color:var(--md-default-fg-color--light);cursor:pointer;flex-shrink:0;margin:0 .1rem;transition:color 125ms}.md-feedback__icon:not(:disabled).md-icon:hover{color:var(--md-accent-fg-color)}.md-feedback__icon:disabled{color:var(--md-default-fg-color--lightest);pointer-events:none}.md-feedback__note{opacity:0;position:relative;transform:translateY(.4rem);transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .15s}.md-feedback__note>*{margin:0 auto;max-width:16rem}:disabled .md-feedback__note{opacity:1;transform:translateY(0)}@media print{.md-feedback{display:none}}.md-footer{background-color:var(--md-footer-bg-color);color:var(--md-footer-fg-color)}@media print{.md-footer{display:none}}.md-footer__inner{justify-content:space-between;overflow:auto;padding:.2rem}.md-footer__inner:not([hidden]){display:flex}.md-footer__link{align-items:end;display:flex;flex-grow:0.01;margin-bottom:.4rem;margin-top:1rem;max-width:100%;outline-color:var(--md-accent-fg-color);overflow:hidden;transition:opacity .25s}.md-footer__link:focus,.md-footer__link:hover{opacity:.7}[dir=rtl] .md-footer__link svg{transform:scaleX(-1)}@media screen and (max-width:44.984375em){.md-footer__link--prev{flex-shrink:0}.md-footer__link--prev .md-footer__title{display:none}}[dir=ltr] .md-footer__link--next{margin-left:auto}[dir=rtl] .md-footer__link--next{margin-right:auto}.md-footer__link--next{text-align:right}[dir=rtl] .md-footer__link--next{text-align:left}.md-footer__title{flex-grow:1;font-size:.9rem;margin-bottom:.7rem;max-width:calc(100% - 2.4rem);padding:0 1rem;white-space:nowrap}.md-footer__button{margin:.2rem;padding:.4rem}.md-footer__direction{font-size:.64rem;opacity:.7}.md-footer-meta{background-color:var(--md-footer-bg-color--dark)}.md-footer-meta__inner{display:flex;flex-wrap:wrap;justify-content:space-between;padding:.2rem}html .md-footer-meta.md-typeset a{color:var(--md-footer-fg-color--light)}html .md-footer-meta.md-typeset a:focus,html .md-footer-meta.md-typeset a:hover{color:var(--md-footer-fg-color)}.md-copyright{color:var(--md-footer-fg-color--lighter);font-size:.64rem;margin:auto .6rem;padding:.4rem 0;width:100%}@media screen and (min-width:45em){.md-copyright{width:auto}}.md-copyright__highlight{color:var(--md-footer-fg-color--light)}.md-social{display:inline-flex;gap:.2rem;margin:0 .4rem;padding:.2rem 0 .6rem}@media screen and (min-width:45em){.md-social{padding:.6rem 0}}.md-social__link{display:inline-block;height:1.6rem;text-align:center;width:1.6rem}.md-social__link:before{line-height:1.9}.md-social__link svg{fill:currentcolor;max-height:.8rem;vertical-align:-25%}.md-social__link svg.lucide{fill:#0000;stroke:currentcolor}.md-typeset .md-button{border:.1rem solid;border-radius:.1rem;color:var(--md-primary-fg-color);cursor:pointer;display:inline-block;font-weight:700;padding:.625em 2em;transition:color 125ms,background-color 125ms,border-color 125ms}.md-typeset .md-button--primary{background-color:var(--md-primary-fg-color);border-color:var(--md-primary-fg-color);color:var(--md-primary-bg-color)}.md-typeset .md-button:focus,.md-typeset .md-button:hover{background-color:var(--md-accent-fg-color);border-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}[dir=ltr] .md-typeset .md-input{border-top-left-radius:.1rem}[dir=ltr] .md-typeset .md-input,[dir=rtl] .md-typeset .md-input{border-top-right-radius:.1rem}[dir=rtl] .md-typeset .md-input{border-top-left-radius:.1rem}.md-typeset .md-input{border-bottom:.1rem solid var(--md-default-fg-color--lighter);box-shadow:var(--md-shadow-z1);font-size:.8rem;height:1.8rem;padding:0 .6rem;transition:border .25s,box-shadow .25s}.md-typeset .md-input:focus,.md-typeset .md-input:hover{border-bottom-color:var(--md-accent-fg-color);box-shadow:var(--md-shadow-z2)}.md-typeset .md-input--stretch{width:100%}.md-header{background-color:var(--md-primary-fg-color);box-shadow:0 0 .2rem #0000,0 .2rem .4rem #0000;color:var(--md-primary-bg-color);display:block;left:0;position:sticky;right:0;top:0;z-index:4}@media print{.md-header{display:none}}.md-header[hidden]{transform:translateY(-100%);transition:transform .25s cubic-bezier(.8,0,.6,1),box-shadow .25s}.md-header--shadow{box-shadow:0 0 .2rem #0000001a,0 .2rem .4rem #0003;transition:transform .25s cubic-bezier(.1,.7,.1,1),box-shadow .25s}.md-header__inner{align-items:center;display:flex;padding:0 .2rem}.md-header__button{color:currentcolor;cursor:pointer;margin:.2rem;outline-color:var(--md-accent-fg-color);padding:.4rem;position:relative;transition:opacity .25s;vertical-align:middle;z-index:1}.md-header__button:hover{opacity:.7}.md-header__button:not([hidden]){display:inline-block}.md-header__button:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}.md-header__button.md-logo{margin:.2rem;padding:.4rem}@media screen and (max-width:76.234375em){.md-header__button.md-logo{display:none}}.md-header__button.md-logo img,.md-header__button.md-logo svg{fill:currentcolor;display:block;height:1.2rem;width:auto}@media screen and (min-width:60em){.md-header__button[for=__search]{display:none}}.no-js .md-header__button[for=__search]{display:none}[dir=rtl] .md-header__button[for=__search] svg{transform:scaleX(-1)}@media screen and (min-width:76.25em){.md-header__button[for=__drawer]{display:none}}.md-header__topic{display:flex;max-width:100%;position:absolute;transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .15s;white-space:nowrap}.md-header__topic+.md-header__topic{opacity:0;pointer-events:none;transform:translateX(1.25rem);transition:transform .4s cubic-bezier(1,.7,.1,.1),opacity .15s;z-index:-1}[dir=rtl] .md-header__topic+.md-header__topic{transform:translateX(-1.25rem)}.md-header__topic:first-child{font-weight:700}[dir=ltr] .md-header__title{margin-left:1rem;margin-right:.4rem}[dir=rtl] .md-header__title{margin-left:.4rem;margin-right:1rem}.md-header__title{flex-grow:1;font-size:.9rem;height:2.4rem;line-height:2.4rem}.md-header__title--active .md-header__topic{opacity:0;pointer-events:none;transform:translateX(-1.25rem);transition:transform .4s cubic-bezier(1,.7,.1,.1),opacity .15s;z-index:-1}[dir=rtl] .md-header__title--active .md-header__topic{transform:translateX(1.25rem)}.md-header__title--active .md-header__topic+.md-header__topic{opacity:1;pointer-events:auto;transform:translateX(0);transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .15s;z-index:0}.md-header__title>.md-header__ellipsis{height:100%;position:relative;width:100%}.md-header__option{display:flex;flex-shrink:0;max-width:100%;white-space:nowrap}.md-header__option>input{bottom:0}.md-header__source{display:none}@media screen and (min-width:60em){[dir=ltr] .md-header__source{margin-left:1rem}[dir=rtl] .md-header__source{margin-right:1rem}.md-header__source{display:block;max-width:11.7rem;width:11.7rem}}@media screen and (min-width:76.25em){[dir=ltr] .md-header__source{margin-left:1.4rem}[dir=rtl] .md-header__source{margin-right:1.4rem}}.md-meta{color:var(--md-default-fg-color--light);font-size:.7rem;line-height:1.3}.md-meta__list{display:inline-flex;flex-wrap:wrap;list-style:none;margin:0;padding:0}.md-meta__item:not(:last-child):after{content:"·";margin-left:.2rem;margin-right:.2rem}.md-meta__link{color:var(--md-typeset-a-color)}.md-meta__link:focus,.md-meta__link:hover{color:var(--md-accent-fg-color)}.md-draft{background-color:#ff1744;border-radius:.125em;color:#fff;display:inline-block;font-weight:700;padding-left:.5714285714em;padding-right:.5714285714em}:root{--md-nav-icon--prev:url('data:image/svg+xml;charset=utf-8,');--md-nav-icon--next:url('data:image/svg+xml;charset=utf-8,');--md-toc-icon:url('data:image/svg+xml;charset=utf-8,')}.md-nav{font-size:.7rem;line-height:1.3}.md-nav__title{color:var(--md-default-fg-color--light);display:block;font-weight:700;overflow:hidden;padding:0 .6rem;text-overflow:ellipsis}.md-nav__title .md-nav__button{display:none}.md-nav__title .md-nav__button img{height:100%;width:auto}.md-nav__title .md-nav__button.md-logo img,.md-nav__title .md-nav__button.md-logo svg{fill:currentcolor;display:block;height:2.4rem;max-width:100%;object-fit:contain;width:auto}.md-nav__list{list-style:none;margin:0;padding:0}.md-nav__link{align-items:flex-start;display:flex;gap:.4rem;margin-top:.625em;scroll-snap-align:start;transition:color 125ms}.md-nav__link--passed,.md-nav__link--passed code{color:var(--md-default-fg-color--light)}.md-nav__item .md-nav__link--active,.md-nav__item .md-nav__link--active code{color:var(--md-typeset-a-color)}.md-nav__link .md-ellipsis{position:relative}.md-nav__link .md-ellipsis code{word-break:normal}[dir=ltr] .md-nav__link .md-icon:last-child{margin-left:auto}[dir=rtl] .md-nav__link .md-icon:last-child{margin-right:auto}.md-nav__link .md-typeset{font-size:.7rem;line-height:1.3}.md-nav__link svg{fill:currentcolor;flex-shrink:0;height:1.3em;position:relative;width:1.3em}.md-nav__link svg.lucide{fill:#0000;stroke:currentcolor}.md-nav__link[for]:focus,.md-nav__link[for]:hover,.md-nav__link[href]:focus,.md-nav__link[href]:hover{color:var(--md-accent-fg-color);cursor:pointer}.md-nav__link[for]:focus code,.md-nav__link[for]:hover code,.md-nav__link[href]:focus code,.md-nav__link[href]:hover code{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-nav__link.focus-visible{outline-color:var(--md-accent-fg-color);outline-offset:.2rem}.md-nav--primary .md-nav__link[for=__toc]{display:none}.md-nav--primary .md-nav__link[for=__toc] .md-icon:after{background-color:currentcolor;display:block;height:100%;-webkit-mask-image:var(--md-toc-icon);mask-image:var(--md-toc-icon);width:100%}.md-nav--primary .md-nav__link[for=__toc]~.md-nav{display:none}.md-nav__container>.md-nav__link{margin-top:0}.md-nav__container>.md-nav__link:first-child{flex-grow:1;min-width:0}.md-nav__icon{flex-shrink:0}.md-nav__source{display:none}@media screen and (max-width:76.234375em){.md-nav--primary,.md-nav--primary .md-nav{background-color:var(--md-default-bg-color);display:flex;flex-direction:column;height:100%;left:0;position:absolute;right:0;top:0;z-index:1}.md-nav--primary .md-nav__item,.md-nav--primary .md-nav__title{font-size:.8rem;line-height:1.5}.md-nav--primary .md-nav__title{background-color:var(--md-default-fg-color--lightest);color:var(--md-default-fg-color--light);cursor:pointer;height:5.6rem;line-height:2.4rem;padding:3rem .8rem .2rem;position:relative;white-space:nowrap}[dir=ltr] .md-nav--primary .md-nav__title .md-nav__icon{left:.4rem}[dir=rtl] .md-nav--primary .md-nav__title .md-nav__icon{right:.4rem}.md-nav--primary .md-nav__title .md-nav__icon{display:block;height:1.2rem;margin:.2rem;position:absolute;top:.4rem;width:1.2rem}.md-nav--primary .md-nav__title .md-nav__icon:after{background-color:currentcolor;content:"";display:block;height:100%;-webkit-mask-image:var(--md-nav-icon--prev);mask-image:var(--md-nav-icon--prev);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:100%}.md-nav--primary .md-nav__title~.md-nav__list{background-color:var(--md-default-bg-color);box-shadow:0 .05rem 0 var(--md-default-fg-color--lightest) inset;overflow-y:auto;overscroll-behavior-y:contain;scroll-snap-type:y mandatory;touch-action:pan-y}.md-nav--primary .md-nav__title~.md-nav__list>:first-child{border-top:0}.md-nav--primary .md-nav__title[for=__drawer]{background-color:var(--md-primary-fg-color);color:var(--md-primary-bg-color);font-weight:700}.md-nav--primary .md-nav__title .md-logo{display:block;left:.2rem;margin:.2rem;padding:.4rem;position:absolute;right:.2rem;top:.2rem}.md-nav--primary .md-nav__list{flex:1}.md-nav--primary .md-nav__item{border-top:.05rem solid var(--md-default-fg-color--lightest)}.md-nav--primary .md-nav__item--active>.md-nav__link{color:var(--md-typeset-a-color)}.md-nav--primary .md-nav__item--active>.md-nav__link:focus,.md-nav--primary .md-nav__item--active>.md-nav__link:hover{color:var(--md-accent-fg-color)}.md-nav--primary .md-nav__link{margin-top:0;padding:.6rem .8rem}.md-nav--primary .md-nav__link svg{margin-top:.1em}.md-nav--primary .md-nav__link>.md-nav__link{padding:0}[dir=ltr] .md-nav--primary .md-nav__link .md-nav__icon{margin-right:-.2rem}[dir=rtl] .md-nav--primary .md-nav__link .md-nav__icon{margin-left:-.2rem}.md-nav--primary .md-nav__link .md-nav__icon{font-size:1.2rem;height:1.2rem;width:1.2rem}.md-nav--primary .md-nav__link .md-nav__icon:after{background-color:currentcolor;content:"";display:block;height:100%;-webkit-mask-image:var(--md-nav-icon--next);mask-image:var(--md-nav-icon--next);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:100%}[dir=rtl] .md-nav--primary .md-nav__icon:after{transform:scale(-1)}.md-nav--primary .md-nav--secondary .md-nav{background-color:initial;position:static}[dir=ltr] .md-nav--primary .md-nav--secondary .md-nav .md-nav__link{padding-left:1.4rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav__link{padding-right:1.4rem}[dir=ltr] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav__link{padding-left:2rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav__link{padding-right:2rem}[dir=ltr] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav__link{padding-left:2.6rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav__link{padding-right:2.6rem}[dir=ltr] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav .md-nav__link{padding-left:3.2rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav .md-nav__link{padding-right:3.2rem}.md-nav--secondary{background-color:initial}.md-nav__toggle~.md-nav{display:flex;opacity:0;transform:translateX(100%);transition:transform .25s cubic-bezier(.8,0,.6,1),opacity 125ms 50ms}[dir=rtl] .md-nav__toggle~.md-nav{transform:translateX(-100%)}.md-nav__toggle:checked~.md-nav{opacity:1;transform:translateX(0);transition:transform .25s cubic-bezier(.4,0,.2,1),opacity 125ms 125ms}.md-nav__toggle:checked~.md-nav>.md-nav__list{backface-visibility:hidden}}@media screen and (max-width:59.984375em){.md-nav--primary .md-nav__link[for=__toc]{display:flex}.md-nav--primary .md-nav__link[for=__toc] .md-icon:after{content:""}.md-nav--primary .md-nav__link[for=__toc]+.md-nav__link{display:none}.md-nav--primary .md-nav__link[for=__toc]~.md-nav{display:flex}.md-nav__source{background-color:var(--md-primary-fg-color--dark);color:var(--md-primary-bg-color);display:block;padding:0 .2rem}}@media screen and (min-width:60em) and (max-width:76.234375em){.md-nav--integrated .md-nav__link[for=__toc]{display:flex}.md-nav--integrated .md-nav__link[for=__toc] .md-icon:after{content:""}.md-nav--integrated .md-nav__link[for=__toc]+.md-nav__link{display:none}.md-nav--integrated .md-nav__link[for=__toc]~.md-nav{display:flex}}@media screen and (min-width:60em){.md-nav{margin-bottom:-.4rem}.md-nav--secondary .md-nav__title{background:var(--md-default-bg-color);box-shadow:0 0 .4rem .4rem var(--md-default-bg-color);position:sticky;top:0;z-index:1}.md-nav--secondary .md-nav__title[for=__toc]{scroll-snap-align:start}.md-nav--secondary .md-nav__title .md-nav__icon{display:none}[dir=ltr] .md-nav--secondary .md-nav__list{padding-left:.6rem}[dir=rtl] .md-nav--secondary .md-nav__list{padding-right:.6rem}.md-nav--secondary .md-nav__list{padding-bottom:.4rem}[dir=ltr] .md-nav--secondary .md-nav__item>.md-nav__link{margin-right:.4rem}[dir=rtl] .md-nav--secondary .md-nav__item>.md-nav__link{margin-left:.4rem}}@media screen and (min-width:76.25em){.md-nav{margin-bottom:-.4rem;transition:max-height .25s cubic-bezier(.86,0,.07,1)}.md-nav--primary .md-nav__title{background:var(--md-default-bg-color);box-shadow:0 0 .4rem .4rem var(--md-default-bg-color);position:sticky;top:0;z-index:1}.md-nav--primary .md-nav__title[for=__drawer]{scroll-snap-align:start}.md-nav--primary .md-nav__title .md-nav__icon{display:none}[dir=ltr] .md-nav--primary .md-nav__list{padding-left:.6rem}[dir=rtl] .md-nav--primary .md-nav__list{padding-right:.6rem}.md-nav--primary .md-nav__list{padding-bottom:.4rem}[dir=ltr] .md-nav--primary .md-nav__item>.md-nav__link{margin-right:.4rem}[dir=rtl] .md-nav--primary .md-nav__item>.md-nav__link{margin-left:.4rem}.md-nav__toggle~.md-nav{display:grid;grid-template-rows:minmax(.4rem,0fr);opacity:0;transition:grid-template-rows .25s cubic-bezier(.86,0,.07,1),opacity .25s,visibility 0ms .25s;visibility:collapse}.md-nav__toggle~.md-nav>.md-nav__list{overflow:hidden}.md-nav__toggle.md-toggle--indeterminate~.md-nav,.md-nav__toggle:checked~.md-nav{grid-template-rows:minmax(.4rem,1fr);opacity:1;transition:grid-template-rows .25s cubic-bezier(.86,0,.07,1),opacity .15s .1s,visibility 0ms;visibility:visible}.md-nav__toggle.md-toggle--indeterminate~.md-nav{transition:none}.md-nav__item--nested>.md-nav>.md-nav__title{display:none}.md-nav__item--section{display:block;margin:1.25em 0}.md-nav__item--section:last-child{margin-bottom:0}.md-nav__item--section>.md-nav__link{font-weight:700}.md-nav__item--section>.md-nav__link[for]{color:var(--md-default-fg-color--light)}.md-nav__item--section>.md-nav__link:not(.md-nav__container){pointer-events:none}.md-nav__item--section>.md-nav__link .md-icon,.md-nav__item--section>.md-nav__link>[for]{display:none}[dir=ltr] .md-nav__item--section>.md-nav{margin-left:-.6rem}[dir=rtl] .md-nav__item--section>.md-nav{margin-right:-.6rem}.md-nav__item--section>.md-nav{display:block;opacity:1;visibility:visible}.md-nav__item--section>.md-nav>.md-nav__list>.md-nav__item{padding:0}.md-nav__icon{border-radius:100%;height:.9rem;transition:background-color .25s;width:.9rem}.md-nav__icon:hover{background-color:var(--md-accent-fg-color--transparent)}.md-nav__icon:after{background-color:currentcolor;border-radius:100%;content:"";display:inline-block;height:100%;-webkit-mask-image:var(--md-nav-icon--next);mask-image:var(--md-nav-icon--next);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:transform .25s;vertical-align:-.1rem;width:100%}[dir=rtl] .md-nav__icon:after{transform:rotate(180deg)}.md-nav__item--nested .md-nav__toggle:checked~.md-nav__link .md-nav__icon:after,.md-nav__item--nested .md-toggle--indeterminate~.md-nav__link .md-nav__icon:after{transform:rotate(90deg)}.md-nav--lifted>.md-nav__list>.md-nav__item,.md-nav--lifted>.md-nav__title{display:none}.md-nav--lifted>.md-nav__list>.md-nav__item--active{display:block}.md-nav--lifted>.md-nav__list>.md-nav__item--active>.md-nav__link{background:var(--md-default-bg-color);box-shadow:0 0 .4rem .4rem var(--md-default-bg-color);margin-top:0;position:sticky;top:0;z-index:1}.md-nav--lifted>.md-nav__list>.md-nav__item--active>.md-nav__link:not(.md-nav__container){pointer-events:none}.md-nav--lifted>.md-nav__list>.md-nav__item--active.md-nav__item--section{margin:0}[dir=ltr] .md-nav--lifted>.md-nav__list>.md-nav__item>.md-nav:not(.md-nav--secondary){margin-left:-.6rem}[dir=rtl] .md-nav--lifted>.md-nav__list>.md-nav__item>.md-nav:not(.md-nav--secondary){margin-right:-.6rem}.md-nav--lifted>.md-nav__list>.md-nav__item>[for]{color:var(--md-default-fg-color--light)}.md-nav--lifted .md-nav[data-md-level="1"]{grid-template-rows:minmax(.4rem,1fr);opacity:1;visibility:visible}[dir=ltr] .md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary{border-left:.05rem solid var(--md-primary-fg-color)}[dir=rtl] .md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary{border-right:.05rem solid var(--md-primary-fg-color)}.md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary{display:block;margin-bottom:1.25em;opacity:1;visibility:visible}.md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary>.md-nav__list{overflow:visible;padding-bottom:0}.md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary>.md-nav__title{display:none}}.md-pagination{font-size:.8rem;font-weight:700;gap:.4rem}.md-pagination,.md-pagination>*{align-items:center;display:flex;justify-content:center}.md-pagination>*{border-radius:.2rem;height:1.8rem;min-width:1.8rem;text-align:center}.md-pagination__current{background-color:var(--md-default-fg-color--lightest);color:var(--md-default-fg-color--light)}.md-pagination__link{transition:color 125ms,background-color 125ms}.md-pagination__link:focus,.md-pagination__link:hover{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-pagination__link:focus svg,.md-pagination__link:hover svg{color:var(--md-accent-fg-color)}.md-pagination__link.focus-visible{outline-color:var(--md-accent-fg-color);outline-offset:.2rem}.md-pagination__link svg{fill:currentcolor;color:var(--md-default-fg-color--lighter);display:block;max-height:100%;width:1.2rem}:root{--md-path-icon:url('data:image/svg+xml;charset=utf-8,')}.md-path{font-size:.7rem;margin:0 .8rem;overflow:auto;padding-top:1.2rem}.md-path:not([hidden]){display:block}@media screen and (min-width:76.25em){.md-path{margin:0 1.2rem}}.md-path__list{align-items:center;display:flex;gap:.2rem;list-style:none;margin:0;padding:0}.md-path__item:not(:first-child){display:inline-flex;gap:.2rem;white-space:nowrap}.md-path__item:not(:first-child):before{background-color:var(--md-default-fg-color--lighter);content:"";display:inline;height:.8rem;-webkit-mask-image:var(--md-path-icon);mask-image:var(--md-path-icon);width:.8rem}.md-path__link{align-items:center;color:var(--md-default-fg-color--light);display:flex}.md-path__link:focus,.md-path__link:hover{color:var(--md-accent-fg-color)}:root{--md-post-pin-icon:url('data:image/svg+xml;charset=utf-8,')}.md-post__back{border-bottom:.05rem solid var(--md-default-fg-color--lightest);margin-bottom:1.2rem;padding-bottom:1.2rem}@media screen and (max-width:76.234375em){.md-post__back{display:none}}[dir=rtl] .md-post__back svg{transform:scaleX(-1)}.md-post__authors{display:flex;flex-direction:column;gap:.6rem;margin:0 .6rem 1.2rem}.md-post .md-post__meta a{transition:color 125ms}.md-post .md-post__meta a:focus,.md-post .md-post__meta a:hover{color:var(--md-accent-fg-color)}.md-post__title{color:var(--md-default-fg-color--light);font-weight:700}.md-post--excerpt{margin-bottom:3.2rem}.md-post--excerpt .md-post__header{align-items:center;display:flex;gap:.6rem;min-height:1.6rem}.md-post--excerpt .md-post__authors{align-items:center;display:inline-flex;flex-direction:row;gap:.2rem;margin:0;min-height:2.4rem}[dir=ltr] .md-post--excerpt .md-post__meta .md-meta__list{margin-right:.4rem}[dir=rtl] .md-post--excerpt .md-post__meta .md-meta__list{margin-left:.4rem}.md-post--excerpt .md-post__content>:first-child{--md-scroll-margin:6rem;margin-top:0}.md-post>.md-nav--secondary{margin:1em 0}.md-pin{background:var(--md-default-fg-color--lightest);border-radius:1rem;margin-top:-.05rem;padding:.2rem}.md-pin:after{background-color:currentcolor;content:"";display:block;height:.6rem;margin:0 auto;-webkit-mask-image:var(--md-post-pin-icon);mask-image:var(--md-post-pin-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:.6rem}.md-profile{align-items:center;display:flex;font-size:.7rem;gap:.6rem;line-height:1.4;width:100%}.md-profile__description{flex-grow:1}.md-content--post{display:flex}@media screen and (max-width:76.234375em){.md-content--post{flex-flow:column-reverse}}.md-content--post>.md-content__inner{flex-grow:1;min-width:0}@media screen and (min-width:76.25em){[dir=ltr] .md-content--post>.md-content__inner{margin-left:1.2rem}[dir=rtl] .md-content--post>.md-content__inner{margin-right:1.2rem}}@media screen and (max-width:76.234375em){.md-sidebar.md-sidebar--post{padding:0;position:static;width:100%}.md-sidebar.md-sidebar--post .md-sidebar__scrollwrap{overflow:visible}.md-sidebar.md-sidebar--post .md-sidebar__inner{padding:0}.md-sidebar.md-sidebar--post .md-post__meta{margin-left:.6rem;margin-right:.6rem}.md-sidebar.md-sidebar--post .md-nav__item{border:none;display:inline}.md-sidebar.md-sidebar--post .md-nav__list{display:inline-flex;flex-wrap:wrap;gap:.6rem;padding-bottom:.6rem;padding-top:.6rem}.md-sidebar.md-sidebar--post .md-nav__link{padding:0}.md-sidebar.md-sidebar--post .md-nav{height:auto;margin-bottom:0;position:static}}:root{--md-progress-value:0;--md-progress-delay:400ms}.md-progress{background:var(--md-primary-bg-color);height:.075rem;opacity:min(clamp(0,var(--md-progress-value),1),clamp(0,100 - var(--md-progress-value),1));position:fixed;top:0;transform:scaleX(calc(var(--md-progress-value)*1%));transform-origin:left;transition:transform .5s cubic-bezier(.19,1,.22,1),opacity .25s var(--md-progress-delay);width:100%;z-index:4}:root{--md-search-icon:url('data:image/svg+xml;charset=utf-8,')}.md-search{position:relative}@media screen and (min-width:60em){.md-search{padding:.2rem 0}}@media screen and (max-width:59.984375em){.md-search{display:none}}.no-js .md-search{display:none}[dir=ltr] .md-search__button{padding-left:1.9rem;padding-right:2.2rem}[dir=rtl] .md-search__button{padding-left:2.2rem;padding-right:1.9rem}.md-search__button{background:var(--md-primary-fg-color);color:var(--md-primary-bg-color);cursor:pointer;font-size:.7rem;position:relative;text-align:left}@media screen and (min-width:45em){.md-search__button{background-color:#00000042;border-radius:.2rem;height:1.6rem;transition:background-color .4s,color .4s;width:8.9rem}.md-search__button:focus,.md-search__button:hover{background-color:#ffffff1f;color:var(--md-primary-bg-color)}}[dir=ltr] .md-search__button:before{left:0}[dir=rtl] .md-search__button:before{right:0}.md-search__button:before{background-color:var(--md-primary-bg-color);content:"";height:1rem;margin-left:.5rem;-webkit-mask-image:var(--md-search-icon);mask-image:var(--md-search-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.3rem;width:1rem}.md-search__button:after{background:#00000042;border-radius:.1rem;content:"Ctrl+K";display:block;font-size:.6rem;padding:.1rem .2rem;position:absolute;right:.6rem;top:.35rem}[data-platform^=Mac] .md-search__button:after{content:"⌘K"}.md-select{position:relative;z-index:1}.md-select__inner{background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color);left:50%;margin-top:.2rem;max-height:0;opacity:0;position:absolute;top:calc(100% - .2rem);transform:translate3d(-50%,.3rem,0);transition:transform .25s 375ms,opacity .25s .25s,max-height 0ms .5s}@media screen and (max-width:59.984375em){.md-select__inner{left:100%;transform:translate3d(-100%,.3rem,0)}}.md-select:focus-within .md-select__inner,.md-select:hover .md-select__inner{max-height:min(75vh,28rem);opacity:1;transform:translate3d(-50%,0,0);transition:transform .25s cubic-bezier(.1,.7,.1,1),opacity .25s,max-height 0ms}@media screen and (max-width:59.984375em){.md-select:focus-within .md-select__inner,.md-select:hover .md-select__inner{transform:translate3d(-100%,0,0)}}.md-select__inner:after{border-bottom:.2rem solid #0000;border-bottom-color:var(--md-default-bg-color);border-left:.2rem solid #0000;border-right:.2rem solid #0000;border-top:0;content:"";filter:drop-shadow(0 -1px 0 var(--md-default-fg-color--lightest));height:0;left:50%;margin-left:-.2rem;margin-top:-.2rem;position:absolute;top:0;width:0}@media screen and (max-width:59.984375em){.md-select__inner:after{left:auto;right:1rem}}.md-select__list{border-radius:.1rem;font-size:.8rem;list-style-type:none;margin:0;max-height:inherit;overflow:auto;padding:0}.md-select__item{line-height:1.8rem}[dir=ltr] .md-select__link{padding-left:.6rem;padding-right:1.2rem}[dir=rtl] .md-select__link{padding-left:1.2rem;padding-right:.6rem}.md-select__link{cursor:pointer;display:block;outline:none;scroll-snap-align:start;transition:background-color .25s,color .25s;width:100%}.md-select__link:focus,.md-select__link:hover{color:var(--md-accent-fg-color)}.md-select__link:focus{background-color:var(--md-default-fg-color--lightest)}.md-sidebar{align-self:flex-start;flex-shrink:0;padding:1.2rem 0;position:sticky;top:2.4rem;width:12.1rem}@media print{.md-sidebar{display:none}}@media screen and (max-width:76.234375em){[dir=ltr] .md-sidebar--primary{left:-12.1rem}[dir=rtl] .md-sidebar--primary{right:-12.1rem}.md-sidebar--primary{background-color:var(--md-default-bg-color);display:block;height:100%;position:fixed;top:0;transform:translateX(0);transition:transform .25s cubic-bezier(.4,0,.2,1),box-shadow .25s;width:12.1rem;z-index:5}[data-md-toggle=drawer]:checked~.md-container .md-sidebar--primary{box-shadow:var(--md-shadow-z3);transform:translateX(12.1rem)}[dir=rtl] [data-md-toggle=drawer]:checked~.md-container .md-sidebar--primary{transform:translateX(-12.1rem)}.md-sidebar--primary .md-sidebar__scrollwrap{bottom:0;left:0;margin:0;overflow:hidden;overscroll-behavior-y:contain;position:absolute;right:0;scroll-snap-type:none;top:0}}@media screen and (min-width:76.25em){.md-sidebar{height:0}.no-js .md-sidebar{height:auto}.md-header--lifted~.md-container .md-sidebar{top:4.8rem}}.md-sidebar--secondary{display:none;order:2}@media screen and (min-width:60em){.md-sidebar--secondary{height:0}.no-js .md-sidebar--secondary{height:auto}.md-sidebar--secondary:not([hidden]){display:block}.md-sidebar--secondary .md-sidebar__scrollwrap{touch-action:pan-y}}.md-sidebar__scrollwrap{backface-visibility:hidden;margin:0 .2rem;overflow-y:auto;scrollbar-color:var(--md-default-fg-color--lighter) #0000}@media screen and (min-width:60em){.md-sidebar__scrollwrap{scrollbar-gutter:stable;scrollbar-width:thin}}.md-sidebar__scrollwrap::-webkit-scrollbar{height:.2rem;width:.2rem}.md-sidebar__scrollwrap:focus-within,.md-sidebar__scrollwrap:hover{scrollbar-color:var(--md-accent-fg-color) #0000}.md-sidebar__scrollwrap:focus-within::-webkit-scrollbar-thumb,.md-sidebar__scrollwrap:hover::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-sidebar__scrollwrap:focus-within::-webkit-scrollbar-thumb:hover,.md-sidebar__scrollwrap:hover::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}@supports selector(::-webkit-scrollbar){.md-sidebar__scrollwrap{scrollbar-gutter:auto}[dir=ltr] .md-sidebar__inner{padding-right:calc(100% - 11.5rem)}[dir=rtl] .md-sidebar__inner{padding-left:calc(100% - 11.5rem)}}@media screen and (max-width:76.234375em){.md-overlay{background-color:#0000008a;height:0;opacity:0;position:fixed;top:0;transition:width 0ms .25s,height 0ms .25s,opacity .25s;width:0;z-index:5}[data-md-toggle=drawer]:checked~.md-overlay{height:100%;opacity:1;transition:width 0ms,height 0ms,opacity .25s;width:100%}}@keyframes facts{0%{height:0}to{height:.65rem}}@keyframes fact{0%{opacity:0;transform:translateY(100%)}50%{opacity:0}to{opacity:1;transform:translateY(0)}}:root{--md-source-forks-icon:url('data:image/svg+xml;charset=utf-8,');--md-source-repositories-icon:url('data:image/svg+xml;charset=utf-8,');--md-source-stars-icon:url('data:image/svg+xml;charset=utf-8,');--md-source-version-icon:url('data:image/svg+xml;charset=utf-8,')}.md-source{backface-visibility:hidden;display:block;font-size:.65rem;line-height:1.2;outline-color:var(--md-accent-fg-color);transition:opacity .25s;white-space:nowrap}.md-source:hover{opacity:.7}.md-source__icon{display:inline-block;height:2.4rem;vertical-align:middle;width:2rem}[dir=ltr] .md-source__icon svg{margin-left:.6rem}[dir=rtl] .md-source__icon svg{margin-right:.6rem}.md-source__icon svg{margin-top:.6rem}[dir=ltr] .md-source__icon+.md-source__repository{padding-left:2rem}[dir=rtl] .md-source__icon+.md-source__repository{padding-right:2rem}[dir=ltr] .md-source__icon+.md-source__repository{margin-left:-2rem}[dir=rtl] .md-source__icon+.md-source__repository{margin-right:-2rem}[dir=ltr] .md-source__repository{margin-left:.6rem}[dir=rtl] .md-source__repository{margin-right:.6rem}.md-source__repository{display:inline-block;max-width:calc(100% - 1.2rem);overflow:hidden;text-overflow:ellipsis;vertical-align:middle}.md-source__facts{display:flex;font-size:.55rem;gap:.4rem;list-style-type:none;margin:.1rem 0 0;opacity:.75;overflow:hidden;padding:0;width:100%}.md-source__repository--active .md-source__facts{animation:facts .25s ease-in}.md-source__fact{overflow:hidden;text-overflow:ellipsis}.md-source__repository--active .md-source__fact{animation:fact .4s ease-out}[dir=ltr] .md-source__fact:before{margin-right:.1rem}[dir=rtl] .md-source__fact:before{margin-left:.1rem}.md-source__fact:before{background-color:currentcolor;content:"";display:inline-block;height:.6rem;-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;vertical-align:text-top;width:.6rem}.md-source__fact:nth-child(1n+2){flex-shrink:0}.md-source__fact--version:before{-webkit-mask-image:var(--md-source-version-icon);mask-image:var(--md-source-version-icon)}.md-source__fact--stars:before{-webkit-mask-image:var(--md-source-stars-icon);mask-image:var(--md-source-stars-icon)}.md-source__fact--forks:before{-webkit-mask-image:var(--md-source-forks-icon);mask-image:var(--md-source-forks-icon)}.md-source__fact--repositories:before{-webkit-mask-image:var(--md-source-repositories-icon);mask-image:var(--md-source-repositories-icon)}.md-source-file{margin:1em 0}[dir=ltr] .md-source-file__fact{margin-right:.6rem}[dir=rtl] .md-source-file__fact{margin-left:.6rem}.md-source-file__fact{align-items:center;color:var(--md-default-fg-color--light);display:inline-flex;font-size:.68rem;gap:.3rem}.md-source-file__fact .md-icon{flex-shrink:0;margin-bottom:.05rem}[dir=ltr] .md-source-file__fact .md-author{float:left}[dir=rtl] .md-source-file__fact .md-author{float:right}.md-source-file__fact .md-author{margin-right:.2rem}.md-source-file__fact svg{width:.9rem}:root{--md-status:url('data:image/svg+xml;charset=utf-8,');--md-status--new:url('data:image/svg+xml;charset=utf-8,');--md-status--deprecated:url('data:image/svg+xml;charset=utf-8,');--md-status--encrypted:url('data:image/svg+xml;charset=utf-8,')}.md-status:after{background-color:var(--md-default-fg-color--light);content:"";display:inline-block;height:1.125em;-webkit-mask-image:var(--md-status);mask-image:var(--md-status);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;vertical-align:text-bottom;width:1.125em}.md-status:hover:after{background-color:currentcolor}.md-status--new:after{-webkit-mask-image:var(--md-status--new);mask-image:var(--md-status--new)}.md-status--deprecated:after{-webkit-mask-image:var(--md-status--deprecated);mask-image:var(--md-status--deprecated)}.md-status--encrypted:after{-webkit-mask-image:var(--md-status--encrypted);mask-image:var(--md-status--encrypted)}.md-tabs{background-color:var(--md-primary-fg-color);color:var(--md-primary-bg-color);display:block;line-height:1.3;overflow:auto;width:100%;z-index:3}@media print{.md-tabs{display:none}}@media screen and (max-width:76.234375em){.md-tabs{display:none}}.md-tabs[hidden]{pointer-events:none}[dir=ltr] .md-tabs__list{margin-left:.2rem}[dir=rtl] .md-tabs__list{margin-right:.2rem}.md-tabs__list{contain:content;display:flex;list-style:none;margin:0;overflow:auto;padding:0;scrollbar-width:none;white-space:nowrap}.md-tabs__list::-webkit-scrollbar{display:none}.md-tabs__item{height:2.4rem;padding-left:.6rem;padding-right:.6rem}.md-tabs__item--active .md-tabs__link{color:inherit;opacity:1}.md-tabs__link{backface-visibility:hidden;display:flex;font-size:.7rem;margin-top:.8rem;opacity:.7;outline-color:var(--md-accent-fg-color);outline-offset:.2rem;transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .25s}.md-tabs__link:focus,.md-tabs__link:hover{color:inherit;opacity:1}[dir=ltr] .md-tabs__link svg{margin-right:.4rem}[dir=rtl] .md-tabs__link svg{margin-left:.4rem}.md-tabs__link svg{fill:currentcolor;height:1.3em}.md-tabs__item:nth-child(2) .md-tabs__link{transition-delay:20ms}.md-tabs__item:nth-child(3) .md-tabs__link{transition-delay:40ms}.md-tabs__item:nth-child(4) .md-tabs__link{transition-delay:60ms}.md-tabs__item:nth-child(5) .md-tabs__link{transition-delay:80ms}.md-tabs__item:nth-child(6) .md-tabs__link{transition-delay:.1s}.md-tabs__item:nth-child(7) .md-tabs__link{transition-delay:.12s}.md-tabs__item:nth-child(8) .md-tabs__link{transition-delay:.14s}.md-tabs__item:nth-child(9) .md-tabs__link{transition-delay:.16s}.md-tabs__item:nth-child(10) .md-tabs__link{transition-delay:.18s}.md-tabs__item:nth-child(11) .md-tabs__link{transition-delay:.2s}.md-tabs__item:nth-child(12) .md-tabs__link{transition-delay:.22s}.md-tabs__item:nth-child(13) .md-tabs__link{transition-delay:.24s}.md-tabs__item:nth-child(14) .md-tabs__link{transition-delay:.26s}.md-tabs__item:nth-child(15) .md-tabs__link{transition-delay:.28s}.md-tabs__item:nth-child(16) .md-tabs__link{transition-delay:.3s}.md-tabs[hidden] .md-tabs__link{opacity:0;transform:translateY(50%);transition:transform 0ms .1s,opacity .1s}:root{--md-tag-icon:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .md-tags:not([hidden]){display:inline-flex;flex-wrap:wrap;gap:.5em;margin-bottom:.75em;margin-top:-.125em}.md-typeset .md-tag{align-items:center;background:var(--md-default-fg-color--lightest);border-radius:2.4rem;display:inline-flex;font-size:.64rem;font-size:min(.8em,.64rem);font-weight:700;gap:.5em;letter-spacing:normal;line-height:1.6;padding:.3125em .78125em}.md-typeset .md-tag[href]{-webkit-tap-highlight-color:transparent;color:inherit;outline:none;transition:color 125ms,background-color 125ms}.md-typeset .md-tag[href]:focus,.md-typeset .md-tag[href]:hover{background-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}[id]>.md-typeset .md-tag{vertical-align:text-top}.md-typeset .md-tag-shadow{opacity:.5}.md-typeset .md-tag-icon:before{background-color:var(--md-default-fg-color--lighter);content:"";display:inline-block;height:1.2em;-webkit-mask-image:var(--md-tag-icon);mask-image:var(--md-tag-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:background-color 125ms;vertical-align:text-bottom;width:1.2em}.md-typeset .md-tag-icon[href]:focus:before,.md-typeset .md-tag-icon[href]:hover:before{background-color:var(--md-accent-bg-color)}@keyframes pulse{0%{transform:scale(.95)}75%{transform:scale(1)}to{transform:scale(.95)}}:root{--md-annotation-bg-icon:url('data:image/svg+xml;charset=utf-8,');--md-annotation-icon:url('data:image/svg+xml;charset=utf-8,')}.md-tooltip{backface-visibility:hidden;background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color);font-family:var(--md-text-font-family);left:clamp(var(--md-tooltip-0,0rem) + .8rem,var(--md-tooltip-x),100vw + var(--md-tooltip-0,0rem) + .8rem - var(--md-tooltip-width) - 2 * .8rem);max-width:calc(100vw - 1.6rem);opacity:0;position:absolute;top:var(--md-tooltip-y);transform:translateY(-.4rem);transition:transform 0ms .25s,opacity .25s,z-index .25s;width:var(--md-tooltip-width);z-index:0}.md-tooltip--active{opacity:1;transform:translateY(0);transition:transform .25s cubic-bezier(.1,.7,.1,1),opacity .25s,z-index 0ms;z-index:2}.md-tooltip--inline{font-weight:700;-webkit-user-select:none;user-select:none;width:auto}.md-tooltip--inline:not(.md-tooltip--active){transform:translateY(.2rem) scale(.9)}.md-tooltip--inline .md-tooltip__inner{font-size:.5rem;padding:.2rem .4rem}[hidden]+.md-tooltip--inline{display:none}.focus-visible>.md-tooltip,.md-tooltip:target{outline:var(--md-accent-fg-color) auto}.md-tooltip__inner{font-size:.64rem;padding:.8rem}.md-tooltip__inner.md-typeset>:first-child{margin-top:0}.md-tooltip__inner.md-typeset>:last-child{margin-bottom:0}.md-annotation{font-style:normal;font-weight:400;outline:none;text-align:initial;vertical-align:text-bottom;white-space:normal}[dir=rtl] .md-annotation{direction:rtl}code .md-annotation{font-family:var(--md-code-font-family);font-size:inherit}.md-annotation:not([hidden]){display:inline-block;line-height:1.25}.md-annotation__index{border-radius:.01px;cursor:pointer;display:inline-block;margin-left:.4ch;margin-right:.4ch;outline:none;overflow:hidden;position:relative;-webkit-user-select:none;user-select:none;vertical-align:text-top;z-index:0}.md-annotation .md-annotation__index{transition:z-index .25s}@media screen{.md-annotation__index{width:2.2ch}[data-md-visible]>.md-annotation__index{animation:pulse 2s infinite}.md-annotation__index:before{background:var(--md-default-bg-color);-webkit-mask-image:var(--md-annotation-bg-icon);mask-image:var(--md-annotation-bg-icon)}.md-annotation__index:after,.md-annotation__index:before{content:"";height:2.2ch;-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:-.1ch;width:2.2ch;z-index:-1}.md-annotation__index:after{background-color:var(--md-default-fg-color--lighter);-webkit-mask-image:var(--md-annotation-icon);mask-image:var(--md-annotation-icon);transform:scale(1.0001);transition:background-color .25s,transform .25s}.md-tooltip--active+.md-annotation__index:after{transform:rotate(45deg)}.md-tooltip--active+.md-annotation__index:after,:hover>.md-annotation__index:after{background-color:var(--md-accent-fg-color)}}.md-tooltip--active+.md-annotation__index{animation-play-state:paused;transition-duration:0ms;z-index:2}.md-annotation__index [data-md-annotation-id]{display:inline-block}@media print{.md-annotation__index [data-md-annotation-id]{background:var(--md-default-fg-color--lighter);border-radius:2ch;color:var(--md-default-bg-color);font-weight:700;padding:0 .6ch;white-space:nowrap}.md-annotation__index [data-md-annotation-id]:after{content:attr(data-md-annotation-id)}}.md-typeset .md-annotation-list{counter-reset:annotation;list-style:none!important}.md-typeset .md-annotation-list li{position:relative}[dir=ltr] .md-typeset .md-annotation-list li:before{left:-2.125em}[dir=rtl] .md-typeset .md-annotation-list li:before{right:-2.125em}.md-typeset .md-annotation-list li:before{background:var(--md-default-fg-color--lighter);border-radius:2ch;color:var(--md-default-bg-color);content:counter(annotation);counter-increment:annotation;font-size:.8875em;font-weight:700;height:2ch;line-height:1.25;min-width:2ch;padding:0 .6ch;position:absolute;text-align:center;top:.25em}:root{--md-tooltip-width:20rem;--md-tooltip-tail:0.3rem}.md-tooltip2{backface-visibility:hidden;color:var(--md-default-fg-color);font-family:var(--md-text-font-family);opacity:0;pointer-events:none;position:absolute;top:calc(var(--md-tooltip-host-y) + var(--md-tooltip-y));transform:translateY(-.4rem);transform-origin:calc(var(--md-tooltip-host-x) + var(--md-tooltip-x)) 0;transition:transform 0ms .25s,opacity .25s,z-index .25s;width:100%;z-index:0}.md-tooltip2:before{border-left:var(--md-tooltip-tail) solid #0000;border-right:var(--md-tooltip-tail) solid #0000;content:"";display:block;left:clamp(1.5 * .8rem,var(--md-tooltip-host-x) + var(--md-tooltip-x) - var(--md-tooltip-tail),100vw - 2 * var(--md-tooltip-tail) - 1.5 * .8rem);position:absolute;z-index:1}.md-tooltip2--top:before{border-top:var(--md-tooltip-tail) solid var(--md-default-bg-color);bottom:calc(var(--md-tooltip-tail)*-1 + .025rem);filter:drop-shadow(0 1px 0 hsla(0,0%,0%,.05))}.md-tooltip2--bottom:before{border-bottom:var(--md-tooltip-tail) solid var(--md-default-bg-color);filter:drop-shadow(0 -1px 0 hsla(0,0%,0%,.05));top:calc(var(--md-tooltip-tail)*-1 + .025rem)}.md-tooltip2--active{opacity:1;transform:translateY(0);transition:transform .4s cubic-bezier(0,1,.5,1),opacity .25s,z-index 0ms;z-index:4}.md-tooltip2__inner{scrollbar-gutter:stable;background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z2);left:clamp(.8rem,var(--md-tooltip-host-x) - .8rem,100vw - var(--md-tooltip-width) - .8rem);max-height:40vh;max-width:calc(100vw - 1.6rem);position:relative;scrollbar-width:thin}.md-tooltip2__inner::-webkit-scrollbar{height:.2rem;width:.2rem}.md-tooltip2__inner::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-tooltip2__inner::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}[role=dialog]>.md-tooltip2__inner{font-size:.64rem;overflow:auto;padding:0 .8rem;pointer-events:auto;width:var(--md-tooltip-width)}[role=dialog]>.md-tooltip2__inner:after,[role=dialog]>.md-tooltip2__inner:before{content:"";display:block;height:.8rem;position:sticky;width:100%;z-index:10}[role=dialog]>.md-tooltip2__inner:before{background:linear-gradient(var(--md-default-bg-color),#0000 75%);top:0}[role=dialog]>.md-tooltip2__inner:after{background:linear-gradient(#0000,var(--md-default-bg-color) 75%);bottom:0}[role=tooltip]>.md-tooltip2__inner{font-size:.5rem;font-weight:700;left:clamp(.8rem,var(--md-tooltip-host-x) + var(--md-tooltip-x) - var(--md-tooltip-width)/2,100vw - var(--md-tooltip-width) - .8rem);max-width:min(100vw - 2 * .8rem,400px);padding:.2rem .4rem;-webkit-user-select:none;user-select:none;width:fit-content}.md-tooltip2__inner.md-typeset>:first-child{margin-top:0}.md-tooltip2__inner.md-typeset>:last-child{margin-bottom:0}[dir=ltr] .md-top{margin-left:50%}[dir=rtl] .md-top{margin-right:50%}.md-top{background-color:var(--md-default-bg-color);border-radius:1.6rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color--light);cursor:pointer;display:block;font-size:.7rem;outline:none;padding:.4rem .8rem;position:fixed;top:3.2rem;transform:translate(-50%);transition:color 125ms,background-color 125ms,transform 125ms cubic-bezier(.4,0,.2,1),opacity 125ms;z-index:2}@media print{.md-top{display:none}}[dir=rtl] .md-top{transform:translate(50%)}.md-top[hidden]{opacity:0;pointer-events:none;transform:translate(-50%,.2rem);transition-duration:0ms}[dir=rtl] .md-top[hidden]{transform:translate(50%,.2rem)}.md-top:focus,.md-top:hover{background-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}.md-top svg{display:inline-block;vertical-align:-.5em}.md-top.lucide{fill:#0000;stroke:currentcolor}@keyframes hoverfix{0%{pointer-events:none}}:root{--md-version-icon:url('data:image/svg+xml;charset=utf-8,')}.md-version{flex-shrink:0;font-size:.8rem;height:2.4rem}[dir=ltr] .md-version__current{margin-left:1.4rem;margin-right:.4rem}[dir=rtl] .md-version__current{margin-left:.4rem;margin-right:1.4rem}.md-version__current{color:inherit;cursor:pointer;outline:none;position:relative;top:.05rem}[dir=ltr] .md-version__current:after{margin-left:.4rem}[dir=rtl] .md-version__current:after{margin-right:.4rem}.md-version__current:after{background-color:currentcolor;content:"";display:inline-block;height:.6rem;-webkit-mask-image:var(--md-version-icon);mask-image:var(--md-version-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:.4rem}.md-version__alias{margin-left:.3rem;opacity:.7}.md-version__list{background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color);list-style-type:none;margin:.2rem .8rem;max-height:0;opacity:0;overflow:auto;padding:0;position:absolute;scroll-snap-type:y mandatory;top:.15rem;transition:max-height 0ms .5s,opacity .25s .25s;z-index:3}.md-version:focus-within .md-version__list,.md-version:hover .md-version__list{max-height:10rem;opacity:1;transition:max-height 0ms,opacity .25s}@media (hover:none),(pointer:coarse){.md-version:hover .md-version__list{animation:hoverfix .25s forwards}.md-version:focus-within .md-version__list{animation:none}}.md-version__item{line-height:1.8rem}[dir=ltr] .md-version__link{padding-left:.6rem;padding-right:1.2rem}[dir=rtl] .md-version__link{padding-left:1.2rem;padding-right:.6rem}.md-version__link{cursor:pointer;display:block;outline:none;scroll-snap-align:start;transition:color .25s,background-color .25s;white-space:nowrap;width:100%}.md-version__link:focus,.md-version__link:hover{color:var(--md-accent-fg-color)}.md-version__link:focus{background-color:var(--md-default-fg-color--lightest)}:root{--md-admonition-icon--note:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--abstract:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--info:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--tip:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--success:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--question:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--warning:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--failure:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--danger:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--bug:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--example:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--quote:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .admonition,.md-typeset details{background-color:var(--md-admonition-bg-color);border:.075rem solid #448aff;border-radius:.2rem;box-shadow:var(--md-shadow-z1);color:var(--md-admonition-fg-color);display:flow-root;font-size:.64rem;margin:1.5625em 0;padding:0 .6rem;page-break-inside:avoid;transition:box-shadow 125ms}@media print{.md-typeset .admonition,.md-typeset details{box-shadow:none}}.md-typeset .admonition:focus-within,.md-typeset details:focus-within{box-shadow:0 0 0 .2rem #448aff1a}.md-typeset .admonition>*,.md-typeset details>*{box-sizing:border-box}.md-typeset .admonition .admonition,.md-typeset .admonition details,.md-typeset details .admonition,.md-typeset details details{margin-bottom:1em;margin-top:1em}.md-typeset .admonition .md-typeset__scrollwrap,.md-typeset details .md-typeset__scrollwrap{margin:1em -.6rem}.md-typeset .admonition .md-typeset__table,.md-typeset details .md-typeset__table{padding:0 .6rem}.md-typeset .admonition>.tabbed-set:only-child,.md-typeset details>.tabbed-set:only-child{margin-top:0}html .md-typeset .admonition>:last-child,html .md-typeset details>:last-child{margin-bottom:.6rem}[dir=ltr] .md-typeset .admonition-title,[dir=ltr] .md-typeset summary{padding-left:2rem;padding-right:.6rem}[dir=rtl] .md-typeset .admonition-title,[dir=rtl] .md-typeset summary{padding-left:.6rem;padding-right:2rem}[dir=ltr] .md-typeset .admonition-title,[dir=ltr] .md-typeset summary{border-left-width:.2rem}[dir=rtl] .md-typeset .admonition-title,[dir=rtl] .md-typeset summary{border-right-width:.2rem}[dir=ltr] .md-typeset .admonition-title,[dir=ltr] .md-typeset summary{border-top-left-radius:.1rem}[dir=ltr] .md-typeset .admonition-title,[dir=ltr] .md-typeset summary,[dir=rtl] .md-typeset .admonition-title,[dir=rtl] .md-typeset summary{border-top-right-radius:.1rem}[dir=rtl] .md-typeset .admonition-title,[dir=rtl] .md-typeset summary{border-top-left-radius:.1rem}.md-typeset .admonition-title,.md-typeset summary{background-color:#448aff1a;border:none;font-weight:700;margin:0 -.6rem;padding-bottom:.4rem;padding-top:.4rem;position:relative}html .md-typeset .admonition-title:last-child,html .md-typeset summary:last-child{margin-bottom:0}[dir=ltr] .md-typeset .admonition-title:before,[dir=ltr] .md-typeset summary:before{left:.6rem}[dir=rtl] .md-typeset .admonition-title:before,[dir=rtl] .md-typeset summary:before{right:.6rem}.md-typeset .admonition-title:before,.md-typeset summary:before{background-color:#448aff;content:"";height:1rem;-webkit-mask-image:var(--md-admonition-icon--note);mask-image:var(--md-admonition-icon--note);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.625em;width:1rem}.md-typeset .admonition-title code,.md-typeset summary code{box-shadow:0 0 0 .05rem var(--md-default-fg-color--lightest)}.md-typeset .admonition.note,.md-typeset details.note{border-color:#448aff}.md-typeset .admonition.note:focus-within,.md-typeset details.note:focus-within{box-shadow:0 0 0 .2rem #448aff1a}.md-typeset .note>.admonition-title,.md-typeset .note>summary{background-color:#448aff1a}.md-typeset .note>.admonition-title:before,.md-typeset .note>summary:before{background-color:#448aff;-webkit-mask-image:var(--md-admonition-icon--note);mask-image:var(--md-admonition-icon--note)}.md-typeset .note>.admonition-title:after,.md-typeset .note>summary:after{color:#448aff}.md-typeset .admonition.abstract,.md-typeset details.abstract{border-color:#00b0ff}.md-typeset .admonition.abstract:focus-within,.md-typeset details.abstract:focus-within{box-shadow:0 0 0 .2rem #00b0ff1a}.md-typeset .abstract>.admonition-title,.md-typeset .abstract>summary{background-color:#00b0ff1a}.md-typeset .abstract>.admonition-title:before,.md-typeset .abstract>summary:before{background-color:#00b0ff;-webkit-mask-image:var(--md-admonition-icon--abstract);mask-image:var(--md-admonition-icon--abstract)}.md-typeset .abstract>.admonition-title:after,.md-typeset .abstract>summary:after{color:#00b0ff}.md-typeset .admonition.info,.md-typeset details.info{border-color:#00b8d4}.md-typeset .admonition.info:focus-within,.md-typeset details.info:focus-within{box-shadow:0 0 0 .2rem #00b8d41a}.md-typeset .info>.admonition-title,.md-typeset .info>summary{background-color:#00b8d41a}.md-typeset .info>.admonition-title:before,.md-typeset .info>summary:before{background-color:#00b8d4;-webkit-mask-image:var(--md-admonition-icon--info);mask-image:var(--md-admonition-icon--info)}.md-typeset .info>.admonition-title:after,.md-typeset .info>summary:after{color:#00b8d4}.md-typeset .admonition.tip,.md-typeset details.tip{border-color:#00bfa5}.md-typeset .admonition.tip:focus-within,.md-typeset details.tip:focus-within{box-shadow:0 0 0 .2rem #00bfa51a}.md-typeset .tip>.admonition-title,.md-typeset .tip>summary{background-color:#00bfa51a}.md-typeset .tip>.admonition-title:before,.md-typeset .tip>summary:before{background-color:#00bfa5;-webkit-mask-image:var(--md-admonition-icon--tip);mask-image:var(--md-admonition-icon--tip)}.md-typeset .tip>.admonition-title:after,.md-typeset .tip>summary:after{color:#00bfa5}.md-typeset .admonition.success,.md-typeset details.success{border-color:#00c853}.md-typeset .admonition.success:focus-within,.md-typeset details.success:focus-within{box-shadow:0 0 0 .2rem #00c8531a}.md-typeset .success>.admonition-title,.md-typeset .success>summary{background-color:#00c8531a}.md-typeset .success>.admonition-title:before,.md-typeset .success>summary:before{background-color:#00c853;-webkit-mask-image:var(--md-admonition-icon--success);mask-image:var(--md-admonition-icon--success)}.md-typeset .success>.admonition-title:after,.md-typeset .success>summary:after{color:#00c853}.md-typeset .admonition.question,.md-typeset details.question{border-color:#64dd17}.md-typeset .admonition.question:focus-within,.md-typeset details.question:focus-within{box-shadow:0 0 0 .2rem #64dd171a}.md-typeset .question>.admonition-title,.md-typeset .question>summary{background-color:#64dd171a}.md-typeset .question>.admonition-title:before,.md-typeset .question>summary:before{background-color:#64dd17;-webkit-mask-image:var(--md-admonition-icon--question);mask-image:var(--md-admonition-icon--question)}.md-typeset .question>.admonition-title:after,.md-typeset .question>summary:after{color:#64dd17}.md-typeset .admonition.warning,.md-typeset details.warning{border-color:#ff9100}.md-typeset .admonition.warning:focus-within,.md-typeset details.warning:focus-within{box-shadow:0 0 0 .2rem #ff91001a}.md-typeset .warning>.admonition-title,.md-typeset .warning>summary{background-color:#ff91001a}.md-typeset .warning>.admonition-title:before,.md-typeset .warning>summary:before{background-color:#ff9100;-webkit-mask-image:var(--md-admonition-icon--warning);mask-image:var(--md-admonition-icon--warning)}.md-typeset .warning>.admonition-title:after,.md-typeset .warning>summary:after{color:#ff9100}.md-typeset .admonition.failure,.md-typeset details.failure{border-color:#ff5252}.md-typeset .admonition.failure:focus-within,.md-typeset details.failure:focus-within{box-shadow:0 0 0 .2rem #ff52521a}.md-typeset .failure>.admonition-title,.md-typeset .failure>summary{background-color:#ff52521a}.md-typeset .failure>.admonition-title:before,.md-typeset .failure>summary:before{background-color:#ff5252;-webkit-mask-image:var(--md-admonition-icon--failure);mask-image:var(--md-admonition-icon--failure)}.md-typeset .failure>.admonition-title:after,.md-typeset .failure>summary:after{color:#ff5252}.md-typeset .admonition.danger,.md-typeset details.danger{border-color:#ff1744}.md-typeset .admonition.danger:focus-within,.md-typeset details.danger:focus-within{box-shadow:0 0 0 .2rem #ff17441a}.md-typeset .danger>.admonition-title,.md-typeset .danger>summary{background-color:#ff17441a}.md-typeset .danger>.admonition-title:before,.md-typeset .danger>summary:before{background-color:#ff1744;-webkit-mask-image:var(--md-admonition-icon--danger);mask-image:var(--md-admonition-icon--danger)}.md-typeset .danger>.admonition-title:after,.md-typeset .danger>summary:after{color:#ff1744}.md-typeset .admonition.bug,.md-typeset details.bug{border-color:#f50057}.md-typeset .admonition.bug:focus-within,.md-typeset details.bug:focus-within{box-shadow:0 0 0 .2rem #f500571a}.md-typeset .bug>.admonition-title,.md-typeset .bug>summary{background-color:#f500571a}.md-typeset .bug>.admonition-title:before,.md-typeset .bug>summary:before{background-color:#f50057;-webkit-mask-image:var(--md-admonition-icon--bug);mask-image:var(--md-admonition-icon--bug)}.md-typeset .bug>.admonition-title:after,.md-typeset .bug>summary:after{color:#f50057}.md-typeset .admonition.example,.md-typeset details.example{border-color:#7c4dff}.md-typeset .admonition.example:focus-within,.md-typeset details.example:focus-within{box-shadow:0 0 0 .2rem #7c4dff1a}.md-typeset .example>.admonition-title,.md-typeset .example>summary{background-color:#7c4dff1a}.md-typeset .example>.admonition-title:before,.md-typeset .example>summary:before{background-color:#7c4dff;-webkit-mask-image:var(--md-admonition-icon--example);mask-image:var(--md-admonition-icon--example)}.md-typeset .example>.admonition-title:after,.md-typeset .example>summary:after{color:#7c4dff}.md-typeset .admonition.quote,.md-typeset details.quote{border-color:#9e9e9e}.md-typeset .admonition.quote:focus-within,.md-typeset details.quote:focus-within{box-shadow:0 0 0 .2rem #9e9e9e1a}.md-typeset .quote>.admonition-title,.md-typeset .quote>summary{background-color:#9e9e9e1a}.md-typeset .quote>.admonition-title:before,.md-typeset .quote>summary:before{background-color:#9e9e9e;-webkit-mask-image:var(--md-admonition-icon--quote);mask-image:var(--md-admonition-icon--quote)}.md-typeset .quote>.admonition-title:after,.md-typeset .quote>summary:after{color:#9e9e9e}:root{--md-footnotes-icon:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .footnote{color:var(--md-default-fg-color--light);font-size:.64rem}[dir=ltr] .md-typeset .footnote>ol{margin-left:0}[dir=rtl] .md-typeset .footnote>ol{margin-right:0}.md-typeset .footnote>ol>li{transition:color 125ms}.md-typeset .footnote>ol>li:target{color:var(--md-default-fg-color)}.md-typeset .footnote>ol>li:focus-within .footnote-backref{opacity:1;transform:translateX(0);transition:none}.md-typeset .footnote>ol>li:hover .footnote-backref,.md-typeset .footnote>ol>li:target .footnote-backref{opacity:1;transform:translateX(0)}.md-typeset .footnote>ol>li>:first-child{margin-top:0}.md-typeset .footnote-ref{font-size:.75em;font-weight:700}html .md-typeset .footnote-ref{outline-offset:.1rem}.md-typeset [id^="fnref:"]:target>.footnote-ref{outline:auto}.md-typeset .footnote-backref{color:var(--md-typeset-a-color);display:inline-block;font-size:0;opacity:0;transform:translateX(.25rem);transition:color .25s,transform .25s .25s,opacity 125ms .25s;vertical-align:text-bottom}@media print{.md-typeset .footnote-backref{color:var(--md-typeset-a-color);opacity:1;transform:translateX(0)}}[dir=rtl] .md-typeset .footnote-backref{transform:translateX(-.25rem)}.md-typeset .footnote-backref:hover{color:var(--md-accent-fg-color)}.md-typeset .footnote-backref:before{background-color:currentcolor;content:"";display:inline-block;height:.8rem;-webkit-mask-image:var(--md-footnotes-icon);mask-image:var(--md-footnotes-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:.8rem}[dir=rtl] .md-typeset .footnote-backref:before{transform:scaleX(-1)}[dir=ltr] .md-typeset .headerlink{margin-left:.5rem}[dir=rtl] .md-typeset .headerlink{margin-right:.5rem}.md-typeset .headerlink{color:var(--md-default-fg-color--lighter);display:inline-block;opacity:0;transition:color .25s,opacity 125ms}@media print{.md-typeset .headerlink{display:none}}.md-typeset .headerlink:focus,.md-typeset :hover>.headerlink,.md-typeset :target>.headerlink{opacity:1;transition:color .25s,opacity 125ms}.md-typeset .headerlink:focus,.md-typeset .headerlink:hover,.md-typeset :target>.headerlink{color:var(--md-accent-fg-color)}.md-typeset :target{--md-scroll-margin:3.6rem;--md-scroll-offset:0rem;scroll-margin-top:calc(var(--md-scroll-margin) - var(--md-scroll-offset))}@media screen and (min-width:76.25em){.md-header--lifted~.md-container .md-typeset :target{--md-scroll-margin:6rem}}.md-typeset h1:target,.md-typeset h2:target,.md-typeset h3:target{--md-scroll-offset:0.2rem}.md-typeset h4:target{--md-scroll-offset:0.15rem}.doc-contents td code{word-break:normal!important}.doc-md-description,.doc-md-description>p:first-child{display:inline}.md-typeset h5 .doc-object-name{text-transform:none}.doc .md-typeset__table,.doc .md-typeset__table table{display:table!important;width:100%}.doc .md-typeset__table tr{display:table-row}.doc-param-default,.doc-type_param-default{float:right}.doc-heading-parameter,.doc-heading-type_parameter{display:inline}.md-typeset .doc-heading-parameter{font-size:inherit}.doc-heading-parameter .headerlink,.doc-heading-type_parameter .headerlink{margin-left:0!important;margin-right:.2rem}.doc-section-title{font-weight:700}.doc-signature .autorefs{color:inherit;text-decoration-style:dotted}:host,:root,[data-md-color-scheme=default]{--doc-symbol-parameter-fg-color:#829bd1;--doc-symbol-type_parameter-fg-color:#829bd1;--doc-symbol-attribute-fg-color:#953800;--doc-symbol-function-fg-color:#8250df;--doc-symbol-method-fg-color:#8250df;--doc-symbol-class-fg-color:#0550ae;--doc-symbol-type_alias-fg-color:#0550ae;--doc-symbol-module-fg-color:#5cad0f;--doc-symbol-parameter-bg-color:#829bd11a;--doc-symbol-type_parameter-bg-color:#829bd11a;--doc-symbol-attribute-bg-color:#9538001a;--doc-symbol-function-bg-color:#8250df1a;--doc-symbol-method-bg-color:#8250df1a;--doc-symbol-class-bg-color:#0550ae1a;--doc-symbol-type_alias-bg-color:#0550ae1a;--doc-symbol-module-bg-color:#5cad0f1a}[data-md-color-scheme=slate]{--doc-symbol-parameter-fg-color:#829bd1;--doc-symbol-type_parameter-fg-color:#829bd1;--doc-symbol-attribute-fg-color:#ffa657;--doc-symbol-function-fg-color:#d2a8ff;--doc-symbol-method-fg-color:#d2a8ff;--doc-symbol-class-fg-color:#79c0ff;--doc-symbol-type_alias-fg-color:#79c0ff;--doc-symbol-module-fg-color:#baff79;--doc-symbol-parameter-bg-color:#829bd11a;--doc-symbol-type_parameter-bg-color:#829bd11a;--doc-symbol-attribute-bg-color:#ffa6571a;--doc-symbol-function-bg-color:#d2a8ff1a;--doc-symbol-method-bg-color:#d2a8ff1a;--doc-symbol-class-bg-color:#79c0ff1a;--doc-symbol-type_alias-bg-color:#79c0ff1a;--doc-symbol-module-bg-color:#baff791a}code.doc-symbol{border-radius:.1rem;font-size:.85em;font-weight:700;padding:0 .3em}a code.doc-symbol-parameter,code.doc-symbol-parameter{background-color:var(--doc-symbol-parameter-bg-color);color:var(--doc-symbol-parameter-fg-color)}code.doc-symbol-parameter:after{content:"param"}a code.doc-symbol-type_parameter,code.doc-symbol-type_parameter{background-color:var(--doc-symbol-type_parameter-bg-color);color:var(--doc-symbol-type_parameter-fg-color)}code.doc-symbol-type_parameter:after{content:"type-param"}a code.doc-symbol-attribute,code.doc-symbol-attribute{background-color:var(--doc-symbol-attribute-bg-color);color:var(--doc-symbol-attribute-fg-color)}code.doc-symbol-attribute:after{content:"attr"}a code.doc-symbol-function,code.doc-symbol-function{background-color:var(--doc-symbol-function-bg-color);color:var(--doc-symbol-function-fg-color)}code.doc-symbol-function:after{content:"func"}a code.doc-symbol-method,code.doc-symbol-method{background-color:var(--doc-symbol-method-bg-color);color:var(--doc-symbol-method-fg-color)}code.doc-symbol-method:after{content:"meth"}a code.doc-symbol-class,code.doc-symbol-class{background-color:var(--doc-symbol-class-bg-color);color:var(--doc-symbol-class-fg-color)}code.doc-symbol-class:after{content:"class"}a code.doc-symbol-type_alias,code.doc-symbol-type_alias{background-color:var(--doc-symbol-type_alias-bg-color);color:var(--doc-symbol-type_alias-fg-color)}code.doc-symbol-type_alias:after{content:"type"}a code.doc-symbol-module,code.doc-symbol-module{background-color:var(--doc-symbol-module-bg-color);color:var(--doc-symbol-module-fg-color)}code.doc-symbol-module:after{content:"mod"}:root{--md-admonition-icon--mkdocstrings-source:url('data:image/svg+xml;charset=utf-8,') }.md-typeset .admonition.mkdocstrings-source,.md-typeset details.mkdocstrings-source{border:none;padding:0}.md-typeset .admonition.mkdocstrings-source:focus-within,.md-typeset details.mkdocstrings-source:focus-within{box-shadow:none}.md-typeset .mkdocstrings-source>.admonition-title,.md-typeset .mkdocstrings-source>summary{background-color:inherit}.md-typeset .mkdocstrings-source>.admonition-title:before,.md-typeset .mkdocstrings-source>summary:before{background-color:var(--md-default-fg-color);-webkit-mask-image:var(--md-admonition-icon--mkdocstrings-source);mask-image:var(--md-admonition-icon--mkdocstrings-source)}.md-typeset div.arithmatex{overflow:auto}@media screen and (max-width:44.984375em){.md-typeset div.arithmatex{margin:0 -.8rem}.md-typeset div.arithmatex>*{width:min-content}}.md-typeset div.arithmatex>*{margin-left:auto!important;margin-right:auto!important;padding:0 .8rem;touch-action:auto}.md-typeset div.arithmatex>* mjx-container{margin:0!important}.md-typeset div.arithmatex mjx-assistive-mml{height:0}.md-typeset del.critic{background-color:var(--md-typeset-del-color)}.md-typeset del.critic,.md-typeset ins.critic{-webkit-box-decoration-break:clone;box-decoration-break:clone}.md-typeset ins.critic{background-color:var(--md-typeset-ins-color)}.md-typeset .critic.comment{-webkit-box-decoration-break:clone;box-decoration-break:clone;color:var(--md-code-hl-comment-color)}.md-typeset .critic.comment:before{content:"/* "}.md-typeset .critic.comment:after{content:" */"}.md-typeset .critic.block{box-shadow:none;display:block;margin:1em 0;overflow:auto;padding-left:.8rem;padding-right:.8rem}.md-typeset .critic.block>:first-child{margin-top:.5em}.md-typeset .critic.block>:last-child{margin-bottom:.5em}:root{--md-details-icon:url('data:image/svg+xml;charset=utf-8,')}.md-typeset details{display:flow-root;overflow:visible;padding-top:0}.md-typeset details[open]>summary:after{transform:rotate(90deg)}.md-typeset details:not([open]){box-shadow:none;padding-bottom:0}.md-typeset details:not([open])>summary{border-radius:.1rem}[dir=ltr] .md-typeset summary{padding-right:1.8rem}[dir=rtl] .md-typeset summary{padding-left:1.8rem}[dir=ltr] .md-typeset summary{border-top-left-radius:.1rem}[dir=ltr] .md-typeset summary,[dir=rtl] .md-typeset summary{border-top-right-radius:.1rem}[dir=rtl] .md-typeset summary{border-top-left-radius:.1rem}.md-typeset summary{cursor:pointer;display:block;min-height:1rem;overflow:hidden}.md-typeset summary.focus-visible{outline-color:var(--md-accent-fg-color);outline-offset:.2rem}.md-typeset summary:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}[dir=ltr] .md-typeset summary:after{right:.4rem}[dir=rtl] .md-typeset summary:after{left:.4rem}.md-typeset summary:after{background-color:currentcolor;content:"";height:1rem;-webkit-mask-image:var(--md-details-icon);mask-image:var(--md-details-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.625em;transform:rotate(0deg);transition:transform .25s;width:1rem}[dir=rtl] .md-typeset summary:after{transform:rotate(180deg)}.md-typeset summary::marker{display:none}.md-typeset summary::-webkit-details-marker{display:none}.md-typeset .emojione,.md-typeset .gemoji,.md-typeset .twemoji{--md-icon-size:1.125em;display:inline-flex;height:var(--md-icon-size);vertical-align:text-top}.md-typeset .emojione svg,.md-typeset .gemoji svg,.md-typeset .twemoji svg{fill:currentcolor;max-height:100%;width:var(--md-icon-size)}.md-typeset .emojione svg.lucide,.md-typeset .gemoji svg.lucide,.md-typeset .twemoji svg.lucide{fill:#0000;stroke:currentcolor}.md-typeset .lg,.md-typeset .xl,.md-typeset .xxl,.md-typeset .xxxl{vertical-align:text-bottom}.md-typeset .middle{vertical-align:middle}.md-typeset .lg{--md-icon-size:1.5em}.md-typeset .xl{--md-icon-size:2.25em}.md-typeset .xxl{--md-icon-size:3em}.md-typeset .xxxl{--md-icon-size:4em}.highlight .o,.highlight .ow{color:var(--md-code-hl-operator-color)}.highlight .p{color:var(--md-code-hl-punctuation-color)}.highlight .cpf,.highlight .l,.highlight .s,.highlight .s1,.highlight .s2,.highlight .sb,.highlight .sc,.highlight .si,.highlight .ss{color:var(--md-code-hl-string-color)}.highlight .cp,.highlight .se,.highlight .sh,.highlight .sr,.highlight .sx{color:var(--md-code-hl-special-color)}.highlight .il,.highlight .m,.highlight .mb,.highlight .mf,.highlight .mh,.highlight .mi,.highlight .mo{color:var(--md-code-hl-number-color)}.highlight .k,.highlight .kd,.highlight .kn,.highlight .kp,.highlight .kr,.highlight .kt{color:var(--md-code-hl-keyword-color)}.highlight .kc,.highlight .n{color:var(--md-code-hl-name-color)}.highlight .bp,.highlight .nb,.highlight .no{color:var(--md-code-hl-constant-color)}.highlight .nc,.highlight .ne,.highlight .nf,.highlight .nn{color:var(--md-code-hl-function-color)}.highlight .nd,.highlight .ni,.highlight .nl,.highlight .nt{color:var(--md-code-hl-keyword-color)}.highlight .c,.highlight .c1,.highlight .ch,.highlight .cm,.highlight .cs,.highlight .sd{color:var(--md-code-hl-comment-color)}.highlight .na,.highlight .nv,.highlight .vc,.highlight .vg,.highlight .vi{color:var(--md-code-hl-variable-color)}.highlight .ge,.highlight .gh,.highlight .go,.highlight .gp,.highlight .gr,.highlight .gs,.highlight .gt,.highlight .gu{color:var(--md-code-hl-generic-color)}.highlight .gd,.highlight .gi{border-radius:.1rem;margin:0 -.125em;padding:0 .125em}.highlight .gd{background-color:var(--md-typeset-del-color)}.highlight .gi{background-color:var(--md-typeset-ins-color)}.highlight .hll{background-color:var(--md-code-hl-color--light);box-shadow:2px 0 0 0 var(--md-code-hl-color) inset;display:block;margin:0 -1.1764705882em;padding:0 1.1764705882em}.highlight span.filename{background-color:var(--md-code-bg-color);border-bottom:.05rem solid var(--md-default-fg-color--lightest);border-top-left-radius:.1rem;border-top-right-radius:.1rem;display:flow-root;font-size:.85em;font-weight:700;margin-top:1em;padding:.6617647059em 1.1764705882em;position:relative}.highlight span.filename+pre{margin-top:0}.highlight span.filename+pre>code{border-top-left-radius:0;border-top-right-radius:0}.highlight [data-linenos]:before{background-color:var(--md-code-bg-color);box-shadow:-.05rem 0 var(--md-default-fg-color--lightest) inset;color:var(--md-default-fg-color--light);content:attr(data-linenos);float:left;left:-1.1764705882em;margin-left:-1.1764705882em;margin-right:1.1764705882em;padding-left:1.1764705882em;position:sticky;-webkit-user-select:none;user-select:none;z-index:3}.highlight code>span[id^=__span]>:last-child .md-annotation{margin-right:2.4rem}.highlight code[data-md-copying]{display:initial}.highlight code[data-md-copying] .hll{display:contents}.highlight code[data-md-copying] .md-annotation{display:none}.highlighttable{display:flow-root}.highlighttable tbody,.highlighttable td{display:block;padding:0}.highlighttable tr{display:flex}.highlighttable pre{margin:0}.highlighttable th.filename{flex-grow:1;padding:0;text-align:left}.highlighttable th.filename span.filename{margin-top:0}.highlighttable .linenos{background-color:var(--md-code-bg-color);border-bottom-left-radius:.1rem;border-top-left-radius:.1rem;font-size:.85em;padding:.7720588235em 0 .7720588235em 1.1764705882em;-webkit-user-select:none;user-select:none}.highlighttable .linenodiv{box-shadow:-.05rem 0 var(--md-default-fg-color--lightest) inset}.highlighttable .linenodiv pre{color:var(--md-default-fg-color--light);text-align:right}.highlighttable .linenodiv span[class]{padding-right:.5882352941em}.highlighttable .code{flex:1;min-width:0}.linenodiv a{color:inherit}.md-typeset .highlighttable{direction:ltr;margin:1em 0}.md-typeset .highlighttable>tbody>tr>.code>div>pre>code{border-bottom-left-radius:0;border-top-left-radius:0}.md-typeset .highlight+.result{border:.05rem solid var(--md-code-bg-color);border-bottom-left-radius:.1rem;border-bottom-right-radius:.1rem;border-top-width:.1rem;margin-top:-1.125em;overflow:visible;padding:0 1em}.md-typeset .highlight+.result:after{clear:both;content:"";display:block}@media screen and (max-width:44.984375em){.md-content__inner>.highlight{margin:1em -.8rem}.md-content__inner>.highlight>.filename,.md-content__inner>.highlight>.highlighttable>tbody>tr>.code>div>pre>code,.md-content__inner>.highlight>.highlighttable>tbody>tr>.filename span.filename,.md-content__inner>.highlight>.highlighttable>tbody>tr>.linenos,.md-content__inner>.highlight>pre>code{border-radius:0}.md-content__inner>.highlight+.result{border-left-width:0;border-radius:0;border-right-width:0;margin-left:-.8rem;margin-right:-.8rem}}.md-typeset .keys kbd:after,.md-typeset .keys kbd:before{-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;color:inherit;margin:0;position:relative}.md-typeset .keys span{color:var(--md-default-fg-color--light);padding:0 .2em}.md-typeset .keys .key-alt:before,.md-typeset .keys .key-left-alt:before,.md-typeset .keys .key-right-alt:before{content:"⎇";padding-right:.4em}.md-typeset .keys .key-command:before,.md-typeset .keys .key-left-command:before,.md-typeset .keys .key-right-command:before{content:"⌘";padding-right:.4em}.md-typeset .keys .key-control:before,.md-typeset .keys .key-left-control:before,.md-typeset .keys .key-right-control:before{content:"⌃";padding-right:.4em}.md-typeset .keys .key-left-meta:before,.md-typeset .keys .key-meta:before,.md-typeset .keys .key-right-meta:before{content:"◆";padding-right:.4em}.md-typeset .keys .key-left-option:before,.md-typeset .keys .key-option:before,.md-typeset .keys .key-right-option:before{content:"⌥";padding-right:.4em}.md-typeset .keys .key-left-shift:before,.md-typeset .keys .key-right-shift:before,.md-typeset .keys .key-shift:before{content:"⇧";padding-right:.4em}.md-typeset .keys .key-left-super:before,.md-typeset .keys .key-right-super:before,.md-typeset .keys .key-super:before{content:"❖";padding-right:.4em}.md-typeset .keys .key-left-windows:before,.md-typeset .keys .key-right-windows:before,.md-typeset .keys .key-windows:before{content:"⊞";padding-right:.4em}.md-typeset .keys .key-arrow-down:before{content:"↓";padding-right:.4em}.md-typeset .keys .key-arrow-left:before{content:"←";padding-right:.4em}.md-typeset .keys .key-arrow-right:before{content:"→";padding-right:.4em}.md-typeset .keys .key-arrow-up:before{content:"↑";padding-right:.4em}.md-typeset .keys .key-backspace:before{content:"⌫";padding-right:.4em}.md-typeset .keys .key-backtab:before{content:"⇤";padding-right:.4em}.md-typeset .keys .key-caps-lock:before{content:"⇪";padding-right:.4em}.md-typeset .keys .key-clear:before{content:"⌧";padding-right:.4em}.md-typeset .keys .key-context-menu:before{content:"☰";padding-right:.4em}.md-typeset .keys .key-delete:before{content:"⌦";padding-right:.4em}.md-typeset .keys .key-eject:before{content:"⏏";padding-right:.4em}.md-typeset .keys .key-end:before{content:"⤓";padding-right:.4em}.md-typeset .keys .key-escape:before{content:"⎋";padding-right:.4em}.md-typeset .keys .key-home:before{content:"⤒";padding-right:.4em}.md-typeset .keys .key-insert:before{content:"⎀";padding-right:.4em}.md-typeset .keys .key-page-down:before{content:"⇟";padding-right:.4em}.md-typeset .keys .key-page-up:before{content:"⇞";padding-right:.4em}.md-typeset .keys .key-print-screen:before{content:"⎙";padding-right:.4em}.md-typeset .keys .key-tab:after{content:"⇥";padding-left:.4em}.md-typeset .keys .key-num-enter:after{content:"⌤";padding-left:.4em}.md-typeset .keys .key-enter:after{content:"⏎";padding-left:.4em}:root{--md-tabbed-icon--prev:url('data:image/svg+xml;charset=utf-8,');--md-tabbed-icon--next:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .tabbed-set{border-radius:.1rem;display:flex;flex-flow:column wrap;margin:1em 0;position:relative}.md-typeset .tabbed-set>input{height:0;opacity:0;position:absolute;width:0}.md-typeset .tabbed-set>input:target{--md-scroll-offset:0.625em}.md-typeset .tabbed-set>input.focus-visible~.tabbed-labels:before{background-color:var(--md-accent-fg-color)}.md-typeset .tabbed-labels{-ms-overflow-style:none;box-shadow:0 -.05rem var(--md-default-fg-color--lightest) inset;display:flex;max-width:100%;overflow:auto;scrollbar-width:none}@media print{.md-typeset .tabbed-labels{display:contents}}@media screen{.js .md-typeset .tabbed-labels{position:relative}.js .md-typeset .tabbed-labels:before{background:var(--md-default-fg-color);bottom:0;content:"";display:block;height:2px;left:0;position:absolute;transform:translateX(var(--md-indicator-x));transition:width 225ms,background-color .25s,transform .25s;transition-timing-function:cubic-bezier(.4,0,.2,1);width:var(--md-indicator-width)}}.md-typeset .tabbed-labels::-webkit-scrollbar{display:none}.md-typeset .tabbed-labels>label{border-bottom:.1rem solid #0000;border-radius:.1rem .1rem 0 0;color:var(--md-default-fg-color--light);cursor:pointer;flex-shrink:0;font-size:.64rem;font-weight:700;padding:.78125em 1.25em .625em;scroll-margin-inline-start:1rem;transition:background-color .25s,color .25s;white-space:nowrap;width:auto}@media print{.md-typeset .tabbed-labels>label:first-child{order:1}.md-typeset .tabbed-labels>label:nth-child(2){order:2}.md-typeset .tabbed-labels>label:nth-child(3){order:3}.md-typeset .tabbed-labels>label:nth-child(4){order:4}.md-typeset .tabbed-labels>label:nth-child(5){order:5}.md-typeset .tabbed-labels>label:nth-child(6){order:6}.md-typeset .tabbed-labels>label:nth-child(7){order:7}.md-typeset .tabbed-labels>label:nth-child(8){order:8}.md-typeset .tabbed-labels>label:nth-child(9){order:9}.md-typeset .tabbed-labels>label:nth-child(10){order:10}.md-typeset .tabbed-labels>label:nth-child(11){order:11}.md-typeset .tabbed-labels>label:nth-child(12){order:12}.md-typeset .tabbed-labels>label:nth-child(13){order:13}.md-typeset .tabbed-labels>label:nth-child(14){order:14}.md-typeset .tabbed-labels>label:nth-child(15){order:15}.md-typeset .tabbed-labels>label:nth-child(16){order:16}.md-typeset .tabbed-labels>label:nth-child(17){order:17}.md-typeset .tabbed-labels>label:nth-child(18){order:18}.md-typeset .tabbed-labels>label:nth-child(19){order:19}.md-typeset .tabbed-labels>label:nth-child(20){order:20}}.md-typeset .tabbed-labels>label:hover{color:var(--md-default-fg-color)}.md-typeset .tabbed-labels>label>[href]:first-child{color:inherit}.md-typeset .tabbed-labels--linked>label{padding:0}.md-typeset .tabbed-labels--linked>label>a{display:block;padding:.78125em 1.25em .625em}.md-typeset .tabbed-content{width:100%}@media print{.md-typeset .tabbed-content{display:contents}}.md-typeset .tabbed-block{display:none}@media print{.md-typeset .tabbed-block{display:block}.md-typeset .tabbed-block:first-child{order:1}.md-typeset .tabbed-block:nth-child(2){order:2}.md-typeset .tabbed-block:nth-child(3){order:3}.md-typeset .tabbed-block:nth-child(4){order:4}.md-typeset .tabbed-block:nth-child(5){order:5}.md-typeset .tabbed-block:nth-child(6){order:6}.md-typeset .tabbed-block:nth-child(7){order:7}.md-typeset .tabbed-block:nth-child(8){order:8}.md-typeset .tabbed-block:nth-child(9){order:9}.md-typeset .tabbed-block:nth-child(10){order:10}.md-typeset .tabbed-block:nth-child(11){order:11}.md-typeset .tabbed-block:nth-child(12){order:12}.md-typeset .tabbed-block:nth-child(13){order:13}.md-typeset .tabbed-block:nth-child(14){order:14}.md-typeset .tabbed-block:nth-child(15){order:15}.md-typeset .tabbed-block:nth-child(16){order:16}.md-typeset .tabbed-block:nth-child(17){order:17}.md-typeset .tabbed-block:nth-child(18){order:18}.md-typeset .tabbed-block:nth-child(19){order:19}.md-typeset .tabbed-block:nth-child(20){order:20}}.md-typeset .tabbed-block>.highlight:first-child>pre,.md-typeset .tabbed-block>pre:first-child{margin:0}.md-typeset .tabbed-block>.highlight:first-child>pre>code,.md-typeset .tabbed-block>pre:first-child>code{border-top-left-radius:0;border-top-right-radius:0}.md-typeset .tabbed-block>.highlight:first-child>.filename{border-top-left-radius:0;border-top-right-radius:0;margin:0}.md-typeset .tabbed-block>.highlight:first-child>.highlighttable{margin:0}.md-typeset .tabbed-block>.highlight:first-child>.highlighttable>tbody>tr>.filename span.filename,.md-typeset .tabbed-block>.highlight:first-child>.highlighttable>tbody>tr>.linenos{border-top-left-radius:0;border-top-right-radius:0;margin:0}.md-typeset .tabbed-block>.highlight:first-child>.highlighttable>tbody>tr>.code>div>pre>code{border-top-left-radius:0;border-top-right-radius:0}.md-typeset .tabbed-block>.highlight:first-child+.result{margin-top:-.125em}.md-typeset .tabbed-block>.tabbed-set{margin:0}.md-typeset .tabbed-button{align-self:center;border-radius:100%;color:var(--md-default-fg-color--light);cursor:pointer;display:block;height:.9rem;margin-top:.1rem;pointer-events:auto;transition:background-color .25s;width:.9rem}.md-typeset .tabbed-button:hover{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-typeset .tabbed-button:after{background-color:currentcolor;content:"";display:block;height:100%;-webkit-mask-image:var(--md-tabbed-icon--prev);mask-image:var(--md-tabbed-icon--prev);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:background-color .25s,transform .25s;width:100%}.md-typeset .tabbed-control{background:linear-gradient(to right,var(--md-default-bg-color) 60%,#0000);display:flex;height:1.9rem;justify-content:start;pointer-events:none;position:absolute;transition:opacity 125ms;width:1.2rem}[dir=rtl] .md-typeset .tabbed-control{transform:rotate(180deg)}.md-typeset .tabbed-control[hidden]{opacity:0}.md-typeset .tabbed-control--next{background:linear-gradient(to left,var(--md-default-bg-color) 60%,#0000);justify-content:end;right:0}.md-typeset .tabbed-control--next .tabbed-button:after{-webkit-mask-image:var(--md-tabbed-icon--next);mask-image:var(--md-tabbed-icon--next)}@media screen and (max-width:44.984375em){[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels{padding-left:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels{padding-right:.8rem}.md-content__inner>.tabbed-set .tabbed-labels{margin:0 -.8rem;max-width:100vw;scroll-padding-inline-start:.8rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels:after{padding-right:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels:after{padding-left:.8rem}.md-content__inner>.tabbed-set .tabbed-labels:after{content:""}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{padding-left:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{padding-right:.8rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{margin-left:-.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{margin-right:-.8rem}.md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{width:2rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{padding-right:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{padding-left:.8rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{margin-right:-.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{margin-left:-.8rem}.md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{width:2rem}}@media screen{.md-typeset .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,.md-typeset .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),.md-typeset .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),.md-typeset .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),.md-typeset .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),.md-typeset .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),.md-typeset .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),.md-typeset .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),.md-typeset .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),.md-typeset .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),.md-typeset .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),.md-typeset .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),.md-typeset .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),.md-typeset .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),.md-typeset .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),.md-typeset .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),.md-typeset .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),.md-typeset .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),.md-typeset .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),.md-typeset .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9){color:var(--md-default-fg-color)}.md-typeset .no-js .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,.md-typeset .no-js .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),.md-typeset .no-js .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),.md-typeset .no-js .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),.md-typeset .no-js .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),.md-typeset .no-js .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),.md-typeset .no-js .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),.md-typeset .no-js .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),.md-typeset .no-js .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),.md-typeset .no-js .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),.md-typeset .no-js .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),.md-typeset .no-js .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),.md-typeset .no-js .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),.md-typeset .no-js .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),.md-typeset .no-js .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),.md-typeset .no-js .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),.md-typeset .no-js .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),.md-typeset .no-js .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),.md-typeset .no-js .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),.md-typeset .no-js .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9),.md-typeset [role=dialog] .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,.md-typeset [role=dialog] .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),.md-typeset [role=dialog] .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),.md-typeset [role=dialog] .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),.md-typeset [role=dialog] .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),.md-typeset [role=dialog] .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),.md-typeset [role=dialog] .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),.md-typeset [role=dialog] .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),.md-typeset [role=dialog] .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),.md-typeset [role=dialog] .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),.md-typeset [role=dialog] .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),.md-typeset [role=dialog] .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),.md-typeset [role=dialog] .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),.md-typeset [role=dialog] .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),.md-typeset [role=dialog] .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),.md-typeset [role=dialog] .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),.md-typeset [role=dialog] .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),.md-typeset [role=dialog] .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),.md-typeset [role=dialog] .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),.md-typeset [role=dialog] .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9),.no-js .md-typeset .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,.no-js .md-typeset .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),.no-js .md-typeset .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),.no-js .md-typeset .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),.no-js .md-typeset .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),.no-js .md-typeset .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),.no-js .md-typeset .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),.no-js .md-typeset .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),.no-js .md-typeset .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),.no-js .md-typeset .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),.no-js .md-typeset .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),.no-js .md-typeset .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),.no-js .md-typeset .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),.no-js .md-typeset .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),.no-js .md-typeset .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),.no-js .md-typeset .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),.no-js .md-typeset .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),.no-js .md-typeset .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),.no-js .md-typeset .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),.no-js .md-typeset .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9),[role=dialog] .md-typeset .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,[role=dialog] .md-typeset .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),[role=dialog] .md-typeset .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),[role=dialog] .md-typeset .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),[role=dialog] .md-typeset .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),[role=dialog] .md-typeset .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),[role=dialog] .md-typeset .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),[role=dialog] .md-typeset .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),[role=dialog] .md-typeset .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),[role=dialog] .md-typeset .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),[role=dialog] .md-typeset .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),[role=dialog] .md-typeset .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),[role=dialog] .md-typeset .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),[role=dialog] .md-typeset .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),[role=dialog] .md-typeset .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),[role=dialog] .md-typeset .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),[role=dialog] .md-typeset .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),[role=dialog] .md-typeset .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),[role=dialog] .md-typeset .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),[role=dialog] .md-typeset .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9){border-color:var(--md-default-fg-color)}}.md-typeset .tabbed-set>input:first-child.focus-visible~.tabbed-labels>:first-child,.md-typeset .tabbed-set>input:nth-child(10).focus-visible~.tabbed-labels>:nth-child(10),.md-typeset .tabbed-set>input:nth-child(11).focus-visible~.tabbed-labels>:nth-child(11),.md-typeset .tabbed-set>input:nth-child(12).focus-visible~.tabbed-labels>:nth-child(12),.md-typeset .tabbed-set>input:nth-child(13).focus-visible~.tabbed-labels>:nth-child(13),.md-typeset .tabbed-set>input:nth-child(14).focus-visible~.tabbed-labels>:nth-child(14),.md-typeset .tabbed-set>input:nth-child(15).focus-visible~.tabbed-labels>:nth-child(15),.md-typeset .tabbed-set>input:nth-child(16).focus-visible~.tabbed-labels>:nth-child(16),.md-typeset .tabbed-set>input:nth-child(17).focus-visible~.tabbed-labels>:nth-child(17),.md-typeset .tabbed-set>input:nth-child(18).focus-visible~.tabbed-labels>:nth-child(18),.md-typeset .tabbed-set>input:nth-child(19).focus-visible~.tabbed-labels>:nth-child(19),.md-typeset .tabbed-set>input:nth-child(2).focus-visible~.tabbed-labels>:nth-child(2),.md-typeset .tabbed-set>input:nth-child(20).focus-visible~.tabbed-labels>:nth-child(20),.md-typeset .tabbed-set>input:nth-child(3).focus-visible~.tabbed-labels>:nth-child(3),.md-typeset .tabbed-set>input:nth-child(4).focus-visible~.tabbed-labels>:nth-child(4),.md-typeset .tabbed-set>input:nth-child(5).focus-visible~.tabbed-labels>:nth-child(5),.md-typeset .tabbed-set>input:nth-child(6).focus-visible~.tabbed-labels>:nth-child(6),.md-typeset .tabbed-set>input:nth-child(7).focus-visible~.tabbed-labels>:nth-child(7),.md-typeset .tabbed-set>input:nth-child(8).focus-visible~.tabbed-labels>:nth-child(8),.md-typeset .tabbed-set>input:nth-child(9).focus-visible~.tabbed-labels>:nth-child(9){color:var(--md-accent-fg-color)}.md-typeset .tabbed-set>input:first-child:checked~.tabbed-content>:first-child,.md-typeset .tabbed-set>input:nth-child(10):checked~.tabbed-content>:nth-child(10),.md-typeset .tabbed-set>input:nth-child(11):checked~.tabbed-content>:nth-child(11),.md-typeset .tabbed-set>input:nth-child(12):checked~.tabbed-content>:nth-child(12),.md-typeset .tabbed-set>input:nth-child(13):checked~.tabbed-content>:nth-child(13),.md-typeset .tabbed-set>input:nth-child(14):checked~.tabbed-content>:nth-child(14),.md-typeset .tabbed-set>input:nth-child(15):checked~.tabbed-content>:nth-child(15),.md-typeset .tabbed-set>input:nth-child(16):checked~.tabbed-content>:nth-child(16),.md-typeset .tabbed-set>input:nth-child(17):checked~.tabbed-content>:nth-child(17),.md-typeset .tabbed-set>input:nth-child(18):checked~.tabbed-content>:nth-child(18),.md-typeset .tabbed-set>input:nth-child(19):checked~.tabbed-content>:nth-child(19),.md-typeset .tabbed-set>input:nth-child(2):checked~.tabbed-content>:nth-child(2),.md-typeset .tabbed-set>input:nth-child(20):checked~.tabbed-content>:nth-child(20),.md-typeset .tabbed-set>input:nth-child(3):checked~.tabbed-content>:nth-child(3),.md-typeset .tabbed-set>input:nth-child(4):checked~.tabbed-content>:nth-child(4),.md-typeset .tabbed-set>input:nth-child(5):checked~.tabbed-content>:nth-child(5),.md-typeset .tabbed-set>input:nth-child(6):checked~.tabbed-content>:nth-child(6),.md-typeset .tabbed-set>input:nth-child(7):checked~.tabbed-content>:nth-child(7),.md-typeset .tabbed-set>input:nth-child(8):checked~.tabbed-content>:nth-child(8),.md-typeset .tabbed-set>input:nth-child(9):checked~.tabbed-content>:nth-child(9){display:block}:root{--md-tasklist-icon:url('data:image/svg+xml;charset=utf-8,');--md-tasklist-icon--checked:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .task-list-item{list-style-type:none;position:relative}[dir=ltr] .md-typeset .task-list-item [type=checkbox]{left:-2em}[dir=rtl] .md-typeset .task-list-item [type=checkbox]{right:-2em}.md-typeset .task-list-item [type=checkbox]{position:absolute;top:.45em}.md-typeset .task-list-control [type=checkbox]{opacity:0;z-index:-1}[dir=ltr] .md-typeset .task-list-indicator:before{left:-1.5em}[dir=rtl] .md-typeset .task-list-indicator:before{right:-1.5em}.md-typeset .task-list-indicator:before{background-color:var(--md-default-fg-color--lightest);content:"";height:1.25em;-webkit-mask-image:var(--md-tasklist-icon);mask-image:var(--md-tasklist-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.15em;width:1.25em}.md-typeset [type=checkbox]:checked+.task-list-indicator:before{background-color:#00e676;-webkit-mask-image:var(--md-tasklist-icon--checked);mask-image:var(--md-tasklist-icon--checked)}@media print{.giscus,[id=__comments]{display:none}}:root>*{--md-mermaid-font-family:var(--md-text-font-family),sans-serif;--md-mermaid-edge-color:var(--md-code-fg-color);--md-mermaid-node-bg-color:var(--md-accent-fg-color--transparent);--md-mermaid-node-fg-color:var(--md-accent-fg-color);--md-mermaid-label-bg-color:var(--md-default-bg-color);--md-mermaid-label-fg-color:var(--md-code-fg-color);--md-mermaid-sequence-actor-bg-color:var(--md-mermaid-label-bg-color);--md-mermaid-sequence-actor-fg-color:var(--md-mermaid-label-fg-color);--md-mermaid-sequence-actor-border-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-actor-line-color:var(--md-default-fg-color--lighter);--md-mermaid-sequence-actorman-bg-color:var(--md-mermaid-label-bg-color);--md-mermaid-sequence-actorman-line-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-box-bg-color:var(--md-mermaid-node-bg-color);--md-mermaid-sequence-box-fg-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-label-bg-color:var(--md-mermaid-node-bg-color);--md-mermaid-sequence-label-fg-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-loop-bg-color:var(--md-mermaid-node-bg-color);--md-mermaid-sequence-loop-fg-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-loop-border-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-message-fg-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-message-line-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-note-bg-color:var(--md-mermaid-label-bg-color);--md-mermaid-sequence-note-fg-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-note-border-color:var(--md-mermaid-label-fg-color);--md-mermaid-sequence-number-bg-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-number-fg-color:var(--md-accent-bg-color)}.mermaid{line-height:normal;margin:1em 0}.md-typeset .grid{grid-gap:.4rem;display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,16rem),1fr));margin:1em 0}.md-typeset .grid.cards>ol,.md-typeset .grid.cards>ul{display:contents}.md-typeset .grid.cards>ol>li,.md-typeset .grid.cards>ul>li,.md-typeset .grid>.card{border:.05rem solid var(--md-default-fg-color--lightest);border-radius:.1rem;display:block;margin:0;padding:.8rem;transition:border .25s,box-shadow .25s}.md-typeset .grid.cards>ol>li:focus-within,.md-typeset .grid.cards>ol>li:hover,.md-typeset .grid.cards>ul>li:focus-within,.md-typeset .grid.cards>ul>li:hover,.md-typeset .grid>.card:focus-within,.md-typeset .grid>.card:hover{border-color:#0000;box-shadow:var(--md-shadow-z2)}.md-typeset .grid.cards>ol>li>hr,.md-typeset .grid.cards>ul>li>hr,.md-typeset .grid>.card>hr{margin-bottom:1em;margin-top:1em}.md-typeset .grid.cards>ol>li>:first-child,.md-typeset .grid.cards>ul>li>:first-child,.md-typeset .grid>.card>:first-child{margin-top:0}.md-typeset .grid.cards>ol>li>:last-child,.md-typeset .grid.cards>ul>li>:last-child,.md-typeset .grid>.card>:last-child{margin-bottom:0}.md-typeset .grid>*,.md-typeset .grid>.admonition,.md-typeset .grid>.highlight>*,.md-typeset .grid>.highlighttable,.md-typeset .grid>.md-typeset details,.md-typeset .grid>details,.md-typeset .grid>pre{margin-bottom:0;margin-top:0}.md-typeset .grid>.highlight>pre:only-child,.md-typeset .grid>.highlight>pre>code,.md-typeset .grid>.highlighttable,.md-typeset .grid>.highlighttable>tbody,.md-typeset .grid>.highlighttable>tbody>tr,.md-typeset .grid>.highlighttable>tbody>tr>.code,.md-typeset .grid>.highlighttable>tbody>tr>.code>.highlight,.md-typeset .grid>.highlighttable>tbody>tr>.code>.highlight>pre,.md-typeset .grid>.highlighttable>tbody>tr>.code>.highlight>pre>code{height:100%}.md-typeset .grid>.tabbed-set{margin-bottom:0;margin-top:0}@media screen and (min-width:45em){[dir=ltr] .md-typeset .inline{float:left}[dir=rtl] .md-typeset .inline{float:right}[dir=ltr] .md-typeset .inline{margin-right:.8rem}[dir=rtl] .md-typeset .inline{margin-left:.8rem}.md-typeset .inline{margin-bottom:.8rem;margin-top:0;width:11.7rem}[dir=ltr] .md-typeset .inline.end{float:right}[dir=rtl] .md-typeset .inline.end{float:left}[dir=ltr] .md-typeset .inline.end{margin-left:.8rem;margin-right:0}[dir=rtl] .md-typeset .inline.end{margin-left:0;margin-right:.8rem}} \ No newline at end of file diff --git a/docs/site/assets/stylesheets/classic/palette.7dc9a0ad.min.css b/docs/site/assets/stylesheets/classic/palette.7dc9a0ad.min.css deleted file mode 100644 index 2d83819..0000000 --- a/docs/site/assets/stylesheets/classic/palette.7dc9a0ad.min.css +++ /dev/null @@ -1 +0,0 @@ -@media screen{[data-md-color-scheme=slate]{--md-default-fg-color:hsla(var(--md-hue),15%,90%,0.82);--md-default-fg-color--light:hsla(var(--md-hue),15%,90%,0.56);--md-default-fg-color--lighter:hsla(var(--md-hue),15%,90%,0.32);--md-default-fg-color--lightest:hsla(var(--md-hue),15%,90%,0.12);--md-default-bg-color:hsla(var(--md-hue),15%,14%,1);--md-default-bg-color--light:hsla(var(--md-hue),15%,14%,0.54);--md-default-bg-color--lighter:hsla(var(--md-hue),15%,14%,0.26);--md-default-bg-color--lightest:hsla(var(--md-hue),15%,14%,0.07);--md-code-fg-color:hsla(var(--md-hue),18%,86%,0.82);--md-code-bg-color:hsla(var(--md-hue),15%,18%,1);--md-code-bg-color--light:hsla(var(--md-hue),15%,18%,0.9);--md-code-bg-color--lighter:hsla(var(--md-hue),15%,18%,0.54);--md-code-hl-color:#2977ff;--md-code-hl-color--light:#2977ff1a;--md-code-hl-number-color:#e6695b;--md-code-hl-special-color:#f06090;--md-code-hl-function-color:#c973d9;--md-code-hl-constant-color:#9383e2;--md-code-hl-keyword-color:#6791e0;--md-code-hl-string-color:#2fb170;--md-code-hl-name-color:var(--md-code-fg-color);--md-code-hl-operator-color:var(--md-default-fg-color--light);--md-code-hl-punctuation-color:var(--md-default-fg-color--light);--md-code-hl-comment-color:var(--md-default-fg-color--light);--md-code-hl-generic-color:var(--md-default-fg-color--light);--md-code-hl-variable-color:var(--md-default-fg-color--light);--md-typeset-color:var(--md-default-fg-color);--md-typeset-a-color:var(--md-primary-fg-color);--md-typeset-kbd-color:hsla(var(--md-hue),15%,90%,0.12);--md-typeset-kbd-accent-color:hsla(var(--md-hue),15%,90%,0.2);--md-typeset-kbd-border-color:hsla(var(--md-hue),15%,14%,1);--md-typeset-mark-color:#4287ff4d;--md-typeset-table-color:hsla(var(--md-hue),15%,95%,0.12);--md-typeset-table-color--light:hsla(var(--md-hue),15%,95%,0.035);--md-admonition-fg-color:var(--md-default-fg-color);--md-admonition-bg-color:var(--md-default-bg-color);--md-footer-bg-color:hsla(var(--md-hue),15%,10%,0.87);--md-footer-bg-color--dark:hsla(var(--md-hue),15%,8%,1);--md-shadow-z1:0 0.2rem 0.5rem #0000000d,0 0 0.05rem #0000001a;--md-shadow-z2:0 0.2rem 0.5rem #00000040,0 0 0.05rem #00000040;--md-shadow-z3:0 0.2rem 0.5rem #0006,0 0 0.05rem #00000059;color-scheme:dark}[data-md-color-scheme=slate] img[src$="#gh-light-mode-only"],[data-md-color-scheme=slate] img[src$="#only-light"]{display:none}[data-md-color-scheme=slate]{--color-foreground:255 255 255;--color-background:22 23 26;--color-background-subtle:33 34 38;--color-backdrop:11 12 15}[data-md-color-scheme=slate][data-md-color-primary=pink]{--md-typeset-a-color:#ed5487}[data-md-color-scheme=slate][data-md-color-primary=purple]{--md-typeset-a-color:#c46fd3}[data-md-color-scheme=slate][data-md-color-primary=deep-purple]{--md-typeset-a-color:#a47bea}[data-md-color-scheme=slate][data-md-color-primary=indigo]{--md-typeset-a-color:#5488e8}[data-md-color-scheme=slate][data-md-color-primary=teal]{--md-typeset-a-color:#00ccb8}[data-md-color-scheme=slate][data-md-color-primary=green]{--md-typeset-a-color:#71c174}[data-md-color-scheme=slate][data-md-color-primary=deep-orange]{--md-typeset-a-color:#ff764d}[data-md-color-scheme=slate][data-md-color-primary=brown]{--md-typeset-a-color:#c1775c}[data-md-color-scheme=slate][data-md-color-primary=black],[data-md-color-scheme=slate][data-md-color-primary=blue-grey],[data-md-color-scheme=slate][data-md-color-primary=grey],[data-md-color-scheme=slate][data-md-color-primary=white]{--md-typeset-a-color:#5e8bde}[data-md-color-switching] *,[data-md-color-switching] :after,[data-md-color-switching] :before{transition-duration:0ms!important}}[data-md-color-accent=red]{--md-accent-fg-color:#ff1947;--md-accent-fg-color--transparent:#ff19471a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=pink]{--md-accent-fg-color:#f50056;--md-accent-fg-color--transparent:#f500561a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=purple]{--md-accent-fg-color:#df41fb;--md-accent-fg-color--transparent:#df41fb1a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=deep-purple]{--md-accent-fg-color:#7c4dff;--md-accent-fg-color--transparent:#7c4dff1a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=indigo]{--md-accent-fg-color:#526cfe;--md-accent-fg-color--transparent:#526cfe1a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=blue]{--md-accent-fg-color:#4287ff;--md-accent-fg-color--transparent:#4287ff1a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=light-blue]{--md-accent-fg-color:#0091eb;--md-accent-fg-color--transparent:#0091eb1a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=cyan]{--md-accent-fg-color:#00bad6;--md-accent-fg-color--transparent:#00bad61a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=teal]{--md-accent-fg-color:#00bda4;--md-accent-fg-color--transparent:#00bda41a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=green]{--md-accent-fg-color:#00c753;--md-accent-fg-color--transparent:#00c7531a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=light-green]{--md-accent-fg-color:#63de17;--md-accent-fg-color--transparent:#63de171a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=lime]{--md-accent-fg-color:#b0eb00;--md-accent-fg-color--transparent:#b0eb001a;--md-accent-bg-color:#000000de;--md-accent-bg-color--light:#0000008a}[data-md-color-accent=yellow]{--md-accent-fg-color:#ffd500;--md-accent-fg-color--transparent:#ffd5001a;--md-accent-bg-color:#000000de;--md-accent-bg-color--light:#0000008a}[data-md-color-accent=amber]{--md-accent-fg-color:#fa0;--md-accent-fg-color--transparent:#ffaa001a;--md-accent-bg-color:#000000de;--md-accent-bg-color--light:#0000008a}[data-md-color-accent=orange]{--md-accent-fg-color:#ff9100;--md-accent-fg-color--transparent:#ff91001a;--md-accent-bg-color:#000000de;--md-accent-bg-color--light:#0000008a}[data-md-color-accent=deep-orange]{--md-accent-fg-color:#ff6e42;--md-accent-fg-color--transparent:#ff6e421a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-primary=red]{--md-primary-fg-color:#ef5552;--md-primary-fg-color--light:#e57171;--md-primary-fg-color--dark:#e53734;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=pink]{--md-primary-fg-color:#e92063;--md-primary-fg-color--light:#ec417a;--md-primary-fg-color--dark:#c3185d;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=purple]{--md-primary-fg-color:#ab47bd;--md-primary-fg-color--light:#bb69c9;--md-primary-fg-color--dark:#8c24a8;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=deep-purple]{--md-primary-fg-color:#7e56c2;--md-primary-fg-color--light:#9574cd;--md-primary-fg-color--dark:#673ab6;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=indigo]{--md-primary-fg-color:#4051b5;--md-primary-fg-color--light:#5d6cc0;--md-primary-fg-color--dark:#303fa1;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=blue]{--md-primary-fg-color:#2094f3;--md-primary-fg-color--light:#42a5f5;--md-primary-fg-color--dark:#1975d2;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=light-blue]{--md-primary-fg-color:#02a6f2;--md-primary-fg-color--light:#28b5f6;--md-primary-fg-color--dark:#0287cf;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=cyan]{--md-primary-fg-color:#00bdd6;--md-primary-fg-color--light:#25c5da;--md-primary-fg-color--dark:#0097a8;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=teal]{--md-primary-fg-color:#009485;--md-primary-fg-color--light:#26a699;--md-primary-fg-color--dark:#007a6c;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=green]{--md-primary-fg-color:#4cae4f;--md-primary-fg-color--light:#68bb6c;--md-primary-fg-color--dark:#398e3d;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=light-green]{--md-primary-fg-color:#8bc34b;--md-primary-fg-color--light:#9ccc66;--md-primary-fg-color--dark:#689f38;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=lime]{--md-primary-fg-color:#cbdc38;--md-primary-fg-color--light:#d3e156;--md-primary-fg-color--dark:#b0b52c;--md-primary-bg-color:#000000de;--md-primary-bg-color--light:#0000008a}[data-md-color-primary=yellow]{--md-primary-fg-color:#ffec3d;--md-primary-fg-color--light:#ffee57;--md-primary-fg-color--dark:#fbc02d;--md-primary-bg-color:#000000de;--md-primary-bg-color--light:#0000008a}[data-md-color-primary=amber]{--md-primary-fg-color:#ffc105;--md-primary-fg-color--light:#ffc929;--md-primary-fg-color--dark:#ffa200;--md-primary-bg-color:#000000de;--md-primary-bg-color--light:#0000008a}[data-md-color-primary=orange]{--md-primary-fg-color:#ffa724;--md-primary-fg-color--light:#ffa724;--md-primary-fg-color--dark:#fa8900;--md-primary-bg-color:#000000de;--md-primary-bg-color--light:#0000008a}[data-md-color-primary=deep-orange]{--md-primary-fg-color:#ff6e42;--md-primary-fg-color--light:#ff8a66;--md-primary-fg-color--dark:#f4511f;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=brown]{--md-primary-fg-color:#795649;--md-primary-fg-color--light:#8d6e62;--md-primary-fg-color--dark:#5d4037;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=grey]{--md-primary-fg-color:#757575;--md-primary-fg-color--light:#9e9e9e;--md-primary-fg-color--dark:#616161;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3;--md-typeset-a-color:#4051b5}[data-md-color-primary=blue-grey]{--md-primary-fg-color:#546d78;--md-primary-fg-color--light:#607c8a;--md-primary-fg-color--dark:#455a63;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3;--md-typeset-a-color:#4051b5}[data-md-color-primary=light-green]:not([data-md-color-scheme=slate]){--md-typeset-a-color:#72ad2e}[data-md-color-primary=lime]:not([data-md-color-scheme=slate]){--md-typeset-a-color:#8b990a}[data-md-color-primary=yellow]:not([data-md-color-scheme=slate]){--md-typeset-a-color:#b8a500}[data-md-color-primary=amber]:not([data-md-color-scheme=slate]){--md-typeset-a-color:#d19d00}[data-md-color-primary=orange]:not([data-md-color-scheme=slate]){--md-typeset-a-color:#e68a00}[data-md-color-primary=white]{--md-primary-fg-color:hsla(var(--md-hue),0%,100%,1);--md-primary-fg-color--light:hsla(var(--md-hue),0%,100%,0.7);--md-primary-fg-color--dark:hsla(var(--md-hue),0%,0%,0.07);--md-primary-bg-color:hsla(var(--md-hue),0%,0%,0.87);--md-primary-bg-color--light:hsla(var(--md-hue),0%,0%,0.54);--md-typeset-a-color:#4051b5}[data-md-color-primary=white] .md-button{color:var(--md-typeset-a-color)}[data-md-color-primary=white] .md-button--primary{background-color:var(--md-typeset-a-color);border-color:var(--md-typeset-a-color);color:hsla(var(--md-hue),0%,100%,1)}@media screen and (min-width:60em){[data-md-color-primary=white] .md-search__form{background-color:hsla(var(--md-hue),0%,0%,.07)}[data-md-color-primary=white] .md-search__form:hover{background-color:hsla(var(--md-hue),0%,0%,.32)}[data-md-color-primary=white] .md-search__input+.md-search__icon{color:hsla(var(--md-hue),0%,0%,.87)}}@media screen and (min-width:76.25em){[data-md-color-primary=white] .md-tabs{border-bottom:.05rem solid #00000012}}[data-md-color-primary=black]{--md-primary-fg-color:hsla(var(--md-hue),15%,9%,1);--md-primary-fg-color--light:hsla(var(--md-hue),15%,9%,0.54);--md-primary-fg-color--dark:hsla(var(--md-hue),15%,9%,1);--md-primary-bg-color:hsla(var(--md-hue),15%,100%,1);--md-primary-bg-color--light:hsla(var(--md-hue),15%,100%,0.7);--md-typeset-a-color:#4051b5}[data-md-color-primary=black] .md-button{color:var(--md-typeset-a-color)}[data-md-color-primary=black] .md-button--primary{background-color:var(--md-typeset-a-color);border-color:var(--md-typeset-a-color);color:hsla(var(--md-hue),0%,100%,1)}[data-md-color-primary=black] .md-header{background-color:hsla(var(--md-hue),15%,9%,1)}@media screen and (max-width:59.984375em){[data-md-color-primary=black] .md-nav__source{background-color:hsla(var(--md-hue),15%,11%,.87)}}@media screen and (max-width:76.234375em){html [data-md-color-primary=black] .md-nav--primary .md-nav__title[for=__drawer]{background-color:hsla(var(--md-hue),15%,9%,1)}}@media screen and (min-width:76.25em){[data-md-color-primary=black] .md-tabs{background-color:hsla(var(--md-hue),15%,9%,1)}} \ No newline at end of file diff --git a/docs/site/assets/stylesheets/modern/main.53a7feaf.min.css b/docs/site/assets/stylesheets/modern/main.53a7feaf.min.css deleted file mode 100644 index 04e34a2..0000000 --- a/docs/site/assets/stylesheets/modern/main.53a7feaf.min.css +++ /dev/null @@ -1 +0,0 @@ -@charset "UTF-8";html{-webkit-text-size-adjust:none;-moz-text-size-adjust:none;text-size-adjust:none;box-sizing:border-box}*,:after,:before{box-sizing:inherit}@media (prefers-reduced-motion){*,:after,:before{transition:none!important}}body{margin:0}a,button,input,label{-webkit-tap-highlight-color:transparent}a{color:inherit;text-decoration:none}hr{border:0;box-sizing:initial;display:block;height:.05rem;overflow:visible;padding:0}small{font-size:80%}sub,sup{line-height:1em}img{border-style:none}table{border-collapse:initial;border-spacing:0}td,th{font-weight:400;vertical-align:top}button{background:#0000;border:0;font-family:inherit;font-size:inherit;margin:0;padding:0}input{border:0;outline:none}:root{--md-primary-fg-color:#4051b5;--md-primary-fg-color--light:#5d6cc0;--md-primary-fg-color--dark:#303fa1;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3;--md-accent-fg-color:#526cfe;--md-accent-fg-color--transparent:#526cfe1a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-scheme=default]{color-scheme:light}[data-md-color-scheme=default] img[src$="#gh-dark-mode-only"],[data-md-color-scheme=default] img[src$="#only-dark"]{display:none}:root,[data-md-color-scheme=default]{--md-hue:225deg;--md-default-fg-color:#000000de;--md-default-fg-color--light:#0000008c;--md-default-fg-color--lighter:#00000052;--md-default-fg-color--lightest:#0000000d;--md-default-bg-color:#fff;--md-default-bg-color--light:#ffffffb3;--md-default-bg-color--lighter:#ffffff4d;--md-default-bg-color--lightest:#ffffff1f;--md-code-fg-color:#36464e;--md-code-bg-color:#f5f5f5;--md-code-bg-color--light:#f5f5f5b3;--md-code-bg-color--lighter:#f5f5f54d;--md-code-hl-color:#4287ff;--md-code-hl-color--light:#4287ff1a;--md-code-hl-number-color:#d52a2a;--md-code-hl-special-color:#db1457;--md-code-hl-function-color:#a846b9;--md-code-hl-constant-color:#6e59d9;--md-code-hl-keyword-color:#3f6ec6;--md-code-hl-string-color:#1c7d4d;--md-code-hl-name-color:var(--md-code-fg-color);--md-code-hl-operator-color:var(--md-default-fg-color--light);--md-code-hl-punctuation-color:var(--md-default-fg-color--light);--md-code-hl-comment-color:var(--md-default-fg-color--light);--md-code-hl-generic-color:var(--md-default-fg-color--light);--md-code-hl-variable-color:var(--md-default-fg-color--light);--md-typeset-color:var(--md-default-fg-color);--md-typeset-a-color:var(--md-primary-fg-color);--md-typeset-del-color:#f5503d26;--md-typeset-ins-color:#0bd57026;--md-typeset-kbd-color:#fafafa;--md-typeset-kbd-accent-color:#fff;--md-typeset-kbd-border-color:#b8b8b8;--md-typeset-mark-color:#ffff0080;--md-typeset-table-color:#0000001f;--md-typeset-table-color--light:rgba(0,0,0,.035);--md-admonition-fg-color:var(--md-default-fg-color);--md-admonition-bg-color:var(--md-default-bg-color);--md-warning-fg-color:#000000de;--md-warning-bg-color:#ff9;--md-shadow-z1:0 0.2rem 0.5rem #0000000d,0 0 0.05rem #0000001a;--md-shadow-z2:0 0.2rem 0.5rem #0000001a,0 0 0.05rem #00000040;--md-shadow-z3:0 0.2rem 0.5rem #0003,0 0 0.05rem #00000059;--color-foreground:0 0 0;--color-background:255 255 255;--color-background-subtle:240 240 240;--color-backdrop:255 255 255}.md-icon svg{fill:currentcolor;display:block;height:1.2rem;width:1.2rem}.md-icon svg.lucide{fill:#0000;stroke:currentcolor}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;--md-text-font-family:var(--md-text-font,_),-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;--md-code-font-family:var(--md-code-font,_),SFMono-Regular,Consolas,Menlo,monospace}aside,body,input{font-feature-settings:"kern","liga";color:var(--md-typeset-color);font-family:var(--md-text-font-family)}code,kbd,pre{font-feature-settings:"kern";font-family:var(--md-code-font-family)}:root{--md-typeset-table-sort-icon:url('data:image/svg+xml;charset=utf-8,');--md-typeset-table-sort-icon--asc:url('data:image/svg+xml;charset=utf-8,');--md-typeset-table-sort-icon--desc:url('data:image/svg+xml;charset=utf-8,');--md-typeset-preview-icon:url('data:image/svg+xml;charset=utf-8,')}.md-typeset{-webkit-print-color-adjust:exact;color-adjust:exact;font-size:.75rem;letter-spacing:-.01em;line-height:1.8;overflow-wrap:break-word}@media print{.md-typeset{font-size:.68rem}}.md-typeset blockquote,.md-typeset dl,.md-typeset figure,.md-typeset ol,.md-typeset pre,.md-typeset ul{margin-bottom:1em;margin-top:1em}.md-typeset h1{color:var(--md-default-fg-color);font-size:1.875em;line-height:1.3;margin:0 0 1.25em}.md-typeset h1,.md-typeset h2{font-weight:700;letter-spacing:-.025em}.md-typeset h2{font-size:1.5em;line-height:1.4;margin:1.6em 0 .64em}.md-typeset h3{font-size:1.25em;font-weight:700;letter-spacing:-.01em;line-height:1.5;margin:1.6em 0 .8em}.md-typeset h2+h3{margin-top:.8em}.md-typeset h4{font-weight:700;letter-spacing:-.01em;margin:1em 0}.md-typeset h5,.md-typeset h6{color:var(--md-default-fg-color--light);font-size:.8em;font-weight:700;letter-spacing:-.01em;margin:1.25em 0}.md-typeset h5{text-transform:uppercase}.md-typeset h5 code{text-transform:none}.md-typeset hr{border-bottom:.05rem solid var(--md-default-fg-color--lightest);display:flow-root;margin:1.5em 0}.md-typeset a{color:var(--md-typeset-a-color);text-decoration:underline;word-break:break-word}.md-typeset a,.md-typeset a:before{transition:color 125ms}.md-typeset a:focus,.md-typeset a:hover{color:var(--md-accent-fg-color)}.md-typeset a:focus code,.md-typeset a:hover code{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-typeset a code{color:var(--md-typeset-a-color)}.md-typeset a.focus-visible{outline-color:var(--md-accent-fg-color);outline-offset:.2rem}.md-typeset code,.md-typeset kbd,.md-typeset pre{color:var(--md-code-fg-color);direction:ltr;font-variant-ligatures:none;transition:background-color 125ms}@media print{.md-typeset code,.md-typeset kbd,.md-typeset pre{white-space:pre-wrap}}.md-typeset code{background-color:var(--md-code-bg-color);border-radius:.2rem;-webkit-box-decoration-break:clone;box-decoration-break:clone;font-size:.85em;padding:.25em .4em;transition:color 125ms,background-color 125ms;word-break:break-word}.md-typeset code:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}.md-typeset pre{display:flow-root;line-height:1.4;position:relative}.md-typeset pre>code{border-radius:.4rem;-webkit-box-decoration-break:slice;box-decoration-break:slice;box-shadow:none;display:block;margin:0;outline-color:var(--md-accent-fg-color);overflow:auto;padding:.7720588235em 1.1764705882em;scrollbar-color:var(--md-default-fg-color--lighter) #0000;scrollbar-width:thin;touch-action:auto;word-break:normal}.md-typeset pre>code:hover{scrollbar-color:var(--md-accent-fg-color) #0000}.md-typeset pre>code::-webkit-scrollbar{height:.2rem;width:.2rem}.md-typeset pre>code::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-typeset pre>code::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}.md-typeset kbd{border-radius:.2rem;box-shadow:0 0 0 .05rem var(--md-typeset-kbd-border-color),0 .15rem 0 var(--md-typeset-kbd-border-color);color:var(--md-default-fg-color);display:inline-block;font-size:.75em;padding:0 .6666666667em;vertical-align:text-top;word-break:break-word}.md-typeset mark{background-color:var(--md-typeset-mark-color);-webkit-box-decoration-break:clone;box-decoration-break:clone;color:inherit;word-break:break-word}.md-typeset abbr{border-bottom:.05rem dotted var(--md-default-fg-color--light);cursor:help;text-decoration:none}.md-typeset [data-preview]{position:relative}[dir=ltr] .md-typeset [data-preview]:after{margin-left:.125em}[dir=rtl] .md-typeset [data-preview]:after{margin-right:.125em}.md-typeset [data-preview]:after{background-color:currentcolor;content:"";display:inline-block;height:.8em;-webkit-mask-image:var(--md-typeset-preview-icon);mask-image:var(--md-typeset-preview-icon);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:background-color 125ms;vertical-align:text-top;width:.8em}.md-typeset small{opacity:.75}[dir=ltr] .md-typeset sub,[dir=ltr] .md-typeset sup{margin-left:.078125em}[dir=rtl] .md-typeset sub,[dir=rtl] .md-typeset sup{margin-right:.078125em}[dir=ltr] .md-typeset blockquote{padding-left:.6rem}[dir=rtl] .md-typeset blockquote{padding-right:.6rem}[dir=ltr] .md-typeset blockquote{border-left:.2rem solid var(--md-default-fg-color--lighter)}[dir=rtl] .md-typeset blockquote{border-right:.2rem solid var(--md-default-fg-color--lighter)}.md-typeset blockquote{color:var(--md-default-fg-color--light);margin-left:0;margin-right:0}.md-typeset ul{list-style-type:disc}.md-typeset ul[type]{list-style-type:revert-layer}[dir=ltr] .md-typeset ol,[dir=ltr] .md-typeset ul{margin-left:.625em}[dir=rtl] .md-typeset ol,[dir=rtl] .md-typeset ul{margin-right:.625em}.md-typeset ol,.md-typeset ul{padding:0}.md-typeset ol:not([hidden]),.md-typeset ul:not([hidden]){display:flow-root}.md-typeset ol ol,.md-typeset ul ol{list-style-type:lower-alpha}.md-typeset ol ol ol,.md-typeset ul ol ol{list-style-type:lower-roman}.md-typeset ol ol ol ol,.md-typeset ul ol ol ol{list-style-type:upper-alpha}.md-typeset ol ol ol ol ol,.md-typeset ul ol ol ol ol{list-style-type:upper-roman}.md-typeset ol[type],.md-typeset ul[type]{list-style-type:revert-layer}[dir=ltr] .md-typeset ol li,[dir=ltr] .md-typeset ul li{margin-left:1.25em}[dir=rtl] .md-typeset ol li,[dir=rtl] .md-typeset ul li{margin-right:1.25em}.md-typeset ol li,.md-typeset ul li{margin-bottom:.5em}.md-typeset ol li blockquote,.md-typeset ol li p,.md-typeset ul li blockquote,.md-typeset ul li p{margin:.5em 0}.md-typeset ol li:last-child,.md-typeset ul li:last-child{margin-bottom:0}[dir=ltr] .md-typeset ol li ol,[dir=ltr] .md-typeset ol li ul,[dir=ltr] .md-typeset ul li ol,[dir=ltr] .md-typeset ul li ul{margin-left:.625em}[dir=rtl] .md-typeset ol li ol,[dir=rtl] .md-typeset ol li ul,[dir=rtl] .md-typeset ul li ol,[dir=rtl] .md-typeset ul li ul{margin-right:.625em}.md-typeset ol li ol,.md-typeset ol li ul,.md-typeset ul li ol,.md-typeset ul li ul{margin-bottom:.5em;margin-top:.5em}[dir=ltr] .md-typeset dd{margin-left:1.875em}[dir=rtl] .md-typeset dd{margin-right:1.875em}.md-typeset dd{margin-bottom:1.5em;margin-top:1em}.md-typeset img,.md-typeset svg,.md-typeset video{height:auto;max-width:100%}.md-typeset img[align=left]{margin:1em 1em 1em 0}.md-typeset img[align=right]{margin:1em 0 1em 1em}.md-typeset img[align]:only-child{margin-top:0}.md-typeset figure{display:flow-root;margin:1em auto;max-width:100%;text-align:center;width:fit-content}.md-typeset figure img{display:block;margin:0 auto}.md-typeset figcaption{font-style:italic;margin:1em auto;max-width:24rem}.md-typeset iframe{max-width:100%}.md-typeset table:not([class]){background-color:var(--md-default-bg-color);border:.05rem solid var(--md-typeset-table-color);border-radius:.1rem;display:inline-block;font-size:.64rem;max-width:100%;overflow:auto;touch-action:auto}@media print{.md-typeset table:not([class]){display:table}}.md-typeset table:not([class])+*{margin-top:1.5em}.md-typeset table:not([class]) td>:first-child,.md-typeset table:not([class]) th>:first-child{margin-top:0}.md-typeset table:not([class]) td>:last-child,.md-typeset table:not([class]) th>:last-child{margin-bottom:0}.md-typeset table:not([class]) td:not([align]),.md-typeset table:not([class]) th:not([align]){text-align:left}[dir=rtl] .md-typeset table:not([class]) td:not([align]),[dir=rtl] .md-typeset table:not([class]) th:not([align]){text-align:right}.md-typeset table:not([class]) th{font-weight:700;min-width:5rem;padding:.9375em 1.25em;vertical-align:top}.md-typeset table:not([class]) td{border-top:.05rem solid var(--md-typeset-table-color);padding:.9375em 1.25em;vertical-align:top}.md-typeset table:not([class]) tbody tr{transition:background-color 125ms}.md-typeset table:not([class]) tbody tr:hover{background-color:var(--md-typeset-table-color--light);box-shadow:0 .05rem 0 var(--md-default-bg-color) inset}.md-typeset table:not([class]) a{word-break:normal}.md-typeset table th[role=columnheader]{cursor:pointer}[dir=ltr] .md-typeset table th[role=columnheader]:after{margin-left:.5em}[dir=rtl] .md-typeset table th[role=columnheader]:after{margin-right:.5em}.md-typeset table th[role=columnheader]:after{content:"";display:inline-block;height:1.2em;-webkit-mask-image:var(--md-typeset-table-sort-icon);mask-image:var(--md-typeset-table-sort-icon);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:background-color 125ms;vertical-align:text-bottom;width:1.2em}.md-typeset table th[role=columnheader]:hover:after{background-color:var(--md-default-fg-color--lighter)}.md-typeset table th[role=columnheader][aria-sort=ascending]:after{background-color:var(--md-default-fg-color--light);-webkit-mask-image:var(--md-typeset-table-sort-icon--asc);mask-image:var(--md-typeset-table-sort-icon--asc)}.md-typeset table th[role=columnheader][aria-sort=descending]:after{background-color:var(--md-default-fg-color--light);-webkit-mask-image:var(--md-typeset-table-sort-icon--desc);mask-image:var(--md-typeset-table-sort-icon--desc)}.md-typeset__scrollwrap{margin:1em -.8rem;overflow-x:auto;touch-action:auto}.md-typeset__table{display:inline-block;margin-bottom:.5em;padding:0 .8rem}@media print{.md-typeset__table{display:block}}html .md-typeset__table table{display:table;margin:0;overflow:hidden;width:100%}@media screen and (max-width:44.984375em){.md-content__inner>pre{margin:1em -.8rem}.md-content__inner>pre code{border-radius:0}}.md-banner{background-color:var(--md-accent-fg-color--transparent);color:var(--md-default-fg-color);overflow:auto}@media print{.md-banner{display:none}}.md-banner--warning{background-color:var(--md-warning-bg-color);color:var(--md-warning-fg-color)}.md-banner__inner{font-size:.7rem;margin:.6rem auto;padding:0 .8rem}[dir=ltr] .md-banner__button{float:right}[dir=rtl] .md-banner__button{float:left}.md-banner__button{color:inherit;cursor:pointer;transition:opacity .25s}.no-js .md-banner__button{display:none}.md-banner__button:hover{opacity:.7}html{scrollbar-gutter:stable;font-size:125%;height:100%;overflow-x:hidden}@media screen and (min-width:100em){html{font-size:137.5%}}@media screen and (min-width:125em){html{font-size:150%}}body{background-color:var(--md-default-bg-color);display:flex;flex-direction:column;font-size:.5rem;min-height:100%;position:relative;width:100%}@media print{body{display:block}}@media screen and (max-width:59.984375em){body[data-md-scrolllock]{position:fixed}}.md-grid{margin-left:auto;margin-right:auto;max-width:61rem}.md-container{display:flex;flex-direction:column;flex-grow:1}@media print{.md-container{display:block}}.md-main{flex-grow:1}.md-main__inner{display:flex;height:100%;margin-top:1.5rem}.md-ellipsis{overflow:hidden;text-overflow:ellipsis}.md-toggle{display:none}.md-option{height:0;opacity:0;position:absolute;width:0}.md-option:checked+label:not([hidden]){display:block}.md-option.focus-visible+label{outline-color:var(--md-accent-fg-color);outline-style:auto}.md-skip{background-color:var(--md-default-fg-color);border-radius:.1rem;color:var(--md-default-bg-color);font-size:.64rem;margin:.5rem;opacity:0;outline-color:var(--md-accent-fg-color);padding:.3rem .5rem;position:fixed;transform:translateY(.4rem);z-index:-1}.md-skip:focus{opacity:1;transform:translateY(0);transition:transform .25s cubic-bezier(.4,0,.2,1),opacity 175ms 75ms;z-index:10}@page{margin:25mm}:root{--md-clipboard-icon:url('data:image/svg+xml;charset=utf-8,')}.md-clipboard{border-radius:.1rem;color:var(--md-default-fg-color--lightest);cursor:pointer;height:1.5em;outline-color:var(--md-accent-fg-color);outline-offset:.1rem;transition:color .25s;width:1.5em;z-index:1}@media print{.md-clipboard{display:none}}.md-clipboard:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}:hover>.md-clipboard{color:var(--md-default-fg-color--light)}.md-clipboard:focus,.md-clipboard:hover{color:var(--md-accent-fg-color)}.md-clipboard:after{background-color:currentcolor;content:"";display:block;height:1.125em;margin:0 auto;-webkit-mask-image:var(--md-clipboard-icon);mask-image:var(--md-clipboard-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:1.125em}.md-clipboard--inline{cursor:pointer}.md-clipboard--inline code{transition:color .25s,background-color .25s}.md-clipboard--inline:focus code,.md-clipboard--inline:hover code{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}:root{--md-code-select-icon:url('data:image/svg+xml;charset=utf-8,');--md-code-copy-icon:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .md-code__content{display:grid}.md-code__nav{background-color:var(--md-code-bg-color--lighter);border-radius:.1rem;display:flex;gap:.2rem;padding:.2rem;position:absolute;right:.25em;top:.25em;transition:background-color .25s;z-index:1}:hover>.md-code__nav{background-color:var(--md-code-bg-color--light)}.md-code__button{color:var(--md-default-fg-color--lightest);cursor:pointer;display:block;height:1.5em;outline-color:var(--md-accent-fg-color);outline-offset:.1rem;transition:color .25s;width:1.5em}:hover>*>.md-code__button{color:var(--md-default-fg-color--light)}.md-code__button.focus-visible,.md-code__button:hover{color:var(--md-accent-fg-color)}.md-code__button--active{color:var(--md-default-fg-color)!important}.md-code__button:after{background-color:currentcolor;content:"";display:block;height:1.125em;margin:0 auto;-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:1.125em}.md-code__button[data-md-type=select]:after{-webkit-mask-image:var(--md-code-select-icon);mask-image:var(--md-code-select-icon)}.md-code__button[data-md-type=copy]:after{-webkit-mask-image:var(--md-code-copy-icon);mask-image:var(--md-code-copy-icon)}@keyframes consent{0%{opacity:0;transform:translateY(100%)}to{opacity:1;transform:translateY(0)}}@keyframes overlay{0%{opacity:0}to{opacity:1}}.md-consent__overlay{animation:overlay .35s both;-webkit-backdrop-filter:blur(.2rem);backdrop-filter:blur(.2rem);background-color:var(--md-default-bg-color--light);height:100%;opacity:1;position:fixed;top:0;width:100%;z-index:5}.md-consent__inner{bottom:0;display:flex;justify-content:center;max-height:100%;padding:0;position:fixed;width:100%;z-index:5}.md-consent__form{animation:consent .5s cubic-bezier(.1,.7,.1,1) both;background-color:var(--md-default-bg-color);border:0;border-radius:.8rem;box-shadow:var(--md-shadow-z3);margin:.4rem;overflow:auto;padding-left:1.2rem;padding-right:1.2rem}.md-consent__settings{display:none;margin:1em 0}input:checked+.md-consent__settings{display:block}.md-consent__controls{line-height:1.2;margin-bottom:.8rem}.md-typeset .md-consent__controls .md-button{display:inline}@media screen and (max-width:44.984375em){.md-typeset .md-consent__controls .md-button{display:block;margin-top:.4rem;text-align:center;width:100%}}.md-consent label{cursor:pointer}.md-content{flex-grow:1;min-width:0}.md-content__inner{margin:0 .8rem 1.2rem;padding-top:.7rem}@media screen and (min-width:76.25em){[dir=ltr] .md-sidebar--primary:not([hidden])~.md-content>.md-content__inner{margin-left:1.2rem}[dir=ltr] .md-sidebar--secondary:not([hidden])~.md-content>.md-content__inner,[dir=rtl] .md-sidebar--primary:not([hidden])~.md-content>.md-content__inner{margin-right:1.2rem}[dir=rtl] .md-sidebar--secondary:not([hidden])~.md-content>.md-content__inner{margin-left:1.2rem}}.md-content__inner:before{content:"";display:block;height:.4rem}.md-content__inner>:last-child{margin-bottom:0}[dir=ltr] .md-content__button{float:right}[dir=rtl] .md-content__button{float:left}[dir=ltr] .md-content__button{margin-left:.4rem}[dir=rtl] .md-content__button{margin-right:.4rem}.md-content__button{background-color:var(--md-default-fg-color--lightest);border-radius:.4rem;display:flex;margin-top:.2rem;padding:.3rem}@media print{.md-content__button{display:none}}.md-typeset .md-content__button{color:var(--md-default-fg-color);transition:color .25s,background-color .25s}.md-typeset .md-content__button svg{opacity:.5;transition:opacity .25s}.md-typeset .md-content__button:focus,.md-typeset .md-content__button:hover{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-typeset .md-content__button:focus svg,.md-typeset .md-content__button:hover svg{opacity:1}.md-content__button svg{height:.9rem;width:.9rem}[dir=rtl] .md-content__button svg{transform:scaleX(-1)}.md-content__button svg.lucide{fill:#0000;stroke:currentcolor}[dir=ltr] .md-dialog{right:.8rem}[dir=rtl] .md-dialog{left:.8rem}.md-dialog{background-color:var(--md-accent-fg-color);border-radius:1.2rem;bottom:.8rem;box-shadow:var(--md-shadow-z3);min-width:11.1rem;opacity:0;padding:.4rem 1.2rem;pointer-events:none;position:fixed;transform:translateY(100%);transition:transform 0ms .4s,opacity .4s;z-index:4}@media print{.md-dialog{display:none}}.md-dialog--active{opacity:1;pointer-events:auto;transform:translateY(0);transition:transform .4s cubic-bezier(.075,.85,.175,1),opacity .4s}.md-dialog__inner{color:var(--md-default-bg-color);font-size:.7rem}.md-feedback{margin:2em 0 1em;text-align:center}.md-feedback fieldset{border:none;margin:0;padding:0}.md-feedback__title{font-weight:700;margin:1em auto}.md-feedback__inner{position:relative}.md-feedback__list{display:flex;flex-wrap:wrap;place-content:baseline center;position:relative}.md-feedback__list:hover .md-icon:not(:disabled){color:var(--md-default-fg-color--lighter)}:disabled .md-feedback__list{min-height:1.8rem}.md-feedback__icon{color:var(--md-default-fg-color--light);cursor:pointer;flex-shrink:0;margin:0 .1rem;transition:color 125ms}.md-feedback__icon:not(:disabled).md-icon:hover{color:var(--md-accent-fg-color)}.md-feedback__icon:disabled{color:var(--md-default-fg-color--lightest);pointer-events:none}.md-feedback__note{opacity:0;position:relative;transform:translateY(.4rem);transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .15s}.md-feedback__note>*{margin:0 auto;max-width:16rem}:disabled .md-feedback__note{opacity:1;transform:translateY(0)}@media print{.md-feedback{display:none}}.md-footer{background-color:var(--md-default-bg-color);border-top:.05rem solid var(--md-default-fg-color--lightest);color:var(--md-default-fg-color)}@media print{.md-footer{display:none}}.md-footer__inner{justify-content:space-between;overflow:auto;padding:.2rem}.md-footer__inner:not([hidden]){display:flex}.md-footer__link{align-items:end;display:flex;flex-grow:0.01;margin-bottom:.4rem;margin-top:1rem;max-width:100%;outline-color:var(--md-accent-fg-color);overflow:hidden;transition:opacity .25s}.md-footer__link:focus,.md-footer__link:hover{opacity:.7}[dir=rtl] .md-footer__link svg{transform:scaleX(-1)}@media screen and (max-width:44.984375em){.md-footer__link--prev{flex-shrink:0}.md-footer__link--prev .md-footer__title{display:none}}[dir=ltr] .md-footer__link--next{margin-left:auto}[dir=rtl] .md-footer__link--next{margin-right:auto}.md-footer__link--next{text-align:right}[dir=rtl] .md-footer__link--next{text-align:left}.md-footer__title{flex-grow:1;font-size:.8rem;margin-bottom:.7rem;max-width:calc(100% - 2.4rem);padding:0 1rem;white-space:nowrap}.md-footer__button{margin:.2rem;padding:.4rem}.md-footer__direction{font-size:.6rem;opacity:.7}.md-footer-meta{background-color:var(--md-default-fg-color--lightest)}.md-footer-meta__inner{display:flex;flex-wrap:wrap;justify-content:space-between;padding:.2rem}html .md-footer-meta.md-typeset a:not(:focus,:hover){color:var(--md-default-fg-color)}.md-copyright{color:var(--md-default-fg-color--light);font-size:.64rem;margin:auto .6rem;padding:.4rem 0;width:100%}@media screen and (min-width:45em){.md-copyright{width:auto}}.md-copyright__highlight{color:var(--md-default-fg-color)}.md-social{display:inline-flex;gap:.2rem;margin:0 .4rem;padding:.2rem 0 .6rem}@media screen and (min-width:45em){.md-social{padding:.6rem 0}}.md-social__link{display:inline-block;height:1.6rem;text-align:center;width:1.6rem}.md-social__link:before{line-height:1.9}.md-social__link svg{fill:currentcolor;max-height:.8rem;vertical-align:-25%}.md-social__link svg.lucide{fill:#0000;stroke:currentcolor}.md-typeset .md-button{background-color:var(--md-default-fg-color--lightest);border-radius:1.2rem;color:var(--md-default-fg-color--light);cursor:pointer;display:inline-block;font-size:.875em;font-weight:700;padding:.625em 2em;text-decoration:none;transition:color 125ms,background-color 125ms,opacity 125ms}.md-typeset .md-button.focus-visible{outline-offset:0}.md-typeset .md-button:focus,.md-typeset .md-button:hover{color:var(--md-default-fg-color--light);opacity:.8}.md-typeset .md-button--primary{background-color:var(--md-primary-fg-color);color:var(--md-primary-bg-color)}.md-typeset .md-button--primary:focus,.md-typeset .md-button--primary:hover{color:var(--md-primary-bg-color);opacity:.8}[dir=ltr] .md-typeset .md-input{border-top-left-radius:.1rem}[dir=ltr] .md-typeset .md-input,[dir=rtl] .md-typeset .md-input{border-top-right-radius:.1rem}[dir=rtl] .md-typeset .md-input{border-top-left-radius:.1rem}.md-typeset .md-input{border-bottom:.1rem solid var(--md-default-fg-color--lighter);box-shadow:var(--md-shadow-z1);font-size:.8rem;height:1.8rem;padding:0 .6rem;transition:border .25s,box-shadow .25s}.md-typeset .md-input:focus,.md-typeset .md-input:hover{border-bottom-color:var(--md-accent-fg-color);box-shadow:var(--md-shadow-z2)}.md-typeset .md-input--stretch{width:100%}.md-header{-webkit-backdrop-filter:blur(.4rem);backdrop-filter:blur(.4rem);background-color:var(--md-default-bg-color--light);color:var(--md-default-fg-color);display:block;left:0;position:sticky;right:0;top:0;z-index:4}@media print{.md-header{display:none}}.md-header[hidden]{transform:translateY(-100%);transition:transform .25s cubic-bezier(.8,0,.6,1)}.md-header--shadow{box-shadow:0 .05rem 0 var(--md-default-fg-color--lightest);transition:transform .25s cubic-bezier(.1,.7,.1,1)}.md-header__inner{align-items:center;display:flex;padding:0 .4rem}.md-header__button{color:currentcolor;cursor:pointer;margin:.2rem;outline-color:var(--md-accent-fg-color);padding:.4rem;position:relative;transition:opacity .25s;vertical-align:middle;z-index:1}.md-header__button:hover{opacity:.7}.md-header__button:not([hidden]){display:inline-block}.md-header__button:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}.md-header__button.md-logo{margin:.2rem;padding:.4rem}@media screen and (max-width:76.234375em){.md-header__button.md-logo{display:none}}.md-header__button.md-logo img,.md-header__button.md-logo svg{fill:currentcolor;display:block;height:1.2rem;width:auto}.md-header__button.md-logo img.lucide,.md-header__button.md-logo svg.lucide{fill:#0000;stroke:currentcolor}@media screen and (min-width:60em){.md-header__button[for=__search]{display:none}}.no-js .md-header__button[for=__search]{display:none}[dir=rtl] .md-header__button[for=__search] svg{transform:scaleX(-1)}@media screen and (min-width:76.25em){.md-header__button[for=__drawer]{display:none}}.md-header__topic{display:flex;max-width:100%;position:absolute;transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .15s;white-space:nowrap}.md-header__topic+.md-header__topic{opacity:0;pointer-events:none;transform:translateX(1.25rem);transition:transform .4s cubic-bezier(1,.7,.1,.1),opacity .15s;z-index:-1}[dir=rtl] .md-header__topic+.md-header__topic{transform:translateX(-1.25rem)}.md-header__topic:first-child{font-weight:700}.md-header__title{flex-grow:1;font-size:.9rem;height:2.4rem;letter-spacing:-.025em;line-height:2.4rem;margin-left:.4rem;margin-right:.4rem}.md-header__title--active .md-header__topic{opacity:0;pointer-events:none;transform:translateX(-1.25rem);transition:transform .4s cubic-bezier(1,.7,.1,.1),opacity .15s;z-index:-1}[dir=rtl] .md-header__title--active .md-header__topic{transform:translateX(1.25rem)}.md-header__title--active .md-header__topic+.md-header__topic{opacity:1;pointer-events:auto;transform:translateX(0);transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .15s;z-index:0}.md-header__title>.md-header__ellipsis{height:100%;position:relative;width:100%}.md-header__option{display:flex;flex-shrink:0;max-width:100%;white-space:nowrap}.md-header__option>input{bottom:0}.md-header__source{display:none}@media screen and (min-width:60em){[dir=ltr] .md-header__source{margin-left:1rem}[dir=rtl] .md-header__source{margin-right:1rem}.md-header__source{display:block;max-width:11.5rem;width:11.5rem}}@media screen and (min-width:76.25em){[dir=ltr] .md-header__source{margin-left:1.4rem}[dir=rtl] .md-header__source{margin-right:1.4rem}}.md-header .md-icon svg{height:1rem;width:1rem}:root{--md-nav-icon--next:url('data:image/svg+xml;charset=utf-8,')}.md-nav{font-size:.7rem;line-height:1.3;transition:max-height .25s cubic-bezier(.86,0,.07,1)}.md-nav .md-nav__title{display:none}.md-nav__list{display:flex;flex-direction:column;gap:.2rem;list-style:none;margin:0;padding:0}[dir=ltr] .md-nav__list .md-nav__list{margin-left:.6rem}[dir=rtl] .md-nav__list .md-nav__list{margin-right:.6rem}.md-nav__item--nested .md-nav__list:after,.md-nav__item--nested .md-nav__list:before{content:" ";display:block;height:0}.md-nav__link{align-items:flex-start;border-radius:.4rem;cursor:pointer;display:flex;gap:.6rem;margin-left:.2rem;margin-right:.2rem;padding:.35rem .8rem;transition:color .25s,background-color .25s}.md-nav__link .md-nav__link{margin:0}.md-nav__link--passed,.md-nav__link--passed code{color:var(--md-default-fg-color--light)}.md-nav__item .md-nav__link--active{font-weight:500}.md-nav--primary .md-nav__item .md-nav__link--active{background:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-nav__item .md-nav__link--active,.md-nav__item .md-nav__link--active code{color:var(--md-typeset-a-color)}.md-nav__item .md-nav__link--active code svg,.md-nav__item .md-nav__link--active svg{opacity:1}[dir=ltr] .md-nav__item--nested>.md-nav__link:not(.md-nav__container){padding-right:.35rem}[dir=rtl] .md-nav__item--nested>.md-nav__link:not(.md-nav__container){padding-left:.35rem}.md-nav__link .md-ellipsis{flex-grow:1;position:relative}.md-nav__link .md-ellipsis code{word-break:normal}.md-nav__link svg{fill:currentcolor;flex-shrink:0;height:1.3em;opacity:.5;position:relative;width:1.3em}.md-nav__link svg.lucide{fill:#0000;stroke:currentcolor}.md-nav--primary .md-nav__link[for]:focus:not(.md-nav__link--active),.md-nav--primary .md-nav__link[for]:hover:not(.md-nav__link--active),.md-nav--primary .md-nav__link[href]:focus:not(.md-nav__link--active),.md-nav--primary .md-nav__link[href]:hover:not(.md-nav__link--active){background-color:var(--md-default-fg-color--lightest);color:var(--md-default-fg-color)}.md-nav--secondary .md-nav__link{margin-left:.2rem;margin-right:.2rem;padding:.35rem .8rem}.md-nav--secondary .md-nav__link[for]:focus,.md-nav--secondary .md-nav__link[for]:hover,.md-nav--secondary .md-nav__link[href]:focus,.md-nav--secondary .md-nav__link[href]:hover{background-color:initial;color:var(--md-accent-fg-color)}.md-nav__link.focus-visible{outline-color:var(--md-accent-fg-color)}.md-nav--primary .md-nav__link[for=__toc],.md-nav--primary .md-nav__link[for=__toc]~.md-nav{display:none}.md-nav__icon{font-size:.9rem;height:.9rem;width:.9rem}[dir=rtl] .md-nav__icon:after{transform:rotate(180deg)}.md-nav__item--nested .md-nav__icon:after{background-color:currentcolor;content:"";display:block;height:100%;-webkit-mask-image:var(--md-nav-icon--next);mask-image:var(--md-nav-icon--next);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:transform .25s;width:100%}@media screen and (min-width:76.25em){.md-nav__item--nested.md-nav__item--section>.md-nav__link .md-nav__icon:after{display:none}}.md-nav__item--nested .md-nav__toggle:checked~.md-nav__link .md-nav__icon:after,.md-nav__item--nested .md-toggle--indeterminate~.md-nav__link .md-nav__icon:after{transform:rotate(90deg)}.md-nav__container{background:#0000;gap:.2rem;padding:0}.md-nav__container>:first-child{flex-grow:1;min-width:0}.md-nav__container>:nth-child(2){padding:.35rem}@media screen and (min-width:76.25em){.md-nav__item--section>.md-nav__container>:nth-child(2){display:none}}.md-nav__container__icon{flex-shrink:0}.md-nav__toggle~.md-nav{display:grid;grid-template-rows:minmax(.005rem,0fr);opacity:0;transition:grid-template-rows .25s cubic-bezier(.86,0,.07,1),opacity .25s,visibility 0ms .25s;visibility:collapse}.md-nav__toggle~.md-nav>.md-nav__list{overflow:hidden}.md-nav__toggle.md-toggle--indeterminate~.md-nav,.md-nav__toggle:checked~.md-nav{grid-template-rows:minmax(.4rem,1fr);opacity:1;transition:grid-template-rows .25s cubic-bezier(.86,0,.07,1),opacity .15s .1s,visibility 0ms;visibility:visible}.md-nav__toggle.md-toggle--indeterminate~.md-nav{transition:none}.md-nav--secondary{margin-bottom:.1rem;margin-top:.1rem}.md-nav--secondary .md-nav{margin-top:.2rem}.md-nav--secondary .md-nav__title{background:var(--md-default-bg-color);display:flex;font-weight:700;margin-left:.2rem;margin-right:.2rem;padding:.35rem .6rem;position:sticky;top:0;z-index:1}.md-nav--secondary .md-nav__title .md-nav__icon{display:none}.md-nav--secondary .md-nav__link{padding:.2rem .6rem}@media screen and (max-width:76.234375em){.md-nav--primary{margin-bottom:.4rem;margin-left:.2rem;margin-right:.2rem}.md-nav .md-nav__title[for=__drawer]{align-items:center;border-bottom:.05rem solid var(--md-default-fg-color-lightest);display:flex;font-size:.8rem;font-weight:700;gap:.4rem;padding:.8rem}.md-nav .md-nav__title[for=__drawer] .md-logo{height:1.6rem;width:1.6rem}.md-nav .md-nav__title[for=__drawer] .md-logo img,.md-nav .md-nav__title[for=__drawer] .md-logo svg{fill:currentcolor;display:block;height:100%;max-width:100%;object-fit:contain;width:auto}.md-nav .md-nav__title[for=__drawer] .md-logo img.lucide,.md-nav .md-nav__title[for=__drawer] .md-logo svg.lucide{fill:#0000;stroke:currentcolor}}.md-nav__source{border:.05rem solid var(--md-default-fg-color--lightest);border-radius:.4rem;margin:.2rem .2rem .6rem;transition:background-color .25s,border-color .25s}.md-nav__source:focus,.md-nav__source:hover{background-color:var(--md-default-fg-color--lightest);border-color:#0000}[dir=ltr] .md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary{margin-left:1.1rem}[dir=rtl] .md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary{margin-right:1.1rem}[dir=ltr] .md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary{border-left:.05rem solid var(--md-default-fg-color--lightest)}[dir=rtl] .md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary{border-right:.05rem solid var(--md-default-fg-color--lightest)}.md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary{display:block;margin-bottom:.5em;margin-top:.5em;opacity:1;visibility:visible}.md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary .md-nav__link{background:#0000}.md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary .md-nav__link--active{font-weight:500}.md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary .md-nav__link:focus,.md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary .md-nav__link:hover{color:var(--md-accent-fg-color)}.md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary>.md-nav__list{margin-left:0;overflow:visible;padding-bottom:0}.md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary>.md-nav__title{display:none}@media screen and (min-width:76.25em){.md-nav--primary{margin-bottom:.1rem;margin-top:.1rem}.md-nav__source{display:none}[dir=ltr] .md-nav__list .md-nav__item--section>.md-nav>.md-nav__list{margin-left:0}[dir=rtl] .md-nav__list .md-nav__item--section>.md-nav>.md-nav__list{margin-right:0}.md-nav__item--section>.md-nav__link--active,.md-nav__item--section>.md-nav__link>.md-nav__link--active{font-weight:700}.md-nav__item--section{margin-top:.4rem}.md-nav__item--section:first-child{margin-top:0}.md-nav__item--section:last-child{margin-bottom:0}.md-nav__item--section>.md-nav__link{font-weight:700}.md-nav__item--section>.md-nav__link:not(.md-nav__container){pointer-events:none}.md-nav__item--section>.md-nav{display:block;opacity:1;visibility:visible}.md-nav__item--section>.md-nav>.md-nav__list>.md-nav__item{padding:0}.md-nav--lifted{margin-top:0}.md-nav--lifted>.md-nav__list>.md-nav__item{display:none}.md-nav--lifted>.md-nav__list>.md-nav__item--active{display:block}.md-nav--lifted>.md-nav__list>.md-nav__item--active>.md-nav{margin-top:.1rem}.md-nav--lifted>.md-nav__list>.md-nav__item--active>.md-nav>.md-nav__list:before,.md-nav--lifted>.md-nav__list>.md-nav__item--active>.md-nav__link{display:none}.md-nav--lifted>.md-nav__list>.md-nav__item--active.md-nav__item--section{margin:0}.md-nav--lifted .md-nav[data-md-level="1"]{grid-template-rows:minmax(.4rem,1fr);opacity:1;visibility:visible}}:root{--md-path-icon:url('data:image/svg+xml;charset=utf-8,')}.md-path{font-size:.7rem;margin:.4rem .8rem 0;overflow:auto;padding-top:1.2rem}.md-path:not([hidden]){display:block}@media screen and (min-width:76.25em){.md-path{margin:.4rem 1.2rem 0}}.md-path__list{align-items:center;display:flex;gap:.2rem;list-style:none;margin:0;padding:0}.md-path__item:not(:first-child){align-items:center;display:inline-flex;gap:.2rem;white-space:nowrap}.md-path__item:not(:first-child):before{background-color:var(--md-default-fg-color--lighter);content:"";display:inline;height:.6rem;-webkit-mask-image:var(--md-path-icon);mask-image:var(--md-path-icon);width:.6rem}.md-path__link{align-items:center;color:var(--md-default-fg-color--light);display:flex;transition:color .25s}.md-path__link:focus,.md-path__link:hover{color:var(--md-accent-fg-color)}:root{--md-progress-value:0;--md-progress-delay:400ms}.md-progress{background:var(--md-primary-bg-color);height:.075rem;opacity:min(clamp(0,var(--md-progress-value),1),clamp(0,100 - var(--md-progress-value),1));position:fixed;top:0;transform:scaleX(calc(var(--md-progress-value)*1%));transform-origin:left;transition:transform .5s cubic-bezier(.19,1,.22,1),opacity .25s var(--md-progress-delay);width:100%;z-index:4}:root{--md-search-icon:url('data:image/svg+xml;charset=utf-8,')}.md-search{position:relative}@media screen and (min-width:45em){.md-search{padding:.2rem 0}}@media screen and (max-width:59.984375em){.md-search{display:none}}.no-js .md-search{display:none}[dir=ltr] .md-search__button{padding-left:1.9rem;padding-right:2.2rem}[dir=rtl] .md-search__button{padding-left:2.2rem;padding-right:1.9rem}.md-search__button{background:var(--md-default-bg-color);color:var(--md-default-fg-color);cursor:pointer;font-size:.7rem;position:relative;text-align:left}@media screen and (min-width:45em){.md-search__button{background-color:var(--md-default-fg-color--lightest);border-radius:.4rem;height:1.6rem;transition:background-color .4s,color .4s;width:8.9rem}.md-search__button:focus,.md-search__button:hover{background-color:var(--md-default-fg-color--lighter);color:var(--md-default-fg-color)}}[dir=ltr] .md-search__button:before{left:0}[dir=rtl] .md-search__button:before{right:0}.md-search__button:before{background-color:var(--md-default-fg-color);content:"";height:1rem;margin-left:.5rem;-webkit-mask-image:var(--md-search-icon);mask-image:var(--md-search-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.3rem;width:1rem}.md-search__button:after{background:var(--md-default-bg-color--light);border-radius:.2rem;content:"Ctrl+K";display:block;font-size:.6rem;padding:.1rem .2rem;position:absolute;right:.6rem;top:.35rem}[data-platform^=Mac] .md-search__button:after{content:"⌘K"}.md-select{position:relative;z-index:1}.md-select__inner{background-color:var(--md-default-bg-color);border-radius:.4rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color);left:50%;margin-top:.2rem;max-height:0;opacity:0;position:absolute;top:calc(100% - .2rem);transform:translate3d(-50%,.3rem,0);transition:transform .25s 375ms,opacity .25s .25s,max-height 0ms .5s}@media screen and (max-width:59.984375em){.md-select__inner{left:100%;transform:translate3d(-100%,.3rem,0)}}.md-select:focus-within .md-select__inner,.md-select:hover .md-select__inner{max-height:min(75vh,28rem);opacity:1;transform:translate3d(-50%,0,0);transition:transform .25s cubic-bezier(.1,.7,.1,1),opacity .25s,max-height 0ms}@media screen and (max-width:59.984375em){.md-select:focus-within .md-select__inner,.md-select:hover .md-select__inner{transform:translate3d(-100%,0,0)}}.md-select__inner:after{border-bottom:.2rem solid #0000;border-bottom-color:var(--md-default-bg-color);border-left:.2rem solid #0000;border-right:.2rem solid #0000;border-top:0;content:"";filter:drop-shadow(0 -1px 0 var(--md-default-fg-color--lightest));height:0;left:50%;margin-left:-.2rem;margin-top:-.2rem;position:absolute;top:0;width:0}@media screen and (max-width:59.984375em){.md-select__inner:after{left:auto;right:1rem}}.md-select__list{border-radius:.1rem;font-size:.8rem;list-style-type:none;margin:0;max-height:inherit;overflow:auto;padding:0}.md-select__item{line-height:1.8rem}[dir=ltr] .md-select__link{padding-left:.6rem;padding-right:1.2rem}[dir=rtl] .md-select__link{padding-left:1.2rem;padding-right:.6rem}.md-select__link{cursor:pointer;display:block;outline:none;scroll-snap-align:start;transition:background-color .25s,color .25s;width:100%}.md-select__link:focus,.md-select__link:hover{color:var(--md-accent-fg-color)}.md-select__link:focus{background-color:var(--md-default-fg-color--lightest)}:root{--md-toc-icon:url('data:image/svg+xml;charset=utf-8,')}.md-sidebar{align-self:flex-start;flex-shrink:0;padding:1.1rem 0;position:sticky;top:2.4rem;width:12.1rem}@media print{.md-sidebar{display:none}}@media screen and (max-width:76.234375em){[dir=ltr] .md-sidebar--primary{left:-12.1rem}[dir=rtl] .md-sidebar--primary{right:-12.1rem}.md-sidebar--primary{-webkit-backdrop-filter:blur(.4rem);backdrop-filter:blur(.4rem);background-color:var(--md-default-bg-color--light);border-radius:.8rem;display:block;height:calc(100% - .8rem);position:fixed;top:.4rem;transform:translateX(0);transition:transform .2s cubic-bezier(.5,0,.5,0),box-shadow .2s;width:12.1rem;z-index:5}[data-md-toggle=drawer]:checked~.md-container .md-sidebar--primary{box-shadow:var(--md-shadow-z3);transform:translateX(12.5rem);transition:transform .25s cubic-bezier(.7,.7,.1,1),box-shadow .25s}[dir=rtl] [data-md-toggle=drawer]:checked~.md-container .md-sidebar--primary{transform:translateX(-12.5rem)}.md-sidebar--primary .md-sidebar__scrollwrap{bottom:0;left:0;margin:0;overscroll-behavior-y:contain;position:absolute;right:0;top:0}}@media screen and (min-width:76.25em){.md-sidebar{height:0}.no-js .md-sidebar{height:auto}.md-header--lifted~.md-container .md-sidebar{top:4.8rem}}.md-sidebar--secondary{order:2}@media screen and (max-width:59.984375em){.md-sidebar--secondary{bottom:1.6rem;padding:0;position:fixed;right:.8rem;top:auto;width:auto;z-index:2}.md-sidebar--secondary .md-nav--secondary{margin-top:0}.md-sidebar--secondary .md-nav__title{padding:.55rem .6rem .35rem}.md-sidebar--secondary .md-sidebar__scrollwrap{display:flex;flex-direction:column-reverse;overflow-y:visible;position:relative}.md-sidebar--secondary .md-sidebar__inner{background-color:var(--md-default-bg-color);border-radius:.4rem;bottom:2.7rem;box-shadow:var(--md-shadow-z2);max-height:50vh;opacity:0;overflow-y:auto;padding-bottom:.4rem;pointer-events:none;position:absolute;right:0;transform:translateY(.4rem);transition:transform 0ms .25s,opacity .25s;width:11.7rem}.md-sidebar--secondary [type=checkbox]:checked~.md-sidebar__inner{opacity:1;pointer-events:auto;transform:translateY(0);transition:transform .4s cubic-bezier(0,1,.35,1),opacity .25s,z-index 0ms}.md-sidebar--secondary .md-sidebar-button{-webkit-backdrop-filter:blur(.4rem);backdrop-filter:blur(.4rem);background-color:var(--md-default-bg-color--light);border-radius:1.6rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color--light);cursor:pointer;display:inline-flex;font-size:.7rem;gap:.4rem;outline:none;padding:.5rem;transition:color 125ms,background-color 125ms,transform 125ms cubic-bezier(.4,0,.2,1),opacity 125ms}.md-sidebar--secondary .md-sidebar-button:after{background-color:currentcolor;content:"";display:block;height:.9rem;-webkit-mask-image:var(--md-toc-icon);mask-image:var(--md-toc-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:transform .25s;width:.9rem}.md-sidebar--secondary .md-sidebar-button:focus,.md-sidebar--secondary .md-sidebar-button:hover{background-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}.md-sidebar--secondary .md-sidebar-button__wrapper{text-align:right}}@media screen and (min-width:60em){.md-sidebar--secondary{height:0}.md-sidebar--secondary .md-sidebar-button{display:none}.no-js .md-sidebar--secondary{height:auto}.md-sidebar--secondary:not([hidden]){display:block}.md-sidebar--secondary .md-sidebar__scrollwrap{touch-action:pan-y}}.md-sidebar__scrollwrap{backface-visibility:hidden;overflow-y:auto;scrollbar-color:var(--md-default-fg-color--lighter) #0000}@media screen and (min-width:60em){.md-sidebar__scrollwrap{scrollbar-gutter:stable;scrollbar-width:thin}}.md-sidebar__scrollwrap::-webkit-scrollbar{height:.2rem;width:.2rem}.md-sidebar__scrollwrap:focus-within,.md-sidebar__scrollwrap:hover{scrollbar-color:var(--md-accent-fg-color) #0000}.md-sidebar__scrollwrap:focus-within::-webkit-scrollbar-thumb,.md-sidebar__scrollwrap:hover::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-sidebar__scrollwrap:focus-within::-webkit-scrollbar-thumb:hover,.md-sidebar__scrollwrap:hover::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}@supports selector(::-webkit-scrollbar){.md-sidebar__scrollwrap{scrollbar-gutter:auto}[dir=ltr] .md-sidebar__inner{padding-right:calc(100% - 11.5rem)}[dir=rtl] .md-sidebar__inner{padding-left:calc(100% - 11.5rem)}@media screen and (max-width:76.234375em){[dir=ltr] .md-sidebar__inner{padding-right:0}[dir=rtl] .md-sidebar__inner{padding-left:0}}}@media screen and (max-width:76.234375em){.md-overlay{-webkit-backdrop-filter:blur(.2rem);backdrop-filter:blur(.2rem);background-color:var(--md-default-bg-color--light);height:0;opacity:0;position:fixed;top:0;transition:width 0ms .5s,height 0ms .5s,opacity .25s 125ms;width:0;z-index:5}[data-md-toggle=drawer]:checked~.md-overlay{height:100%;opacity:1;transition:width 0ms,height 0ms,opacity .25s;width:100%}}@keyframes facts{0%{height:0}to{height:.65rem}}@keyframes fact{0%{opacity:0;transform:translateY(100%)}50%{opacity:0}to{opacity:1;transform:translateY(0)}}:root{--md-source-forks-icon:url('data:image/svg+xml;charset=utf-8,');--md-source-repositories-icon:url('data:image/svg+xml;charset=utf-8,');--md-source-stars-icon:url('data:image/svg+xml;charset=utf-8,');--md-source-version-icon:url('data:image/svg+xml;charset=utf-8,')}.md-source{backface-visibility:hidden;display:block;font-size:.55rem;line-height:1.2;outline-color:var(--md-accent-fg-color);transition:opacity .25s;white-space:nowrap}.md-source:hover{opacity:.7}.md-source__icon{display:inline-block;height:2.4rem;vertical-align:middle;width:2rem}[dir=ltr] .md-source__icon svg{margin-left:.6rem}[dir=rtl] .md-source__icon svg{margin-right:.6rem}.md-source__icon svg{margin-top:.6rem}.md-header .md-source__icon svg{height:1.2rem;width:1.2rem}[dir=ltr] .md-source__icon+.md-source__repository{padding-left:2rem}[dir=rtl] .md-source__icon+.md-source__repository{padding-right:2rem}[dir=ltr] .md-source__icon+.md-source__repository{margin-left:-2rem}[dir=rtl] .md-source__icon+.md-source__repository{margin-right:-2rem}[dir=ltr] .md-source__repository{margin-left:.6rem}[dir=rtl] .md-source__repository{margin-right:.6rem}.md-source__repository{display:inline-block;max-width:calc(100% - 1.2rem);overflow:hidden;text-overflow:ellipsis;vertical-align:middle}.md-source__facts{display:flex;font-size:.55rem;gap:.4rem;list-style-type:none;margin:.1rem 0 0;opacity:.75;overflow:hidden;padding:0;width:100%}.md-source__repository--active .md-source__facts{animation:facts 0ms ease-in}.md-source__fact{overflow:hidden;text-overflow:ellipsis}.md-source__repository--active .md-source__fact{animation:fact 0ms ease-out}[dir=ltr] .md-source__fact:before{margin-right:.1rem}[dir=rtl] .md-source__fact:before{margin-left:.1rem}.md-source__fact:before{background-color:currentcolor;content:"";display:inline-block;height:.6rem;-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;vertical-align:text-top;width:.6rem}.md-source__fact:nth-child(1n+2){flex-shrink:0}.md-source__fact--version:before{-webkit-mask-image:var(--md-source-version-icon);mask-image:var(--md-source-version-icon)}.md-source__fact--stars:before{-webkit-mask-image:var(--md-source-stars-icon);mask-image:var(--md-source-stars-icon)}.md-source__fact--forks:before{-webkit-mask-image:var(--md-source-forks-icon);mask-image:var(--md-source-forks-icon)}.md-source__fact--repositories:before{-webkit-mask-image:var(--md-source-repositories-icon);mask-image:var(--md-source-repositories-icon)}.md-source-file{margin:1em 0}[dir=ltr] .md-source-file__fact{margin-right:.6rem}[dir=rtl] .md-source-file__fact{margin-left:.6rem}.md-source-file__fact{align-items:center;color:var(--md-default-fg-color--light);display:inline-flex;font-size:.68rem;gap:.3rem}.md-source-file__fact .md-icon{flex-shrink:0;margin-bottom:.05rem}[dir=ltr] .md-source-file__fact .md-author{float:left}[dir=rtl] .md-source-file__fact .md-author{float:right}.md-source-file__fact .md-author{margin-right:.2rem}.md-source-file__fact svg{width:.9rem}:root{--md-status:url('data:image/svg+xml;charset=utf-8,');--md-status--new:url('data:image/svg+xml;charset=utf-8,');--md-status--deprecated:url('data:image/svg+xml;charset=utf-8,')}.md-status:after{background-color:var(--md-default-fg-color--light);content:"";display:inline-block;height:1.125em;-webkit-mask-image:var(--md-status);mask-image:var(--md-status);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;vertical-align:text-bottom;width:1.125em}.md-status:hover:after{background-color:currentcolor}.md-status--new:after{-webkit-mask-image:var(--md-status--new);mask-image:var(--md-status--new)}.md-status--deprecated:after{-webkit-mask-image:var(--md-status--deprecated);mask-image:var(--md-status--deprecated)}.md-tabs{box-shadow:0 -.05rem 0 inset var(--md-default-fg-color--lightest);color:var(--md-default-fg-color);display:block;line-height:1.3;overflow:auto;width:100%;z-index:2}@media print{.md-tabs{display:none}}@media screen and (max-width:76.234375em){.md-tabs{display:none}}.md-header--lifted .md-tabs{box-shadow:none;margin-bottom:-.05rem}.md-tabs[hidden]{pointer-events:none}[dir=ltr] .md-tabs__list{margin-left:.4rem}[dir=rtl] .md-tabs__list{margin-right:.4rem}.md-tabs__list{contain:content;display:flex;list-style:none;margin:0;overflow:auto;padding:0;scrollbar-width:none;white-space:nowrap}.md-tabs__list::-webkit-scrollbar{display:none}.md-tabs__item{height:2.4rem;padding-left:.6rem;padding-right:.6rem}.md-tabs__item--active{border-bottom:.05rem solid var(--md-default-fg-color);font-weight:500;position:relative;transition:border-bottom .25s}.md-tabs[hidden] .md-tabs__item--active{border-bottom:.05rem solid #0000}.md-tabs__item--active .md-tabs__link{color:inherit;opacity:1}.md-tabs__link{backface-visibility:hidden;display:flex;font-size:.7rem;margin-top:.8rem;opacity:.7;outline-color:var(--md-accent-fg-color);outline-offset:.2rem;transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .25s}.md-tabs__link:focus,.md-tabs__link:hover{color:inherit;opacity:1}[dir=ltr] .md-tabs__link svg{margin-right:.4rem}[dir=rtl] .md-tabs__link svg{margin-left:.4rem}.md-tabs__link svg{fill:currentcolor;height:1.3em}.md-tabs__item:nth-child(2) .md-tabs__link{transition-delay:20ms}.md-tabs__item:nth-child(3) .md-tabs__link{transition-delay:40ms}.md-tabs__item:nth-child(4) .md-tabs__link{transition-delay:60ms}.md-tabs__item:nth-child(5) .md-tabs__link{transition-delay:80ms}.md-tabs__item:nth-child(6) .md-tabs__link{transition-delay:.1s}.md-tabs__item:nth-child(7) .md-tabs__link{transition-delay:.12s}.md-tabs__item:nth-child(8) .md-tabs__link{transition-delay:.14s}.md-tabs__item:nth-child(9) .md-tabs__link{transition-delay:.16s}.md-tabs__item:nth-child(10) .md-tabs__link{transition-delay:.18s}.md-tabs__item:nth-child(11) .md-tabs__link{transition-delay:.2s}.md-tabs__item:nth-child(12) .md-tabs__link{transition-delay:.22s}.md-tabs__item:nth-child(13) .md-tabs__link{transition-delay:.24s}.md-tabs__item:nth-child(14) .md-tabs__link{transition-delay:.26s}.md-tabs__item:nth-child(15) .md-tabs__link{transition-delay:.28s}.md-tabs__item:nth-child(16) .md-tabs__link{transition-delay:.3s}.md-tabs[hidden] .md-tabs__link{opacity:0;transform:translateY(50%);transition:transform 0ms .1s,opacity .1s}:root{--md-tag-icon:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .md-tags:not([hidden]){display:flex;flex-wrap:wrap;gap:.5em;margin-bottom:1.2rem;margin-top:.8rem;padding-top:1.2rem}.md-typeset .md-tag{align-items:center;background:var(--md-default-fg-color--lightest);border-radius:.4rem;display:inline-flex;font-size:.64rem;font-size:min(.8em,.64rem);font-weight:700;gap:.5em;letter-spacing:normal;line-height:1.6;padding:.3125em .78125em}.md-typeset .md-tag[href]{-webkit-tap-highlight-color:transparent;color:inherit;outline:none;transition:color 125ms,background-color 125ms}.md-typeset .md-tag[href]:focus,.md-typeset .md-tag[href]:hover{background-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}[id]>.md-typeset .md-tag{vertical-align:text-top}.md-typeset .md-tag-shadow{opacity:.5}.md-typeset .md-tag-icon:before{background-color:var(--md-default-fg-color--lighter);content:"";display:inline-block;height:1.2em;-webkit-mask-image:var(--md-tag-icon);mask-image:var(--md-tag-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:background-color 125ms;vertical-align:text-bottom;width:1.2em}.md-typeset .md-tag-icon[href]:focus:before,.md-typeset .md-tag-icon[href]:hover:before{background-color:var(--md-accent-bg-color)}@keyframes pulse{0%{transform:scale(.95)}75%{transform:scale(1)}to{transform:scale(.95)}}:root{--md-annotation-bg-icon:url('data:image/svg+xml;charset=utf-8,');--md-annotation-icon:url('data:image/svg+xml;charset=utf-8,')}.md-tooltip{backface-visibility:hidden;background-color:var(--md-default-bg-color);border-radius:.4rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color);font-family:var(--md-text-font-family);left:clamp(var(--md-tooltip-0,0rem) + .8rem,var(--md-tooltip-x) - .1rem,100vw + var(--md-tooltip-0,0rem) + .8rem - var(--md-tooltip-width) - 2 * .8rem);max-width:calc(100vw - 1.6rem);opacity:0;position:absolute;top:calc(var(--md-tooltip-y) - .1rem);transform:translateY(-.4rem);transition:transform 0ms .25s,opacity .25s,z-index .25s;width:var(--md-tooltip-width);z-index:0}.md-tooltip--active{opacity:1;transform:translateY(0);transition:transform .25s cubic-bezier(.1,.7,.1,1),opacity .25s,z-index 0ms;z-index:2}.md-tooltip--inline{font-weight:400;-webkit-user-select:none;user-select:none;width:auto}.md-tooltip--inline:not(.md-tooltip--active){transform:translateY(.2rem) scale(.9)}.md-tooltip--inline .md-tooltip__inner{font-size:.55rem;padding:.2rem .4rem}[hidden]+.md-tooltip--inline{display:none}.focus-visible>.md-tooltip,.md-tooltip:target{outline:var(--md-accent-fg-color) auto}.md-tooltip__inner{font-size:.64rem;padding:.8rem}.md-tooltip__inner.md-typeset>:first-child{margin-top:0}.md-tooltip__inner.md-typeset>:last-child{margin-bottom:0}.md-annotation{font-style:normal;font-weight:400;outline:none;text-align:initial;vertical-align:text-bottom;white-space:normal}[dir=rtl] .md-annotation{direction:rtl}code .md-annotation{font-family:var(--md-code-font-family);font-size:inherit}.md-annotation:not([hidden]){display:inline-block;line-height:1.25}.md-annotation__index{border-radius:.01px;cursor:pointer;display:inline-block;margin-left:.4ch;margin-right:.4ch;outline:none;overflow:hidden;position:relative;-webkit-user-select:none;user-select:none;vertical-align:text-top;z-index:0}.md-annotation .md-annotation__index{transition:z-index .25s}@media screen{.md-annotation__index{width:2.2ch}[data-md-visible]>.md-annotation__index{animation:pulse 2s infinite}.md-annotation__index:before{background:var(--md-default-bg-color);-webkit-mask-image:var(--md-annotation-bg-icon);mask-image:var(--md-annotation-bg-icon)}.md-annotation__index:after,.md-annotation__index:before{content:"";height:2.2ch;-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:-.1ch;width:2.2ch;z-index:-1}.md-annotation__index:after{background-color:var(--md-default-fg-color--lighter);-webkit-mask-image:var(--md-annotation-icon);mask-image:var(--md-annotation-icon);transform:scale(1.0001);transition:background-color .25s,transform .25s}.md-tooltip--active+.md-annotation__index:after{transform:rotate(45deg)}.md-tooltip--active+.md-annotation__index:after,:hover>.md-annotation__index:after{background-color:var(--md-accent-fg-color)}}.md-tooltip--active+.md-annotation__index{animation-play-state:paused;transition-duration:0ms;z-index:2}.md-annotation__index [data-md-annotation-id]{display:inline-block}@media print{.md-annotation__index [data-md-annotation-id]{background:var(--md-default-fg-color--lighter);border-radius:2ch;color:var(--md-default-bg-color);font-weight:700;padding:0 .6ch;white-space:nowrap}.md-annotation__index [data-md-annotation-id]:after{content:attr(data-md-annotation-id)}}.md-typeset .md-annotation-list{counter-reset:annotation;list-style:none!important}.md-typeset .md-annotation-list li{position:relative}[dir=ltr] .md-typeset .md-annotation-list li:before{left:-2.125em}[dir=rtl] .md-typeset .md-annotation-list li:before{right:-2.125em}.md-typeset .md-annotation-list li:before{background:var(--md-default-fg-color--lighter);border-radius:2ch;color:var(--md-default-bg-color);content:counter(annotation);counter-increment:annotation;font-size:.8875em;font-weight:700;height:2ch;line-height:1.25;min-width:2ch;padding:0 .6ch;position:absolute;text-align:center;top:.25em}:root{--md-tooltip-width:20rem;--md-tooltip-tail:0.3rem}.md-tooltip2{backface-visibility:hidden;color:var(--md-default-fg-color);font-family:var(--md-text-font-family);opacity:0;pointer-events:none;position:absolute;top:calc(var(--md-tooltip-host-y) + var(--md-tooltip-y));transform:translateY(.4rem);transform-origin:calc(var(--md-tooltip-host-x) + var(--md-tooltip-x)) 0;transition:transform 0ms .25s,opacity .25s,z-index .25s;width:100%;z-index:0}.md-tooltip2:before{border-left:var(--md-tooltip-tail) solid #0000;border-right:var(--md-tooltip-tail) solid #0000;content:"";display:block;left:clamp(1.5 * .8rem,var(--md-tooltip-host-x) + var(--md-tooltip-x) - var(--md-tooltip-tail),100vw - 2 * var(--md-tooltip-tail) - 1.5 * .8rem);position:absolute;z-index:1}.md-tooltip2--top:before{border-top:var(--md-tooltip-tail) solid var(--md-default-bg-color);bottom:calc(var(--md-tooltip-tail)*-1 + .025rem);filter:drop-shadow(0 1px 0 var(--md-default-fg-color--lightest))}.md-tooltip2--bottom:before{border-bottom:var(--md-tooltip-tail) solid var(--md-default-bg-color);filter:drop-shadow(0 -1px 0 var(--md-default-fg-color--lightest));top:calc(var(--md-tooltip-tail)*-1 + .025rem)}.md-tooltip2--active{opacity:1;transform:translateY(0);transition:transform .4s cubic-bezier(0,1,.35,1),opacity .25s,z-index 0ms;z-index:4}.md-tooltip2__inner{scrollbar-gutter:stable;background-color:var(--md-default-bg-color);border-radius:.4rem;box-shadow:var(--md-shadow-z2);left:clamp(.8rem,var(--md-tooltip-host-x) - .8rem,100vw - var(--md-tooltip-width) - .8rem);max-height:40vh;max-width:calc(100vw - 1.6rem);position:relative;scrollbar-width:thin}.md-tooltip2__inner::-webkit-scrollbar{height:.2rem;width:.2rem}.md-tooltip2__inner::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-tooltip2__inner::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}[role=dialog]>.md-tooltip2__inner{font-size:.64rem;overflow:auto;padding:0 .8rem;pointer-events:auto;width:var(--md-tooltip-width)}[role=dialog]>.md-tooltip2__inner:after,[role=dialog]>.md-tooltip2__inner:before{content:"";display:block;height:.8rem;position:sticky;width:100%;z-index:10}[role=dialog]>.md-tooltip2__inner:before{background:linear-gradient(var(--md-default-bg-color),#0000 75%);top:0}[role=dialog]>.md-tooltip2__inner:after{background:linear-gradient(#0000,var(--md-default-bg-color) 75%);bottom:0}[role=tooltip]>.md-tooltip2__inner{font-size:.55rem;font-weight:400;left:clamp(.8rem,var(--md-tooltip-host-x) + var(--md-tooltip-x) - var(--md-tooltip-width)/2,100vw - var(--md-tooltip-width) - .8rem);max-width:min(100vw - 2 * .8rem,400px);padding:.2rem .4rem;-webkit-user-select:none;user-select:none;width:fit-content}.md-tooltip2__inner.md-typeset>:first-child{margin-top:0}.md-tooltip2__inner.md-typeset>:last-child{margin-bottom:0}[dir=ltr] .md-top{margin-left:50%}[dir=rtl] .md-top{margin-right:50%}.md-top{-webkit-backdrop-filter:blur(.4rem);backdrop-filter:blur(.4rem);background-color:var(--md-default-bg-color--light);border-radius:1.6rem;bottom:1.6rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color--light);cursor:pointer;display:flex;font-size:.7rem;gap:.4rem;outline:none;padding:.5rem .9rem .5rem .7rem;position:fixed;top:auto!important;transform:translate(-50%);transition:color 125ms,background-color 125ms,transform 125ms cubic-bezier(.4,0,.2,1),opacity 125ms;z-index:2}@media print{.md-top{display:none}}[dir=rtl] .md-top{transform:translate(50%)}.md-top[hidden]{opacity:0;pointer-events:none;transform:translate(-50%,.2rem);transition-duration:0ms}[dir=rtl] .md-top[hidden]{transform:translate(50%,.2rem)}.md-top:focus,.md-top:hover{background-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}.md-top svg{display:inline-block;height:.9rem;vertical-align:-.5em;width:.9rem}.md-top svg.lucide{fill:#0000;stroke:currentcolor}@keyframes hoverfix{0%{pointer-events:none}}:root{--md-version-icon:url('data:image/svg+xml;charset=utf-8,')}.md-version{flex-shrink:0;font-size:.8rem;height:2.4rem}[dir=ltr] .md-version__current{margin-left:1.4rem;margin-right:.4rem}[dir=rtl] .md-version__current{margin-left:.4rem;margin-right:1.4rem}.md-version__current{color:inherit;cursor:pointer;outline:none;position:relative;top:.05rem}[dir=ltr] .md-version__current:after{margin-left:.4rem}[dir=rtl] .md-version__current:after{margin-right:.4rem}.md-version__current:after{background-color:currentcolor;content:"";display:inline-block;height:.6rem;-webkit-mask-image:var(--md-version-icon);mask-image:var(--md-version-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:.4rem}.md-version__alias{margin-left:.3rem;opacity:.7}.md-version__list{background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color);list-style-type:none;margin:.2rem .8rem;max-height:0;opacity:0;overflow:auto;padding:0;position:absolute;scroll-snap-type:y mandatory;top:.15rem;transition:max-height 0ms .5s,opacity .25s .25s;z-index:3}.md-version:focus-within .md-version__list,.md-version:hover .md-version__list{max-height:10rem;opacity:1;transition:max-height 0ms,opacity .25s}@media (hover:none),(pointer:coarse){.md-version:hover .md-version__list{animation:hoverfix .25s forwards}.md-version:focus-within .md-version__list{animation:none}}.md-version__item{line-height:1.8rem}[dir=ltr] .md-version__link{padding-left:.6rem;padding-right:1.2rem}[dir=rtl] .md-version__link{padding-left:1.2rem;padding-right:.6rem}.md-version__link{cursor:pointer;display:block;outline:none;scroll-snap-align:start;transition:color .25s,background-color .25s;white-space:nowrap;width:100%}.md-version__link:focus,.md-version__link:hover{color:var(--md-accent-fg-color)}.md-version__link:focus{background-color:var(--md-default-fg-color--lightest)}:root{--md-admonition-icon--note:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--abstract:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--info:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--tip:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--success:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--question:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--warning:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--failure:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--danger:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--bug:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--example:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--quote:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .admonition,.md-typeset details{background-color:#448aff1a;border-radius:.4rem;color:var(--md-admonition-fg-color);display:flow-root;font-size:.64rem;margin:1.5625em 0;padding:0 .8rem;page-break-inside:avoid}.md-typeset .admonition>*,.md-typeset details>*{box-sizing:border-box}.md-typeset .admonition .admonition,.md-typeset .admonition details,.md-typeset details .admonition,.md-typeset details details{margin-bottom:1em;margin-top:1em}.md-typeset .admonition .md-typeset__scrollwrap,.md-typeset details .md-typeset__scrollwrap{margin:1em -.6rem}.md-typeset .admonition .md-typeset__table,.md-typeset details .md-typeset__table{padding:0 .6rem}.md-typeset .admonition>.tabbed-set:only-child,.md-typeset details>.tabbed-set:only-child{margin-top:0}html .md-typeset .admonition>:last-child,html .md-typeset details>:last-child{margin-bottom:.6rem}[dir=ltr] .md-typeset .admonition-title,[dir=ltr] .md-typeset summary{padding-left:1.6rem;padding-right:.8rem}[dir=rtl] .md-typeset .admonition-title,[dir=rtl] .md-typeset summary{padding-left:.8rem;padding-right:1.6rem}.md-typeset .admonition-title,.md-typeset summary{font-weight:700;margin-bottom:1em;margin-top:.6rem;position:relative}[dir=ltr] .md-typeset .admonition-title:before,[dir=ltr] .md-typeset summary:before{left:0}[dir=rtl] .md-typeset .admonition-title:before,[dir=rtl] .md-typeset summary:before{right:0}.md-typeset .admonition-title:before,.md-typeset summary:before{background-color:#448aff;content:"";height:1rem;-webkit-mask-image:var(--md-admonition-icon--note);mask-image:var(--md-admonition-icon--note);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.125em;width:1rem}.md-typeset .admonition.note,.md-typeset details.note{background-color:#448aff1a}.md-typeset .note>.admonition-title:before,.md-typeset .note>summary:before{background-color:#448aff;-webkit-mask-image:var(--md-admonition-icon--note);mask-image:var(--md-admonition-icon--note)}.md-typeset .note>.admonition-title:after,.md-typeset .note>summary:after{color:#448aff}.md-typeset .admonition.abstract,.md-typeset details.abstract{background-color:#00b0ff1a}.md-typeset .abstract>.admonition-title:before,.md-typeset .abstract>summary:before{background-color:#00b0ff;-webkit-mask-image:var(--md-admonition-icon--abstract);mask-image:var(--md-admonition-icon--abstract)}.md-typeset .abstract>.admonition-title:after,.md-typeset .abstract>summary:after{color:#00b0ff}.md-typeset .admonition.info,.md-typeset details.info{background-color:#00b8d41a}.md-typeset .info>.admonition-title:before,.md-typeset .info>summary:before{background-color:#00b8d4;-webkit-mask-image:var(--md-admonition-icon--info);mask-image:var(--md-admonition-icon--info)}.md-typeset .info>.admonition-title:after,.md-typeset .info>summary:after{color:#00b8d4}.md-typeset .admonition.tip,.md-typeset details.tip{background-color:#00bfa51a}.md-typeset .tip>.admonition-title:before,.md-typeset .tip>summary:before{background-color:#00bfa5;-webkit-mask-image:var(--md-admonition-icon--tip);mask-image:var(--md-admonition-icon--tip)}.md-typeset .tip>.admonition-title:after,.md-typeset .tip>summary:after{color:#00bfa5}.md-typeset .admonition.success,.md-typeset details.success{background-color:#00c8531a}.md-typeset .success>.admonition-title:before,.md-typeset .success>summary:before{background-color:#00c853;-webkit-mask-image:var(--md-admonition-icon--success);mask-image:var(--md-admonition-icon--success)}.md-typeset .success>.admonition-title:after,.md-typeset .success>summary:after{color:#00c853}.md-typeset .admonition.question,.md-typeset details.question{background-color:#64dd171a}.md-typeset .question>.admonition-title:before,.md-typeset .question>summary:before{background-color:#64dd17;-webkit-mask-image:var(--md-admonition-icon--question);mask-image:var(--md-admonition-icon--question)}.md-typeset .question>.admonition-title:after,.md-typeset .question>summary:after{color:#64dd17}.md-typeset .admonition.warning,.md-typeset details.warning{background-color:#ff91001a}.md-typeset .warning>.admonition-title:before,.md-typeset .warning>summary:before{background-color:#ff9100;-webkit-mask-image:var(--md-admonition-icon--warning);mask-image:var(--md-admonition-icon--warning)}.md-typeset .warning>.admonition-title:after,.md-typeset .warning>summary:after{color:#ff9100}.md-typeset .admonition.failure,.md-typeset details.failure{background-color:#ff52521a}.md-typeset .failure>.admonition-title:before,.md-typeset .failure>summary:before{background-color:#ff5252;-webkit-mask-image:var(--md-admonition-icon--failure);mask-image:var(--md-admonition-icon--failure)}.md-typeset .failure>.admonition-title:after,.md-typeset .failure>summary:after{color:#ff5252}.md-typeset .admonition.danger,.md-typeset details.danger{background-color:#ff17441a}.md-typeset .danger>.admonition-title:before,.md-typeset .danger>summary:before{background-color:#ff1744;-webkit-mask-image:var(--md-admonition-icon--danger);mask-image:var(--md-admonition-icon--danger)}.md-typeset .danger>.admonition-title:after,.md-typeset .danger>summary:after{color:#ff1744}.md-typeset .admonition.bug,.md-typeset details.bug{background-color:#f500571a}.md-typeset .bug>.admonition-title:before,.md-typeset .bug>summary:before{background-color:#f50057;-webkit-mask-image:var(--md-admonition-icon--bug);mask-image:var(--md-admonition-icon--bug)}.md-typeset .bug>.admonition-title:after,.md-typeset .bug>summary:after{color:#f50057}.md-typeset .admonition.example,.md-typeset details.example{background-color:#7c4dff1a}.md-typeset .example>.admonition-title:before,.md-typeset .example>summary:before{background-color:#7c4dff;-webkit-mask-image:var(--md-admonition-icon--example);mask-image:var(--md-admonition-icon--example)}.md-typeset .example>.admonition-title:after,.md-typeset .example>summary:after{color:#7c4dff}.md-typeset .admonition.quote,.md-typeset details.quote{background-color:#9e9e9e1a}.md-typeset .quote>.admonition-title:before,.md-typeset .quote>summary:before{background-color:#9e9e9e;-webkit-mask-image:var(--md-admonition-icon--quote);mask-image:var(--md-admonition-icon--quote)}.md-typeset .quote>.admonition-title:after,.md-typeset .quote>summary:after{color:#9e9e9e}:root{--md-footnotes-icon:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .footnote{color:var(--md-default-fg-color--light);font-size:.64rem}[dir=ltr] .md-typeset .footnote>ol{margin-left:0}[dir=rtl] .md-typeset .footnote>ol{margin-right:0}.md-typeset .footnote>ol>li{transition:color 125ms}.md-typeset .footnote>ol>li:target{color:var(--md-default-fg-color)}.md-typeset .footnote>ol>li:focus-within .footnote-backref{opacity:1;transform:translateY(0);transition:none}.md-typeset .footnote>ol>li:hover .footnote-backref,.md-typeset .footnote>ol>li:target .footnote-backref{opacity:1;transform:translateY(0)}.md-typeset .footnote>ol>li>:first-child{margin-top:0}.md-typeset .footnote-ref{font-size:.75em;font-weight:700;text-decoration:none}html .md-typeset .footnote-ref{outline-offset:.1rem}.md-typeset [id^="fnref:"]:target>.footnote-ref{outline:auto}.md-typeset .footnote-backref{color:var(--md-typeset-a-color);display:inline-block;font-size:0;opacity:0;transform:translateY(.25rem);transition:color .25s,transform .25s .25s,opacity 125ms .25s;vertical-align:text-bottom}@media print{.md-typeset .footnote-backref{color:var(--md-typeset-a-color);opacity:1;transform:translateY(0)}}[dir=rtl] .md-typeset .footnote-backref{transform:translateY(-.25rem)}.md-typeset .footnote-backref:hover{color:var(--md-accent-fg-color)}.md-typeset .footnote-backref:before{background-color:currentcolor;content:"";display:inline-block;height:.8rem;-webkit-mask-image:var(--md-footnotes-icon);mask-image:var(--md-footnotes-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:.8rem}[dir=rtl] .md-typeset .footnote-backref:before{transform:scaleX(-1)}[dir=ltr] .md-typeset .headerlink{margin-left:.5rem}[dir=rtl] .md-typeset .headerlink{margin-right:.5rem}.md-typeset .headerlink{color:var(--md-default-fg-color--lighter);display:inline-block;opacity:0;text-decoration:none;transition:color .25s,opacity 125ms}@media print{.md-typeset .headerlink{display:none}}.md-typeset .headerlink:focus,.md-typeset :hover>.headerlink,.md-typeset :target>.headerlink{opacity:1;transition:color .25s,opacity 125ms}.md-typeset .headerlink:focus,.md-typeset .headerlink:hover,.md-typeset :target>.headerlink{color:var(--md-accent-fg-color)}.md-typeset :target{--md-scroll-margin:3.6rem;--md-scroll-offset:0rem;scroll-margin-top:calc(var(--md-scroll-margin) - var(--md-scroll-offset))}@media screen and (min-width:76.25em){.md-header--lifted~.md-container .md-typeset :target{--md-scroll-margin:6rem}}.md-typeset h1:target{--md-scroll-offset:0.1rem}.md-typeset h3:target,.md-typeset h4:target{--md-scroll-offset:-0.1rem}:root{--md-admonition-icon--mkdocstrings:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--mkdocstrings-open:url('data:image/svg+xml;charset=utf-8,')}.doc-object-name{font-family:var(--md-code-font-family)}code.doc-symbol-heading{margin-right:.4rem;padding:0}[dir=ltr] .doc-labels{margin-left:.4rem}[dir=rtl] .doc-labels{margin-right:.4rem}.doc-label code{background:#0000;border:1px solid var(--md-default-fg-color--lightest);border-radius:.5rem;color:var(--md-default-fg-color--light);font-weight:400;padding-left:.3rem;padding-right:.3rem;vertical-align:text-bottom}.doc-contents td code{word-break:normal!important}.doc-md-description,.doc-md-description>p:first-child{display:inline}.md-typeset h5 .doc-object-name{text-transform:none}.doc .md-typeset__table,.doc .md-typeset__table table{display:table!important;width:100%}.doc .md-typeset__table tr{display:table-row}.doc-param-default,.doc-type_param-default{float:right}.doc-heading-parameter,.doc-heading-type_parameter{display:inline}.md-typeset .doc-heading-parameter{font-size:inherit}.doc-heading-parameter .headerlink,.doc-heading-type_parameter .headerlink{margin-left:0!important;margin-right:.2rem}.doc-section-title{font-weight:700}.doc-signature .autorefs{color:inherit;text-decoration-style:dotted}div.doc-contents:not(.first){border-left:.05rem solid var(--md-code-bg-color);margin-left:.4rem;padding-left:.8rem}:host,:root,[data-md-color-scheme=default]{--doc-symbol-parameter-fg-color:#829bd1;--doc-symbol-type_parameter-fg-color:#829bd1;--doc-symbol-attribute-fg-color:#953800;--doc-symbol-function-fg-color:#8250df;--doc-symbol-method-fg-color:#8250df;--doc-symbol-class-fg-color:#0550ae;--doc-symbol-type_alias-fg-color:#0550ae;--doc-symbol-module-fg-color:#5cad0f}[data-md-color-scheme=slate]{--doc-symbol-parameter-fg-color:#829bd1;--doc-symbol-type_parameter-fg-color:#829bd1;--doc-symbol-attribute-fg-color:#ffa657;--doc-symbol-function-fg-color:#d2a8ff;--doc-symbol-method-fg-color:#d2a8ff;--doc-symbol-class-fg-color:#79c0ff;--doc-symbol-type_alias-fg-color:#79c0ff;--doc-symbol-module-fg-color:#baff79}.md-ellipsis:has(.doc-symbol){font-family:var(--md-code-font-family);font-size:.95em}code.doc-symbol{background-color:initial;border-radius:.1rem;font-size:1em;font-weight:400}a code.doc-symbol-parameter,code.doc-symbol-parameter{color:var(--doc-symbol-parameter-fg-color)}.md-content code.doc-symbol-parameter:after{content:"param"}.md-sidebar code.doc-symbol-parameter:after{content:"p"}a code.doc-symbol-type_parameter,code.doc-symbol-type_parameter{color:var(--doc-symbol-type_parameter-fg-color)}.md-content code.doc-symbol-type_parameter:after{content:"type-param"}.md-sidebar code.doc-symbol-type_parameter:after{content:"t"}a code.doc-symbol-attribute,code.doc-symbol-attribute{color:var(--doc-symbol-attribute-fg-color)}.md-content code.doc-symbol-attribute:after{content:"attribute"}.md-sidebar code.doc-symbol-attribute:after{content:"a"}a code.doc-symbol-function,code.doc-symbol-function{color:var(--doc-symbol-function-fg-color)}.md-content code.doc-symbol-function:after{content:"function"}.md-sidebar code.doc-symbol-function:after{content:"f"}a code.doc-symbol-method,code.doc-symbol-method{color:var(--doc-symbol-method-fg-color)}.md-content code.doc-symbol-method:after{content:"method"}.md-sidebar code.doc-symbol-method:after{content:"m"}a code.doc-symbol-class,code.doc-symbol-class{color:var(--doc-symbol-class-fg-color)}.md-content code.doc-symbol-class:after{content:"class"}.md-sidebar code.doc-symbol-class:after{content:"c"}a code.doc-symbol-type_alias,code.doc-symbol-type_alias{color:var(--doc-symbol-type_alias-fg-color)}.md-content code.doc-symbol-type_alias:after{content:"type"}.md-sidebar code.doc-symbol-type_alias:after{content:"t"}a code.doc-symbol-module,code.doc-symbol-module{color:var(--doc-symbol-module-fg-color)}.md-content code.doc-symbol-module:after{content:"module"}.md-sidebar code.doc-symbol-module:after{content:"mod"}.md-typeset details.mkdocstrings-source{background:#0000;border:.05rem solid var(--md-code-bg-color)}.md-typeset details.mkdocstrings-source>summary:before{background-color:var(--md-default-fg-color--light);-webkit-mask-image:var(--md-admonition-icon--mkdocstrings);mask-image:var(--md-admonition-icon--mkdocstrings)}.md-typeset details.mkdocstrings-source[open]>summary:before{-webkit-mask-image:var(--md-admonition-icon--mkdocstrings-open);mask-image:var(--md-admonition-icon--mkdocstrings-open)}.md-typeset details.mkdocstrings-source>summary:after{background-color:var(--md-default-fg-color--light)}.md-typeset div.arithmatex{overflow:auto}@media screen and (max-width:44.984375em){.md-typeset div.arithmatex{margin:0 -.8rem}.md-typeset div.arithmatex>*{width:min-content}}.md-typeset div.arithmatex>*{margin-left:auto!important;margin-right:auto!important;padding:0 .8rem;touch-action:auto}.md-typeset div.arithmatex>* mjx-container{margin:0!important}.md-typeset div.arithmatex mjx-assistive-mml{height:0}.md-typeset del.critic{background-color:var(--md-typeset-del-color)}.md-typeset del.critic,.md-typeset ins.critic{-webkit-box-decoration-break:clone;box-decoration-break:clone}.md-typeset ins.critic{background-color:var(--md-typeset-ins-color)}.md-typeset .critic.comment{-webkit-box-decoration-break:clone;box-decoration-break:clone;color:var(--md-code-hl-comment-color)}.md-typeset .critic.comment:before{content:"/* "}.md-typeset .critic.comment:after{content:" */"}.md-typeset .critic.block{box-shadow:none;display:block;margin:1em 0;overflow:auto;padding-left:.8rem;padding-right:.8rem}.md-typeset .critic.block>:first-child{margin-top:.5em}.md-typeset .critic.block>:last-child{margin-bottom:.5em}:root{--md-details-icon:url('data:image/svg+xml;charset=utf-8,')}.md-typeset details{display:flow-root;overflow:visible;padding-top:0}.md-typeset details[open]>summary:after{transform:rotate(90deg)}.md-typeset details:not([open]){box-shadow:none;padding-bottom:0}.md-typeset details:not([open])>summary{border-radius:.1rem;margin-bottom:.6rem}[dir=ltr] .md-typeset summary{padding-right:1.6rem}[dir=rtl] .md-typeset summary{padding-left:1.6rem}[dir=ltr] .md-typeset summary{border-top-left-radius:.1rem}[dir=ltr] .md-typeset summary,[dir=rtl] .md-typeset summary{border-top-right-radius:.1rem}[dir=rtl] .md-typeset summary{border-top-left-radius:.1rem}.md-typeset summary{cursor:pointer;display:block;min-height:1rem;overflow:hidden}.md-typeset summary.focus-visible{outline-color:var(--md-accent-fg-color);outline-offset:.2rem}.md-typeset summary:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}[dir=ltr] .md-typeset summary:after{right:0}[dir=rtl] .md-typeset summary:after{left:0}.md-typeset summary:after{background-color:currentcolor;content:"";height:1rem;-webkit-mask-image:var(--md-details-icon);mask-image:var(--md-details-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.125em;transform:rotate(0deg);transition:transform .25s;width:1rem}[dir=rtl] .md-typeset summary:after{transform:rotate(180deg)}.md-typeset summary::marker{display:none}.md-typeset summary::-webkit-details-marker{display:none}.md-typeset .emojione,.md-typeset .gemoji,.md-typeset .twemoji{--md-icon-size:1.125em;display:inline-flex;height:var(--md-icon-size);vertical-align:text-top}.md-typeset .emojione svg,.md-typeset .gemoji svg,.md-typeset .twemoji svg{fill:currentcolor;max-height:100%;width:var(--md-icon-size)}.md-typeset .emojione svg.lucide,.md-typeset .gemoji svg.lucide,.md-typeset .twemoji svg.lucide{fill:#0000;stroke:currentcolor}.md-typeset .lg,.md-typeset .xl,.md-typeset .xxl,.md-typeset .xxxl{vertical-align:text-bottom}.md-typeset .middle{vertical-align:middle}.md-typeset .lg{--md-icon-size:1.5em}.md-typeset .xl{--md-icon-size:2.25em}.md-typeset .xxl{--md-icon-size:3em}.md-typeset .xxxl{--md-icon-size:4em}.highlight .o,.highlight .ow{color:var(--md-code-hl-operator-color)}.highlight .p{color:var(--md-code-hl-punctuation-color)}.highlight .cpf,.highlight .l,.highlight .s,.highlight .s1,.highlight .s2,.highlight .sb,.highlight .sc,.highlight .si,.highlight .ss{color:var(--md-code-hl-string-color)}.highlight .cp,.highlight .se,.highlight .sh,.highlight .sr,.highlight .sx{color:var(--md-code-hl-special-color)}.highlight .il,.highlight .m,.highlight .mb,.highlight .mf,.highlight .mh,.highlight .mi,.highlight .mo{color:var(--md-code-hl-number-color)}.highlight .k,.highlight .kd,.highlight .kn,.highlight .kp,.highlight .kr,.highlight .kt{color:var(--md-code-hl-keyword-color)}.highlight .kc,.highlight .n{color:var(--md-code-hl-name-color)}.highlight .bp,.highlight .nb,.highlight .no{color:var(--md-code-hl-constant-color)}.highlight .nc,.highlight .ne,.highlight .nf,.highlight .nn{color:var(--md-code-hl-function-color)}.highlight .nd,.highlight .ni,.highlight .nl,.highlight .nt{color:var(--md-code-hl-keyword-color)}.highlight .c,.highlight .c1,.highlight .ch,.highlight .cm,.highlight .cs,.highlight .sd{color:var(--md-code-hl-comment-color)}.highlight .na,.highlight .nv,.highlight .vc,.highlight .vg,.highlight .vi{color:var(--md-code-hl-variable-color)}.highlight .ge,.highlight .gh,.highlight .go,.highlight .gp,.highlight .gr,.highlight .gs,.highlight .gt,.highlight .gu{color:var(--md-code-hl-generic-color)}.highlight .gd,.highlight .gi{border-radius:.1rem;margin:0 -.125em;padding:0 .125em}.highlight .gd{background-color:var(--md-typeset-del-color)}.highlight .gi{background-color:var(--md-typeset-ins-color)}.highlight .hll{background-color:var(--md-code-hl-color--light);box-shadow:2px 0 0 0 var(--md-code-hl-color) inset;display:block;margin:0 -1.1764705882em;padding:0 1.1764705882em}.highlight span.filename{background-color:var(--md-code-bg-color);border-bottom:.05rem solid var(--md-default-fg-color--lightest);border-top-left-radius:.4rem;border-top-right-radius:.4rem;display:flow-root;font-size:.85em;font-weight:700;margin-top:1em;padding:.6617647059em 1.1764705882em;position:relative}.highlight span.filename+pre{margin-top:0}.highlight span.filename+pre>code{border-top-left-radius:0;border-top-right-radius:0}.highlight [data-linenos]:before{background-color:var(--md-code-bg-color);box-shadow:-.05rem 0 var(--md-default-fg-color--lightest) inset;color:var(--md-default-fg-color--light);content:attr(data-linenos);float:left;left:-1.1764705882em;margin-left:-1.1764705882em;margin-right:1.1764705882em;padding-left:1.1764705882em;position:sticky;-webkit-user-select:none;user-select:none;z-index:3}.highlight code>span[id^=__span]>:last-child .md-annotation{margin-right:2.4rem}.highlight code[data-md-copying]{display:initial}.highlight code[data-md-copying] .hll{display:contents}.highlight code[data-md-copying] .md-annotation{display:none}.highlighttable{display:flow-root}.highlighttable tbody,.highlighttable td{display:block;padding:0}.highlighttable tr{display:flex}.highlighttable pre{margin:0}.highlighttable th.filename{flex-grow:1;padding:0;text-align:left}.highlighttable th.filename span.filename{margin-top:0}.highlighttable .linenos{background-color:var(--md-code-bg-color);border-bottom-left-radius:.4rem;border-top-left-radius:.4rem;font-size:.85em;padding:.7720588235em 0 .7720588235em 1.1764705882em;-webkit-user-select:none;user-select:none}.highlighttable .linenodiv{box-shadow:-.05rem 0 var(--md-default-fg-color--lightest) inset}.highlighttable .linenodiv pre{color:var(--md-default-fg-color--light);text-align:right}.highlighttable .linenodiv span[class]{padding-right:.5882352941em}.highlighttable .code{flex:1;min-width:0}.linenodiv a{color:inherit;text-decoration:none}.md-typeset .highlighttable{direction:ltr;margin:1em 0}.md-typeset .highlighttable>tbody>tr>.code>div>pre>code{border-bottom-left-radius:0;border-top-left-radius:0}.md-typeset .highlight+.result{border:.05rem solid var(--md-code-bg-color);border-bottom-left-radius:.4rem;border-bottom-right-radius:.4rem;border-top-width:.4rem;margin-top:-1.5em;overflow:visible;padding:0 1em}.md-typeset .highlight+.result:after{clear:both;content:"";display:block}@media screen and (max-width:44.984375em){.md-content__inner>.highlight{margin:1em -.8rem}.md-content__inner>.highlight>.filename,.md-content__inner>.highlight>.highlighttable>tbody>tr>.code>div>pre>code,.md-content__inner>.highlight>.highlighttable>tbody>tr>.filename span.filename,.md-content__inner>.highlight>.highlighttable>tbody>tr>.linenos,.md-content__inner>.highlight>pre>code{border-radius:0}.md-content__inner>.highlight+.result{border-left-width:0;border-radius:0;border-right-width:0;margin-left:-.8rem;margin-right:-.8rem}}.md-typeset .keys kbd:after,.md-typeset .keys kbd:before{-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;color:inherit;margin:0;position:relative}.md-typeset .keys span{color:var(--md-default-fg-color--light);padding:0 .2em}.md-typeset .keys .key-alt:before,.md-typeset .keys .key-left-alt:before,.md-typeset .keys .key-right-alt:before{content:"⎇";padding-right:.4em}.md-typeset .keys .key-command:before,.md-typeset .keys .key-left-command:before,.md-typeset .keys .key-right-command:before{content:"⌘";padding-right:.4em}.md-typeset .keys .key-control:before,.md-typeset .keys .key-left-control:before,.md-typeset .keys .key-right-control:before{content:"⌃";padding-right:.4em}.md-typeset .keys .key-left-meta:before,.md-typeset .keys .key-meta:before,.md-typeset .keys .key-right-meta:before{content:"◆";padding-right:.4em}.md-typeset .keys .key-left-option:before,.md-typeset .keys .key-option:before,.md-typeset .keys .key-right-option:before{content:"⌥";padding-right:.4em}.md-typeset .keys .key-left-shift:before,.md-typeset .keys .key-right-shift:before,.md-typeset .keys .key-shift:before{content:"⇧";padding-right:.4em}.md-typeset .keys .key-left-super:before,.md-typeset .keys .key-right-super:before,.md-typeset .keys .key-super:before{content:"❖";padding-right:.4em}.md-typeset .keys .key-left-windows:before,.md-typeset .keys .key-right-windows:before,.md-typeset .keys .key-windows:before{content:"⊞";padding-right:.4em}.md-typeset .keys .key-arrow-down:before{content:"↓";padding-right:.4em}.md-typeset .keys .key-arrow-left:before{content:"←";padding-right:.4em}.md-typeset .keys .key-arrow-right:before{content:"→";padding-right:.4em}.md-typeset .keys .key-arrow-up:before{content:"↑";padding-right:.4em}.md-typeset .keys .key-backspace:before{content:"⌫";padding-right:.4em}.md-typeset .keys .key-backtab:before{content:"⇤";padding-right:.4em}.md-typeset .keys .key-caps-lock:before{content:"⇪";padding-right:.4em}.md-typeset .keys .key-clear:before{content:"⌧";padding-right:.4em}.md-typeset .keys .key-context-menu:before{content:"☰";padding-right:.4em}.md-typeset .keys .key-delete:before{content:"⌦";padding-right:.4em}.md-typeset .keys .key-eject:before{content:"⏏";padding-right:.4em}.md-typeset .keys .key-end:before{content:"⤓";padding-right:.4em}.md-typeset .keys .key-escape:before{content:"⎋";padding-right:.4em}.md-typeset .keys .key-home:before{content:"⤒";padding-right:.4em}.md-typeset .keys .key-insert:before{content:"⎀";padding-right:.4em}.md-typeset .keys .key-page-down:before{content:"⇟";padding-right:.4em}.md-typeset .keys .key-page-up:before{content:"⇞";padding-right:.4em}.md-typeset .keys .key-print-screen:before{content:"⎙";padding-right:.4em}.md-typeset .keys .key-tab:after{content:"⇥";padding-left:.4em}.md-typeset .keys .key-num-enter:after{content:"⌤";padding-left:.4em}.md-typeset .keys .key-enter:after{content:"⏎";padding-left:.4em}:root{--md-tabbed-icon--prev:url('data:image/svg+xml;charset=utf-8,');--md-tabbed-icon--next:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .tabbed-set{border-radius:.075rem;display:flex;flex-flow:column wrap;margin:1em 0;position:relative}.md-typeset .tabbed-set>input{height:0;opacity:0;position:absolute;width:0}.md-typeset .tabbed-set>input:target{--md-scroll-offset:0.625em}.md-typeset .tabbed-set>input.focus-visible~.tabbed-labels:before{background-color:var(--md-accent-fg-color)}.md-typeset .tabbed-labels{-ms-overflow-style:none;box-shadow:0 -.05rem var(--md-default-fg-color--lightest) inset;display:flex;max-width:100%;overflow:auto;scrollbar-width:none}@media print{.md-typeset .tabbed-labels{display:contents}}@media screen{.js .md-typeset .tabbed-labels{position:relative}.js .md-typeset .tabbed-labels:before{background:var(--md-default-fg-color);bottom:0;content:"";display:block;height:1.5px;left:0;position:absolute;transform:translateX(var(--md-indicator-x));transition:width 225ms,background-color .25s,transform .25s;transition-timing-function:cubic-bezier(.4,0,.2,1);width:var(--md-indicator-width)}}.md-typeset .tabbed-labels::-webkit-scrollbar{display:none}.md-typeset .tabbed-labels>label{border-bottom:.1rem solid #0000;border-radius:.1rem .1rem 0 0;color:var(--md-default-fg-color--light);cursor:pointer;flex-shrink:0;font-size:.7rem;font-weight:400;padding:.78125em 1.25em .625em;scroll-margin-inline-start:1rem;transition:background-color .25s,color .25s;white-space:nowrap;width:auto}@media print{.md-typeset .tabbed-labels>label:first-child{order:1}.md-typeset .tabbed-labels>label:nth-child(2){order:2}.md-typeset .tabbed-labels>label:nth-child(3){order:3}.md-typeset .tabbed-labels>label:nth-child(4){order:4}.md-typeset .tabbed-labels>label:nth-child(5){order:5}.md-typeset .tabbed-labels>label:nth-child(6){order:6}.md-typeset .tabbed-labels>label:nth-child(7){order:7}.md-typeset .tabbed-labels>label:nth-child(8){order:8}.md-typeset .tabbed-labels>label:nth-child(9){order:9}.md-typeset .tabbed-labels>label:nth-child(10){order:10}.md-typeset .tabbed-labels>label:nth-child(11){order:11}.md-typeset .tabbed-labels>label:nth-child(12){order:12}.md-typeset .tabbed-labels>label:nth-child(13){order:13}.md-typeset .tabbed-labels>label:nth-child(14){order:14}.md-typeset .tabbed-labels>label:nth-child(15){order:15}.md-typeset .tabbed-labels>label:nth-child(16){order:16}.md-typeset .tabbed-labels>label:nth-child(17){order:17}.md-typeset .tabbed-labels>label:nth-child(18){order:18}.md-typeset .tabbed-labels>label:nth-child(19){order:19}.md-typeset .tabbed-labels>label:nth-child(20){order:20}}.md-typeset .tabbed-labels>label:hover{color:var(--md-default-fg-color)}.md-typeset .tabbed-labels>label>[href]:first-child{color:inherit;text-decoration:none}.md-typeset .tabbed-labels--linked>label{padding:0}.md-typeset .tabbed-labels--linked>label>a{display:block;padding:.78125em 1.25em .625em}.md-typeset .tabbed-content{width:100%}@media print{.md-typeset .tabbed-content{display:contents}}.md-typeset .tabbed-block{display:none}@media print{.md-typeset .tabbed-block{display:block}.md-typeset .tabbed-block:first-child{order:1}.md-typeset .tabbed-block:nth-child(2){order:2}.md-typeset .tabbed-block:nth-child(3){order:3}.md-typeset .tabbed-block:nth-child(4){order:4}.md-typeset .tabbed-block:nth-child(5){order:5}.md-typeset .tabbed-block:nth-child(6){order:6}.md-typeset .tabbed-block:nth-child(7){order:7}.md-typeset .tabbed-block:nth-child(8){order:8}.md-typeset .tabbed-block:nth-child(9){order:9}.md-typeset .tabbed-block:nth-child(10){order:10}.md-typeset .tabbed-block:nth-child(11){order:11}.md-typeset .tabbed-block:nth-child(12){order:12}.md-typeset .tabbed-block:nth-child(13){order:13}.md-typeset .tabbed-block:nth-child(14){order:14}.md-typeset .tabbed-block:nth-child(15){order:15}.md-typeset .tabbed-block:nth-child(16){order:16}.md-typeset .tabbed-block:nth-child(17){order:17}.md-typeset .tabbed-block:nth-child(18){order:18}.md-typeset .tabbed-block:nth-child(19){order:19}.md-typeset .tabbed-block:nth-child(20){order:20}}.md-typeset .tabbed-block>.highlight:first-child>pre,.md-typeset .tabbed-block>pre:first-child{margin:0}.md-typeset .tabbed-block>.highlight:first-child>pre>code,.md-typeset .tabbed-block>pre:first-child>code{border-top-left-radius:0;border-top-right-radius:0}.md-typeset .tabbed-block>.highlight:first-child>.filename{border-top-left-radius:0;border-top-right-radius:0;margin:0}.md-typeset .tabbed-block>.highlight:first-child>.highlighttable{margin:0}.md-typeset .tabbed-block>.highlight:first-child>.highlighttable>tbody>tr>.filename span.filename,.md-typeset .tabbed-block>.highlight:first-child>.highlighttable>tbody>tr>.linenos{border-top-left-radius:0;border-top-right-radius:0;margin:0}.md-typeset .tabbed-block>.highlight:first-child>.highlighttable>tbody>tr>.code>div>pre>code{border-top-left-radius:0;border-top-right-radius:0}.md-typeset .tabbed-block>.highlight:first-child+.result{margin-top:-.125em}.md-typeset .tabbed-block>.tabbed-set{margin:0}.md-typeset .tabbed-button{align-self:center;-webkit-backdrop-filter:blur(.4rem);backdrop-filter:blur(.4rem);background-color:var(--md-default-bg-color--light);border-radius:100%;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color--light);cursor:pointer;display:block;height:.9rem;margin-top:.4rem;pointer-events:auto;transition:transform 125ms;width:.9rem}.md-typeset .tabbed-button:hover{transform:scale(1.125)}.md-typeset .tabbed-button:after{background-color:currentcolor;content:"";display:block;height:100%;-webkit-mask-image:var(--md-tabbed-icon--prev);mask-image:var(--md-tabbed-icon--prev);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:background-color .25s,transform .25s;width:100%}.md-typeset .tabbed-control{display:flex;height:1.9rem;justify-content:start;pointer-events:none;position:absolute;transition:opacity 125ms;width:1.2rem}[dir=rtl] .md-typeset .tabbed-control{transform:rotate(180deg)}.md-typeset .tabbed-control[hidden]{opacity:0}.md-typeset .tabbed-control--next{justify-content:end;right:0}.md-typeset .tabbed-control--next .tabbed-button:after{-webkit-mask-image:var(--md-tabbed-icon--next);mask-image:var(--md-tabbed-icon--next)}@media screen and (max-width:44.984375em){[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels{padding-left:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels{padding-right:.8rem}.md-content__inner>.tabbed-set .tabbed-labels{margin:0 -.8rem;max-width:100vw;scroll-padding-inline-start:.8rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels:after{padding-right:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels:after{padding-left:.8rem}.md-content__inner>.tabbed-set .tabbed-labels:after{content:""}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{padding-left:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{padding-right:.8rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{margin-left:-.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{margin-right:-.8rem}.md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{width:2rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{padding-right:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{padding-left:.8rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{margin-right:-.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{margin-left:-.8rem}.md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{width:2rem}}@media screen{.md-typeset .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,.md-typeset .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),.md-typeset .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),.md-typeset .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),.md-typeset .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),.md-typeset .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),.md-typeset .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),.md-typeset .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),.md-typeset .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),.md-typeset .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),.md-typeset .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),.md-typeset .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),.md-typeset .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),.md-typeset .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),.md-typeset .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),.md-typeset .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),.md-typeset .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),.md-typeset .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),.md-typeset .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),.md-typeset .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9){color:var(--md-default-fg-color);font-weight:500}.md-typeset .no-js .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,.md-typeset .no-js .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),.md-typeset .no-js .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),.md-typeset .no-js .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),.md-typeset .no-js .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),.md-typeset .no-js .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),.md-typeset .no-js .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),.md-typeset .no-js .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),.md-typeset .no-js .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),.md-typeset .no-js .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),.md-typeset .no-js .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),.md-typeset .no-js .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),.md-typeset .no-js .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),.md-typeset .no-js .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),.md-typeset .no-js .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),.md-typeset .no-js .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),.md-typeset .no-js .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),.md-typeset .no-js .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),.md-typeset .no-js .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),.md-typeset .no-js .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9),.md-typeset [role=dialog] .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,.md-typeset [role=dialog] .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),.md-typeset [role=dialog] .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),.md-typeset [role=dialog] .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),.md-typeset [role=dialog] .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),.md-typeset [role=dialog] .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),.md-typeset [role=dialog] .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),.md-typeset [role=dialog] .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),.md-typeset [role=dialog] .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),.md-typeset [role=dialog] .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),.md-typeset [role=dialog] .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),.md-typeset [role=dialog] .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),.md-typeset [role=dialog] .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),.md-typeset [role=dialog] .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),.md-typeset [role=dialog] .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),.md-typeset [role=dialog] .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),.md-typeset [role=dialog] .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),.md-typeset [role=dialog] .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),.md-typeset [role=dialog] .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),.md-typeset [role=dialog] .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9),.no-js .md-typeset .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,.no-js .md-typeset .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),.no-js .md-typeset .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),.no-js .md-typeset .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),.no-js .md-typeset .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),.no-js .md-typeset .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),.no-js .md-typeset .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),.no-js .md-typeset .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),.no-js .md-typeset .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),.no-js .md-typeset .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),.no-js .md-typeset .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),.no-js .md-typeset .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),.no-js .md-typeset .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),.no-js .md-typeset .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),.no-js .md-typeset .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),.no-js .md-typeset .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),.no-js .md-typeset .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),.no-js .md-typeset .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),.no-js .md-typeset .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),.no-js .md-typeset .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9),[role=dialog] .md-typeset .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,[role=dialog] .md-typeset .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),[role=dialog] .md-typeset .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),[role=dialog] .md-typeset .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),[role=dialog] .md-typeset .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),[role=dialog] .md-typeset .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),[role=dialog] .md-typeset .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),[role=dialog] .md-typeset .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),[role=dialog] .md-typeset .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),[role=dialog] .md-typeset .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),[role=dialog] .md-typeset .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),[role=dialog] .md-typeset .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),[role=dialog] .md-typeset .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),[role=dialog] .md-typeset .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),[role=dialog] .md-typeset .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),[role=dialog] .md-typeset .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),[role=dialog] .md-typeset .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),[role=dialog] .md-typeset .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),[role=dialog] .md-typeset .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),[role=dialog] .md-typeset .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9){border-color:var(--md-default-fg-color)}}.md-typeset .tabbed-set>input:first-child.focus-visible~.tabbed-labels>:first-child,.md-typeset .tabbed-set>input:nth-child(10).focus-visible~.tabbed-labels>:nth-child(10),.md-typeset .tabbed-set>input:nth-child(11).focus-visible~.tabbed-labels>:nth-child(11),.md-typeset .tabbed-set>input:nth-child(12).focus-visible~.tabbed-labels>:nth-child(12),.md-typeset .tabbed-set>input:nth-child(13).focus-visible~.tabbed-labels>:nth-child(13),.md-typeset .tabbed-set>input:nth-child(14).focus-visible~.tabbed-labels>:nth-child(14),.md-typeset .tabbed-set>input:nth-child(15).focus-visible~.tabbed-labels>:nth-child(15),.md-typeset .tabbed-set>input:nth-child(16).focus-visible~.tabbed-labels>:nth-child(16),.md-typeset .tabbed-set>input:nth-child(17).focus-visible~.tabbed-labels>:nth-child(17),.md-typeset .tabbed-set>input:nth-child(18).focus-visible~.tabbed-labels>:nth-child(18),.md-typeset .tabbed-set>input:nth-child(19).focus-visible~.tabbed-labels>:nth-child(19),.md-typeset .tabbed-set>input:nth-child(2).focus-visible~.tabbed-labels>:nth-child(2),.md-typeset .tabbed-set>input:nth-child(20).focus-visible~.tabbed-labels>:nth-child(20),.md-typeset .tabbed-set>input:nth-child(3).focus-visible~.tabbed-labels>:nth-child(3),.md-typeset .tabbed-set>input:nth-child(4).focus-visible~.tabbed-labels>:nth-child(4),.md-typeset .tabbed-set>input:nth-child(5).focus-visible~.tabbed-labels>:nth-child(5),.md-typeset .tabbed-set>input:nth-child(6).focus-visible~.tabbed-labels>:nth-child(6),.md-typeset .tabbed-set>input:nth-child(7).focus-visible~.tabbed-labels>:nth-child(7),.md-typeset .tabbed-set>input:nth-child(8).focus-visible~.tabbed-labels>:nth-child(8),.md-typeset .tabbed-set>input:nth-child(9).focus-visible~.tabbed-labels>:nth-child(9){color:var(--md-accent-fg-color)}.md-typeset .tabbed-set>input:first-child:checked~.tabbed-content>:first-child,.md-typeset .tabbed-set>input:nth-child(10):checked~.tabbed-content>:nth-child(10),.md-typeset .tabbed-set>input:nth-child(11):checked~.tabbed-content>:nth-child(11),.md-typeset .tabbed-set>input:nth-child(12):checked~.tabbed-content>:nth-child(12),.md-typeset .tabbed-set>input:nth-child(13):checked~.tabbed-content>:nth-child(13),.md-typeset .tabbed-set>input:nth-child(14):checked~.tabbed-content>:nth-child(14),.md-typeset .tabbed-set>input:nth-child(15):checked~.tabbed-content>:nth-child(15),.md-typeset .tabbed-set>input:nth-child(16):checked~.tabbed-content>:nth-child(16),.md-typeset .tabbed-set>input:nth-child(17):checked~.tabbed-content>:nth-child(17),.md-typeset .tabbed-set>input:nth-child(18):checked~.tabbed-content>:nth-child(18),.md-typeset .tabbed-set>input:nth-child(19):checked~.tabbed-content>:nth-child(19),.md-typeset .tabbed-set>input:nth-child(2):checked~.tabbed-content>:nth-child(2),.md-typeset .tabbed-set>input:nth-child(20):checked~.tabbed-content>:nth-child(20),.md-typeset .tabbed-set>input:nth-child(3):checked~.tabbed-content>:nth-child(3),.md-typeset .tabbed-set>input:nth-child(4):checked~.tabbed-content>:nth-child(4),.md-typeset .tabbed-set>input:nth-child(5):checked~.tabbed-content>:nth-child(5),.md-typeset .tabbed-set>input:nth-child(6):checked~.tabbed-content>:nth-child(6),.md-typeset .tabbed-set>input:nth-child(7):checked~.tabbed-content>:nth-child(7),.md-typeset .tabbed-set>input:nth-child(8):checked~.tabbed-content>:nth-child(8),.md-typeset .tabbed-set>input:nth-child(9):checked~.tabbed-content>:nth-child(9){display:block}:root{--md-tasklist-icon:url('data:image/svg+xml;charset=utf-8,');--md-tasklist-icon--checked:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .task-list-item{list-style-type:none;position:relative}[dir=ltr] .md-typeset .task-list-item [type=checkbox]{left:-2em}[dir=rtl] .md-typeset .task-list-item [type=checkbox]{right:-2em}.md-typeset .task-list-item [type=checkbox]{position:absolute;top:.45em}.md-typeset .task-list-control [type=checkbox]{opacity:0;z-index:-1}[dir=ltr] .md-typeset .task-list-indicator:before{left:-1.5em}[dir=rtl] .md-typeset .task-list-indicator:before{right:-1.5em}.md-typeset .task-list-indicator:before{background-color:var(--md-default-fg-color--lighter);content:"";height:1.25em;-webkit-mask-image:var(--md-tasklist-icon);mask-image:var(--md-tasklist-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.25em;width:1.25em}.md-typeset [type=checkbox]:checked+.task-list-indicator:before{background-color:#00e676;-webkit-mask-image:var(--md-tasklist-icon--checked);mask-image:var(--md-tasklist-icon--checked)}@media print{.giscus,[id=__comments]{display:none}}:root>*{--md-mermaid-font-family:var(--md-text-font-family),sans-serif;--md-mermaid-edge-color:var(--md-code-fg-color);--md-mermaid-node-bg-color:var(--md-accent-fg-color--transparent);--md-mermaid-node-fg-color:var(--md-accent-fg-color);--md-mermaid-label-bg-color:var(--md-default-bg-color);--md-mermaid-label-fg-color:var(--md-code-fg-color);--md-mermaid-sequence-actor-bg-color:var(--md-mermaid-label-bg-color);--md-mermaid-sequence-actor-fg-color:var(--md-mermaid-label-fg-color);--md-mermaid-sequence-actor-border-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-actor-line-color:var(--md-default-fg-color--lighter);--md-mermaid-sequence-actorman-bg-color:var(--md-mermaid-label-bg-color);--md-mermaid-sequence-actorman-line-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-box-bg-color:var(--md-mermaid-node-bg-color);--md-mermaid-sequence-box-fg-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-label-bg-color:var(--md-mermaid-node-bg-color);--md-mermaid-sequence-label-fg-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-loop-bg-color:var(--md-mermaid-node-bg-color);--md-mermaid-sequence-loop-fg-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-loop-border-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-message-fg-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-message-line-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-note-bg-color:var(--md-mermaid-label-bg-color);--md-mermaid-sequence-note-fg-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-note-border-color:var(--md-mermaid-label-fg-color);--md-mermaid-sequence-number-bg-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-number-fg-color:var(--md-accent-bg-color)}.mermaid{line-height:normal;margin:1em 0}.md-typeset .grid{grid-gap:.4rem;display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,16rem),1fr));margin:1em 0}.md-typeset .grid.cards>ol,.md-typeset .grid.cards>ul{display:contents}.md-typeset .grid.cards>ol>li,.md-typeset .grid.cards>ul>li,.md-typeset .grid>.card{border:.05rem solid var(--md-default-fg-color--lightest);border-radius:.4rem;display:block;margin:0;padding:.8rem;transition:background-color .25s,border .25s,box-shadow .25s}.md-typeset .grid.cards>ol>li:focus-within,.md-typeset .grid.cards>ol>li:hover,.md-typeset .grid.cards>ul>li:focus-within,.md-typeset .grid.cards>ul>li:hover,.md-typeset .grid>.card:focus-within,.md-typeset .grid>.card:hover{border-color:#0000;box-shadow:var(--md-shadow-z2)}.md-typeset .grid.cards>ol>li>hr,.md-typeset .grid.cards>ul>li>hr,.md-typeset .grid>.card>hr{margin-bottom:1em;margin-top:1em}.md-typeset .grid.cards>ol>li>:first-child,.md-typeset .grid.cards>ul>li>:first-child,.md-typeset .grid>.card>:first-child{margin-top:0}.md-typeset .grid.cards>ol>li>:last-child,.md-typeset .grid.cards>ul>li>:last-child,.md-typeset .grid>.card>:last-child{margin-bottom:0}.md-typeset .grid>*,.md-typeset .grid>.admonition,.md-typeset .grid>.highlight>*,.md-typeset .grid>.highlighttable,.md-typeset .grid>.md-typeset details,.md-typeset .grid>details,.md-typeset .grid>pre{margin-bottom:0;margin-top:0}.md-typeset .grid>.highlight>pre:only-child,.md-typeset .grid>.highlight>pre>code,.md-typeset .grid>.highlighttable,.md-typeset .grid>.highlighttable>tbody,.md-typeset .grid>.highlighttable>tbody>tr,.md-typeset .grid>.highlighttable>tbody>tr>.code,.md-typeset .grid>.highlighttable>tbody>tr>.code>.highlight,.md-typeset .grid>.highlighttable>tbody>tr>.code>.highlight>pre,.md-typeset .grid>.highlighttable>tbody>tr>.code>.highlight>pre>code{height:100%}.md-typeset .grid>.tabbed-set{margin-bottom:0;margin-top:0}@media screen and (min-width:45em){[dir=ltr] .md-typeset .inline{float:left}[dir=rtl] .md-typeset .inline{float:right}[dir=ltr] .md-typeset .inline{margin-right:.8rem}[dir=rtl] .md-typeset .inline{margin-left:.8rem}.md-typeset .inline{margin-bottom:.8rem;margin-top:0;width:11.7rem}[dir=ltr] .md-typeset .inline.end{float:right}[dir=rtl] .md-typeset .inline.end{float:left}[dir=ltr] .md-typeset .inline.end{margin-left:.8rem;margin-right:0}[dir=rtl] .md-typeset .inline.end{margin-left:0;margin-right:.8rem}} \ No newline at end of file diff --git a/docs/site/assets/stylesheets/modern/palette.dfe2e883.min.css b/docs/site/assets/stylesheets/modern/palette.dfe2e883.min.css deleted file mode 100644 index d58a561..0000000 --- a/docs/site/assets/stylesheets/modern/palette.dfe2e883.min.css +++ /dev/null @@ -1 +0,0 @@ -@media screen{[data-md-color-scheme=slate]{--md-default-fg-color:hsla(var(--md-hue),15%,90%,0.82);--md-default-fg-color--light:hsla(var(--md-hue),15%,90%,0.56);--md-default-fg-color--lighter:hsla(var(--md-hue),15%,90%,0.32);--md-default-fg-color--lightest:hsla(var(--md-hue),15%,90%,0.12);--md-default-bg-color:hsla(var(--md-hue),15%,5%,1);--md-default-bg-color--light:hsla(var(--md-hue),15%,5%,0.54);--md-default-bg-color--lighter:hsla(var(--md-hue),15%,5%,0.26);--md-default-bg-color--lightest:hsla(var(--md-hue),15%,5%,0.07);--md-code-fg-color:hsla(var(--md-hue),20%,80%,1);--md-code-bg-color:hsla(var(--md-hue),20%,10%,1);--md-code-bg-color--light:hsla(var(--md-hue),20%,10%,0.9);--md-code-bg-color--lighter:hsla(var(--md-hue),20%,10%,0.54);--md-code-hl-color:#2977ff;--md-code-hl-color--light:#2977ff1a;--md-code-hl-number-color:#e6695b;--md-code-hl-special-color:#f06090;--md-code-hl-function-color:#c973d9;--md-code-hl-constant-color:#9383e2;--md-code-hl-keyword-color:#6791e0;--md-code-hl-string-color:#2fb170;--md-code-hl-name-color:var(--md-code-fg-color);--md-code-hl-operator-color:var(--md-default-fg-color--light);--md-code-hl-punctuation-color:var(--md-default-fg-color--light);--md-code-hl-comment-color:var(--md-default-fg-color--light);--md-code-hl-generic-color:var(--md-default-fg-color--light);--md-code-hl-variable-color:var(--md-default-fg-color--light);--md-typeset-color:var(--md-default-fg-color);--md-typeset-a-color:var(--md-primary-fg-color);--md-typeset-kbd-color:hsla(var(--md-hue),15%,90%,0.12);--md-typeset-kbd-accent-color:hsla(var(--md-hue),15%,90%,0.2);--md-typeset-kbd-border-color:hsla(var(--md-hue),15%,14%,1);--md-typeset-mark-color:#4287ff4d;--md-typeset-table-color:hsla(var(--md-hue),15%,95%,0.12);--md-typeset-table-color--light:hsla(var(--md-hue),15%,95%,0.035);--md-admonition-fg-color:var(--md-default-fg-color);--md-admonition-bg-color:var(--md-default-bg-color);--md-footer-bg-color:hsla(var(--md-hue),15%,10%,0.87);--md-footer-bg-color--dark:hsla(var(--md-hue),15%,8%,1);--md-shadow-z1:0 0.2rem 0.5rem #0000000d,0 0 0.05rem #ffffff1a;--md-shadow-z2:0 0.2rem 0.5rem #00000040,0 0 0.05rem #ffffff59;--md-shadow-z3:0 0.5rem 2rem #0006,0 0 0.05rem #00000059;color-scheme:dark}[data-md-color-scheme=slate] .md-header__title,[data-md-color-scheme=slate] h1,[data-md-color-scheme=slate] h2,[data-md-color-scheme=slate] h3,[data-md-color-scheme=slate] h4,[data-md-color-scheme=slate] h5,[data-md-color-scheme=slate] h6{color:hsla(var(--md-hue),0%,100%,1)}[data-md-color-scheme=slate] img[src$="#gh-light-mode-only"],[data-md-color-scheme=slate] img[src$="#only-light"]{display:none}[data-md-color-scheme=slate]{--color-foreground:255 255 255;--color-background:22 23 26;--color-background-subtle:33 34 38;--color-backdrop:11 12 15}[data-md-color-scheme=slate][data-md-color-primary=pink]{--md-typeset-a-color:#ed5487}[data-md-color-scheme=slate][data-md-color-primary=purple]{--md-typeset-a-color:#c46fd3}[data-md-color-scheme=slate][data-md-color-primary=deep-purple]{--md-typeset-a-color:#a47bea}[data-md-color-scheme=slate][data-md-color-primary=indigo]{--md-typeset-a-color:#5488e8}[data-md-color-scheme=slate][data-md-color-primary=teal]{--md-typeset-a-color:#00ccb8}[data-md-color-scheme=slate][data-md-color-primary=green]{--md-typeset-a-color:#71c174}[data-md-color-scheme=slate][data-md-color-primary=deep-orange]{--md-typeset-a-color:#ff764d}[data-md-color-scheme=slate][data-md-color-primary=brown]{--md-typeset-a-color:#c1775c}[data-md-color-scheme=slate][data-md-color-primary=black],[data-md-color-scheme=slate][data-md-color-primary=blue-grey],[data-md-color-scheme=slate][data-md-color-primary=grey],[data-md-color-scheme=slate][data-md-color-primary=white]{--md-typeset-a-color:#5e8bde}[data-md-color-switching] *,[data-md-color-switching] :after,[data-md-color-switching] :before{transition-duration:0ms!important}}[data-md-color-accent=red]{--md-accent-fg-color:#ff1947;--md-accent-fg-color--transparent:#ff19471a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=pink]{--md-accent-fg-color:#f50056;--md-accent-fg-color--transparent:#f500561a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=purple]{--md-accent-fg-color:#df41fb;--md-accent-fg-color--transparent:#df41fb1a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=deep-purple]{--md-accent-fg-color:#7c4dff;--md-accent-fg-color--transparent:#7c4dff1a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=indigo]{--md-accent-fg-color:#526cfe;--md-accent-fg-color--transparent:#526cfe1a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=blue]{--md-accent-fg-color:#4287ff;--md-accent-fg-color--transparent:#4287ff1a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=light-blue]{--md-accent-fg-color:#0091eb;--md-accent-fg-color--transparent:#0091eb1a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=cyan]{--md-accent-fg-color:#00bad6;--md-accent-fg-color--transparent:#00bad61a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=teal]{--md-accent-fg-color:#00bda4;--md-accent-fg-color--transparent:#00bda41a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=green]{--md-accent-fg-color:#00c753;--md-accent-fg-color--transparent:#00c7531a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=light-green]{--md-accent-fg-color:#63de17;--md-accent-fg-color--transparent:#63de171a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-accent=lime]{--md-accent-fg-color:#b0eb00;--md-accent-fg-color--transparent:#b0eb001a;--md-accent-bg-color:#000000de;--md-accent-bg-color--light:#0000008a}[data-md-color-accent=yellow]{--md-accent-fg-color:#ffd500;--md-accent-fg-color--transparent:#ffd5001a;--md-accent-bg-color:#000000de;--md-accent-bg-color--light:#0000008a}[data-md-color-accent=amber]{--md-accent-fg-color:#fa0;--md-accent-fg-color--transparent:#ffaa001a;--md-accent-bg-color:#000000de;--md-accent-bg-color--light:#0000008a}[data-md-color-accent=orange]{--md-accent-fg-color:#ff9100;--md-accent-fg-color--transparent:#ff91001a;--md-accent-bg-color:#000000de;--md-accent-bg-color--light:#0000008a}[data-md-color-accent=deep-orange]{--md-accent-fg-color:#ff6e42;--md-accent-fg-color--transparent:#ff6e421a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-primary=red]{--md-primary-fg-color:#ef5552;--md-primary-fg-color--light:#e57171;--md-primary-fg-color--dark:#e53734;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=pink]{--md-primary-fg-color:#e92063;--md-primary-fg-color--light:#ec417a;--md-primary-fg-color--dark:#c3185d;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=purple]{--md-primary-fg-color:#ab47bd;--md-primary-fg-color--light:#bb69c9;--md-primary-fg-color--dark:#8c24a8;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=deep-purple]{--md-primary-fg-color:#7e56c2;--md-primary-fg-color--light:#9574cd;--md-primary-fg-color--dark:#673ab6;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=indigo]{--md-primary-fg-color:#4051b5;--md-primary-fg-color--light:#5d6cc0;--md-primary-fg-color--dark:#303fa1;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=blue]{--md-primary-fg-color:#2094f3;--md-primary-fg-color--light:#42a5f5;--md-primary-fg-color--dark:#1975d2;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=light-blue]{--md-primary-fg-color:#02a6f2;--md-primary-fg-color--light:#28b5f6;--md-primary-fg-color--dark:#0287cf;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=cyan]{--md-primary-fg-color:#00bdd6;--md-primary-fg-color--light:#25c5da;--md-primary-fg-color--dark:#0097a8;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=teal]{--md-primary-fg-color:#009485;--md-primary-fg-color--light:#26a699;--md-primary-fg-color--dark:#007a6c;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=green]{--md-primary-fg-color:#4cae4f;--md-primary-fg-color--light:#68bb6c;--md-primary-fg-color--dark:#398e3d;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=light-green]{--md-primary-fg-color:#8bc34b;--md-primary-fg-color--light:#9ccc66;--md-primary-fg-color--dark:#689f38;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=lime]{--md-primary-fg-color:#cbdc38;--md-primary-fg-color--light:#d3e156;--md-primary-fg-color--dark:#b0b52c;--md-primary-bg-color:#000000de;--md-primary-bg-color--light:#0000008a}[data-md-color-primary=yellow]{--md-primary-fg-color:#ffec3d;--md-primary-fg-color--light:#ffee57;--md-primary-fg-color--dark:#fbc02d;--md-primary-bg-color:#000000de;--md-primary-bg-color--light:#0000008a}[data-md-color-primary=amber]{--md-primary-fg-color:#ffc105;--md-primary-fg-color--light:#ffc929;--md-primary-fg-color--dark:#ffa200;--md-primary-bg-color:#000000de;--md-primary-bg-color--light:#0000008a}[data-md-color-primary=orange]{--md-primary-fg-color:#ffa724;--md-primary-fg-color--light:#ffa724;--md-primary-fg-color--dark:#fa8900;--md-primary-bg-color:#000000de;--md-primary-bg-color--light:#0000008a}[data-md-color-primary=deep-orange]{--md-primary-fg-color:#ff6e42;--md-primary-fg-color--light:#ff8a66;--md-primary-fg-color--dark:#f4511f;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=brown]{--md-primary-fg-color:#795649;--md-primary-fg-color--light:#8d6e62;--md-primary-fg-color--dark:#5d4037;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3}[data-md-color-primary=grey]{--md-primary-fg-color:#757575;--md-primary-fg-color--light:#9e9e9e;--md-primary-fg-color--dark:#616161;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3;--md-typeset-a-color:#4051b5}[data-md-color-primary=blue-grey]{--md-primary-fg-color:#546d78;--md-primary-fg-color--light:#607c8a;--md-primary-fg-color--dark:#455a63;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3;--md-typeset-a-color:#4051b5}[data-md-color-primary=light-green]:not([data-md-color-scheme=slate]){--md-typeset-a-color:#72ad2e}[data-md-color-primary=lime]:not([data-md-color-scheme=slate]){--md-typeset-a-color:#8b990a}[data-md-color-primary=yellow]:not([data-md-color-scheme=slate]){--md-typeset-a-color:#b8a500}[data-md-color-primary=amber]:not([data-md-color-scheme=slate]){--md-typeset-a-color:#d19d00}[data-md-color-primary=orange]:not([data-md-color-scheme=slate]){--md-typeset-a-color:#e68a00} \ No newline at end of file diff --git a/docs/site/components/discovery/index.html b/docs/site/components/discovery/index.html deleted file mode 100644 index 7d5a871..0000000 --- a/docs/site/components/discovery/index.html +++ /dev/null @@ -1,2027 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Discovery - Cortex - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - -
- - -
- -
- - - - - - - - - -
-
- - - -
-
-
- - - - - - - -
-
-
- - - -
-
- - - - - - - - -
- -
- - - -
-
- - - -
- -
- - - - - - -

Discovery

-
-

Source: cortex.discovery.daemon, -cortex.discovery.client, -cortex.discovery.protocol

-
-

Discovery is Cortex's control plane: a single long-lived process that maps -topic names to ZMQ endpoints. It sits off the data path — once a subscriber -has an endpoint, messages flow publisher → subscriber directly without the -daemon's involvement.

-

Moving parts

-
flowchart LR
-    subgraph DP[discovery package]
-        PR[protocol.py<br/>DiscoveryRequest /<br/>DiscoveryResponse /<br/>TopicInfo]
-        DM[daemon.py<br/>DiscoveryDaemon<br/>ZMQ REP loop]
-        CL[client.py<br/>DiscoveryClient<br/>ZMQ REQ wrapper]
-    end
-
-    CL -- msgpack REQ --> DM
-    DM -- msgpack REP --> CL
-    PR -.-> DM
-    PR -.-> CL
-

Everyone agrees on the wire format via protocol.py. The daemon runs a -single-threaded REP loop. The client speaks REQ from every publisher and -subscriber in the graph.

-

Daemon

-

Implemented in [DiscoveryDaemon][cortex.discovery.daemon.DiscoveryDaemon].

-

Key behaviors:

-
    -
  • Binds zmq.REP at ipc:///tmp/cortex/discovery.sock by default.
  • -
  • Maintains _topics: dict[str, TopicInfo]one publisher per topic.
  • -
  • RCVTIMEO=1000 on the socket so the loop can check _running for clean - Ctrl-C. This also means the daemon is naturally single-request-at-a-time — - a slow client blocks all others.
  • -
-

State transitions

-
stateDiagram-v2
-    [*] --> Starting
-    Starting --> Running: bind OK
-    Running --> Running: REGISTER → insert
-    Running --> Running: LOOKUP → read
-    Running --> Running: UNREGISTER → delete
-    Running --> Running: LIST → snapshot
-    Running --> Stopping: SIGINT / SHUTDOWN
-    Stopping --> [*]: close socket, unlink .sock
-

Registry semantics

- - - - - - - - - - - - - - - - - - - - - - - - - -
CaseResult
New topicInsert → OK
Same topic, same publisher_nodeOverwrite → OK (re-registration)
Same topic, different publisher_nodeReject → ALREADY_EXISTS
UNREGISTER missing topicNOT_FOUND
-

Client

-

Implemented in [DiscoveryClient][cortex.discovery.client.DiscoveryClient].

-

Thin REQ wrapper around the protocol. Important operational detail: REQ -sockets stick after a timeout — they block subsequent sends waiting for a -reply that never came. The client handles this by closing and recreating the -socket on every timeout (_reconnect). Callers don't see it.

-

REQ timeout recovery

-
flowchart TD
-    S[send request] --> W[wait RCVTIMEO]
-    W -->|reply| OK[return DiscoveryResponse]
-    W -->|timeout| T[zmq.Again]
-    T --> C[close REQ socket]
-    C --> N[create fresh REQ<br/>same endpoint]
-    N -->|attempts < retries| S
-    N -->|exhausted| F[raise TimeoutError]
-

Polling helpers

-
    -
  • [lookup_topic(name)][cortex.discovery.client.DiscoveryClient.lookup_topic] — - one-shot, returns None on miss.
  • -
  • [wait_for_topic(name, timeout, poll_interval)][cortex.discovery.client.DiscoveryClient.wait_for_topic] — - blocking poll loop (time.sleep).
  • -
  • [wait_for_topic_async(name, timeout, poll_interval)][cortex.discovery.client.DiscoveryClient.wait_for_topic_async] — - async poll loop (asyncio.sleep). This is what [Subscriber][cortex.core.subscriber.Subscriber] - uses when wait_for_topic=True.
  • -
-

Protocol

-

Implemented in cortex.discovery.protocol.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
TypePurpose
[DiscoveryCommand][cortex.discovery.protocol.DiscoveryCommand]REGISTER_TOPIC / UNREGISTER_TOPIC / LOOKUP_TOPIC / LIST_TOPICS / SHUTDOWN
[DiscoveryStatus][cortex.discovery.protocol.DiscoveryStatus]OK / NOT_FOUND / ALREADY_EXISTS / ERROR
[TopicInfo][cortex.discovery.protocol.TopicInfo]name, address, message_type, fingerprint, publisher_node
[DiscoveryRequest][cortex.discovery.protocol.DiscoveryRequest]command + optional topic_info / topic_name
[DiscoveryResponse][cortex.discovery.protocol.DiscoveryResponse]status, message, topic_info, topics
-

All payloads are msgpack. TopicInfo is nested as a packed sub-blob so -discovery responses stay flat.

-

Known limitations

-

Summarized here, detailed in critique.md:

-
    -
  • One-publisher-per-topic.
  • -
  • No heartbeats or leases — crashed publishers leave stale entries.
  • -
  • Single-threaded REP — slow client starves others.
  • -
  • retries=1 in the client is a fencepost; effective retries today is zero.
  • -
  • Daemon state lost on restart; publishers do not auto-re-register.
  • -
-

See also

- - - - - - - - - - - - - - - - -
-
- - - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/site/components/messages/index.html b/docs/site/components/messages/index.html deleted file mode 100644 index 241b812..0000000 --- a/docs/site/components/messages/index.html +++ /dev/null @@ -1,1916 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Messages - Cortex - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - -
- - -
- -
- - - - - - - - - -
-
- - - -
-
-
- - - - - - - -
-
-
- - - -
-
- - - - - - - - -
- -
- - - -
-
- - - -
- -
- - - - - - -

Messages

-
-

Source: cortex.messages.base, -cortex.messages.standard

-
-

Messages are just @dataclasses that inherit from -[Message][cortex.messages.base.Message]. Registering with the type system, -computing a fingerprint, and (de)serialization all happen automatically.

-

Anatomy of a message

-
classDiagram
-    class Message {
-        +fingerprint() int
-        +to_bytes() bytes
-        +to_frames() list
-        +from_bytes(data) tuple
-        +from_frames(frames) tuple
-        +decode(bytes) tuple [static]
-        -_build_header()
-        -_field_names() tuple
-        -_field_values() list
-        -_next_sequence() int
-    }
-    class MessageHeader {
-        +fingerprint: int
-        +timestamp_ns: int
-        +sequence: int
-        +to_bytes() bytes
-        +from_bytes(data) MessageHeader
-        +size() int
-    }
-    class MessageType {
-        +register(cls)
-        +get(fingerprint) type
-        +get_all() dict
-    }
-    Message ..> MessageHeader : emits
-    Message ..> MessageType : auto-registers on subclass
-

Defining a custom message

-
from dataclasses import dataclass
-import numpy as np
-from cortex.messages.base import Message
-
-@dataclass
-class JointTrajectory(Message):
-    timestamp: float
-    positions: np.ndarray   # shape (N,)
-    velocities: np.ndarray  # shape (N,)
-    frame_id: str = ""
-
-

That is the entire contract. The class is registered into -[MessageType._registry][cortex.messages.base.MessageType] by fingerprint at -import time, and gains:

-
    -
  • JointTrajectory.fingerprint() — 64-bit ID.
  • -
  • msg.to_frames() / JointTrajectory.from_frames(frames) — the transport path.
  • -
  • msg.to_bytes() / JointTrajectory.from_bytes(data) — the legacy blob path.
  • -
  • Message.decode(blob) — class dispatch via fingerprint registry.
  • -
-

Sequence numbering

-
-

Class-level counter

-

Message._sequence_counter is shared across all publisher instances of -the same message class in the process. Two ArrayMessage publishers -interleave sequence numbers. Per-topic gap detection therefore needs a -per-publisher counter today; see critique.md § 12.

-
-

Built-in messages

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ClassUse for
[StringMessage][cortex.messages.standard.StringMessage]Plain strings
[IntMessage][cortex.messages.standard.IntMessage] / [FloatMessage][cortex.messages.standard.FloatMessage]Single scalars
[BytesMessage][cortex.messages.standard.BytesMessage]Opaque binary
[DictMessage][cortex.messages.standard.DictMessage]Nested dicts with arrays/tensors
[ListMessage][cortex.messages.standard.ListMessage]Mixed-type lists
[ArrayMessage][cortex.messages.standard.ArrayMessage]Single NumPy array + name / frame_id
[MultiArrayMessage][cortex.messages.standard.MultiArrayMessage]dict[str, np.ndarray] (e.g. points+colors)
[TensorMessage][cortex.messages.standard.TensorMessage]PyTorch tensor (preserves device/grad)
[MultiTensorMessage][cortex.messages.standard.MultiTensorMessage]Named tensor bundle (model I/O)
[ImageMessage][cortex.messages.standard.ImageMessage]Image + encoding + width/height
[PointCloudMessage][cortex.messages.standard.PointCloudMessage]XYZ + optional RGB / intensity / normals
[PoseMessage][cortex.messages.standard.PoseMessage]6-DoF pose (position + quaternion)
[TransformMessage][cortex.messages.standard.TransformMessage]4×4 homogeneous transform
[TimestampMessage][cortex.messages.standard.TimestampMessage] / [HeaderMessage][cortex.messages.standard.HeaderMessage]ROS-style stamps
-

Encode / decode lifecycle

-
flowchart LR
-    A[User builds dataclass] --> B[Publisher.publish]
-    B --> C[message.to_frames]
-    C --> D[[ZMQ multipart send]]
-    D --> E[[ZMQ multipart recv]]
-    E --> F[Message.from_frames]
-    F --> G[user callback msg, header]
-

See also

- - - - - - - - - - - - - - - - -
-
- - - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/site/components/node-and-executors/index.html b/docs/site/components/node-and-executors/index.html deleted file mode 100644 index 5849547..0000000 --- a/docs/site/components/node-and-executors/index.html +++ /dev/null @@ -1,2069 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Node & Executors - Cortex - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - -
- - -
- -
- - - - - - - - - -
-
- - - -
-
-
- - - - - - - -
-
-
- - - -
-
- - - - - - - - -
- -
- - - -
-
- - - -
- -
- - - - - - -

Node & Executors

-
-

Source: cortex.core.node, -cortex.core.executor

-
-

A [Node][cortex.core.node.Node] is the user-facing composition unit: it owns -a shared ZMQ async context and a collection of publishers, subscribers, and -timers. Executors provide the scheduling primitives that timers and -subscriber receive loops run on.

-

Responsibilities

-
flowchart TB
-    subgraph NodeResp[Node]
-        CTX[shared zmq.asyncio.Context]
-        PUBS[Publishers dict]
-        SUBS[Subscribers dict]
-        TIMERS[Timers list]
-    end
-
-    NodeResp -- create_publisher --> P[Publisher]
-    NodeResp -- create_subscriber --> S[Subscriber]
-    NodeResp -- create_timer --> RE[RateExecutor]
-    NodeResp -- run / close --> Lifecycle
-
-    P -. uses .-> CTX
-    S -. uses .-> CTX
-

One node = one process boundary in practice. Nothing stops you running -multiple nodes in the same process (asyncio.gather([n.run() for n in nodes]), -see examples/multi_node_system.py), -but remember they share the same event loop — a slow callback in one still -blocks the others.

-

Lifecycle

-
stateDiagram-v2
-    [*] --> Constructed: Node(name)
-    Constructed --> Configured: create_publisher/subscriber/timer
-    Configured --> Running: await node.run()
-    Running --> Running: timers fire, callbacks dispatch
-    Running --> Stopping: node.stop() or cancel
-    Stopping --> Closed: await node.close()
-    Closed --> [*]: context terminated
-

node.run()

-

Spawns one asyncio task per timer and one per callback-bearing subscriber, -then asyncio.gathers them. Returns when all tasks complete or the node is -stopped.

-
async with Node("my_node") as node:
-    node.create_publisher("/x", IntMessage)
-    node.create_subscriber("/y", IntMessage, callback=on_y)
-    await node.run()   # blocks until cancelled
-# __aexit__ calls close() automatically
-
-

node.close()

-

Stops all executors, cancels outstanding tasks, closes every publisher and -subscriber (each of which unregisters/unbinds their own socket), and -terminates the shared ZMQ context. Idempotent.

-

Executors

-

Two flavours, both subclasses of BaseExecutor.

-
classDiagram
-    class BaseExecutor {
-        <<abstract>>
-        +func: AsyncCallback
-        +start()
-        +stop()
-        +run(*args, **kwargs)
-        #_run_impl()*
-    }
-    class AsyncExecutor {
-        +_run_impl()
-    }
-    class RateExecutor {
-        +rate_hz: float
-        +interval: float
-        +_run_impl()
-    }
-    BaseExecutor <|-- AsyncExecutor
-    BaseExecutor <|-- RateExecutor
-

AsyncExecutor

-

"Run this coroutine as fast as possible, yielding between iterations."

-
flowchart LR
-    Start --> Check{running?}
-    Check -- no --> End
-    Check -- yes --> Call[await func]
-    Call -- exception --> Log[log error]
-    Log --> Sleep
-    Call --> Sleep[await sleep 0]
-    Sleep --> Check
-

Used by Subscriber.run to drive the receive-dispatch loop.

-

RateExecutor

-

"Run this coroutine at a constant rate, catching up on overruns."

-
flowchart TD
-    Start[next = perf_counter] --> Loop{running?}
-    Loop -- no --> End
-    Loop -- yes --> Now[now = perf_counter]
-    Now --> Due{now >= next?}
-    Due -- yes --> Call[await func]
-    Call --> Advance[next += interval]
-    Advance --> Behind{next < now?}
-    Behind -- yes --> Reset[next = now + interval]
-    Behind -- no --> Wait
-    Reset --> Wait
-    Due -- no --> Wait[await sleep next - now]
-    Wait --> Loop
-

The catch-up branch silently drops ticks — if your 100 Hz callback takes -20 ms once, you do not get two callbacks back-to-back; you skip one tick.

-
-

Redundant yield

-

Today there is an await asyncio.sleep(0) inside the loop and -await asyncio.sleep(max(0, dt)) at the bottom. That generates an extra -wakeup per tick. See critique § 15.

-
-

Timer usage

-
node.create_timer(1.0 / 30, self.publish_frame)   # 30 Hz
-node.create_timer(1.0, self.log_stats)            # 1 Hz
-
-

Timers are plain async functions — no decorator, no magic. They run in the -same event loop as subscriber callbacks, so the same head-of-line caveat -applies.

-

Shared ZMQ context

-

Every publisher and subscriber created through a node reuses the node's -zmq.asyncio.Context. This means:

-
    -
  • Socket creation is cheap.
  • -
  • io threads are shared across all sockets in the node.
  • -
  • Terminating the node's context cleanly shuts down all its sockets.
  • -
-

Do not create your own context inside callbacks; you'll leak resources and -defeat the shared-io-thread optimization.

-

Minimal complete node

-
from dataclasses import dataclass
-import numpy as np
-import cortex
-from cortex import Node, Message
-from cortex.messages.base import MessageHeader
-
-
-@dataclass
-class Ping(Message):
-    payload: np.ndarray
-    counter: int
-
-
-class Echo(Node):
-    def __init__(self):
-        super().__init__("echo")
-        self.pub = self.create_publisher("/pong", Ping)
-        self.create_subscriber("/ping", Ping, callback=self.on_ping)
-        self._n = 0
-
-    async def on_ping(self, msg: Ping, header: MessageHeader):
-        self._n += 1
-        self.pub.publish(Ping(payload=msg.payload, counter=self._n))
-
-
-async def main():
-    async with Echo() as node:
-        await node.run()
-
-
-if __name__ == "__main__":
-    cortex.run(main())
-
-

See also

- - - - - - - - - - - - - - - - -
-
- - - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/site/components/publisher-subscriber/index.html b/docs/site/components/publisher-subscriber/index.html deleted file mode 100644 index 63d01c4..0000000 --- a/docs/site/components/publisher-subscriber/index.html +++ /dev/null @@ -1,2331 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Publisher & Subscriber - Cortex - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - -
- - -
- -
- - - - - - - - - -
-
- - - -
-
-
- - - - - - - -
-
-
- - - - - - - -
- -
- - - - - - -

Publisher & Subscriber

-
-

Source: cortex.core.publisher, -cortex.core.subscriber

-
-

The data-plane workhorses. A Publisher binds a ZMQ PUB socket and registers -with discovery; a Subscriber looks up the endpoint, connects a SUB socket, -and drives an async receive loop. Discovery is consulted once per topic on -startup — it is not on the hot path.

-

Relationship to the rest of the stack

-
flowchart LR
-    Node -.owns.-> P[Publisher]
-    Node -.owns.-> S[Subscriber]
-    P -- register --> DC1[DiscoveryClient]
-    S -- lookup --> DC2[DiscoveryClient]
-    P -- send_multipart --> Sock1[(zmq.PUB<br/>IPC)]
-    Sock1 -. IPC .-> Sock2[(zmq.SUB)]
-    S -- recv_multipart --> Sock2
-    M[Message] -- to_frames --> P
-    S -- from_frames --> M
-

Publisher

-

Construction

-

Always create via [Node.create_publisher][cortex.core.node.Node.create_publisher] — -direct construction works but skips the shared ZMQ context reuse and the -node-level registration bookkeeping.

-
pub = node.create_publisher(
-    topic_name="/camera/image",   # must start with "/"
-    message_type=ImageMessage,    # fingerprint is taken from this class
-    queue_size=100,               # SNDHWM; drops under backpressure
-)
-
-

Startup sequence

-
sequenceDiagram
-    autonumber
-    participant U as User
-    participant Pub as Publisher
-    participant FS as /tmp/cortex/topics/
-    participant ZMQ as zmq.PUB
-    participant D as Discovery daemon
-
-    U->>Pub: __init__(topic, msg_cls, ...)
-    Pub->>Pub: address = generate_ipc_address(topic, node)
-    Pub->>FS: mkdir -p; unlink stale .sock
-    Pub->>ZMQ: socket(PUB); setsockopt HWM/LINGER; bind(address)
-    Pub->>D: REGISTER TopicInfo{name, address, fingerprint, node}
-    D-->>Pub: OK / ALREADY_EXISTS
-    Note over Pub: ready; user can publish()
-

Two things worth calling out:

-
    -
  1. The IPC address is derived deterministically from node_name and - topic_name via [generate_ipc_address][cortex.core.publisher.generate_ipc_address]: - ipc:///tmp/cortex/topics/<node>__<topic-with-slashes-as-underscores>.sock.
  2. -
  3. _setup_socket unlinks any existing file at that path before binding. That - protects against crash-leftover sockets, but also means two publishers - configured with the same node_name + topic_name in the same process tree - will silently stomp each other — see critique § 10.
  4. -
-

Publish path

-
flowchart LR
-    Msg[Message dataclass] --> H[build MessageHeader<br/>fp, ts, seq]
-    Msg --> V[serialize_message_frames<br/>values]
-    H --> F[[frame 1: header 24B]]
-    V --> F2[[frame 2: msgpack metadata]]
-    V --> FN[[frames 3..N: array buffers]]
-    T[[frame 0: topic bytes]]
-    F --> Send
-    F2 --> Send
-    FN --> Send
-    T --> Send
-    Send[send_multipart NOBLOCK] -->|success| Pub[publish count++]
-    Send -->|zmq.Again| Drop[return False]
-

publish() is synchronous and returns a boolean:

-
    -
  • True — handed to ZMQ successfully.
  • -
  • Falsezmq.Again, queue full, message dropped.
  • -
-

Any other exception is logged and swallowed; publish still returns False. -For robotics code this "fire and forget" is intentional — the caller decides -whether to retry based on the return value and the topic's role.

-

Async context quirk

-

Node owns a zmq.asyncio.Context. The Publisher constructor detects this -and wraps a sync zmq.Context around the same underlying io threads:

-
if isinstance(self._context, zmq.asyncio.Context):
-    self._context: zmq.Context = zmq.Context(self._context)
-
-

This keeps publish() a normal function call instead of forcing every publish -to be awaited. It is the right performance choice, but it has consequences:

-
-

zmq.PUB is not thread-safe

-

Do not call publish() on the same Publisher from multiple threads -(or multiple asyncio tasks that could race on send_multipart). Serialize -per-publisher calls yourself if you fan out work.

-
-

Lifecycle and cleanup

-
stateDiagram-v2
-    [*] --> Bound: bind + register
-    Bound --> Publishing: publish() calls
-    Publishing --> Publishing: more messages
-    Publishing --> Closed: close()
-    Bound --> Closed: close()
-    Closed --> [*]: unregister,<br/>unlink .sock file
-

Publisher.close() is best-effort: it unregisters from the daemon (silently -tolerates a dead daemon), closes the socket, and removes the IPC file. -Exceptions from any one step do not block the others.

-

Statistics

-

publisher.publish_count, publisher.last_publish_time, and -publisher.is_registered are exposed for instrumentation. They update on the -hot path with no locking — read them from the same task that calls publish() -for deterministic numbers.

-

Subscriber

-

Construction

-
sub = node.create_subscriber(
-    topic_name="/camera/image",
-    message_type=ImageMessage,
-    callback=on_image,          # async def callback(msg, header)
-    queue_size=10,              # RCVHWM
-    wait_for_topic=True,        # poll until topic appears
-    topic_timeout=30.0,         # abort wait after N seconds
-)
-
-

If callback is None, the subscriber is passive — call await sub.receive() -manually. With a callback, Node.run() will drive the receive loop.

-

Startup sequence

-
sequenceDiagram
-    autonumber
-    participant U as User
-    participant S as Subscriber
-    participant D as DiscoveryClient
-    participant Pub as publisher IPC
-
-    U->>S: __init__(...)
-    S->>D: lookup_topic(name)  # non-blocking
-    alt found immediately
-        D-->>S: TopicInfo
-        S->>S: verify fingerprint
-        S->>Pub: SUB connect + SUBSCRIBE topic
-        Note over S: is_connected = True
-    else not found
-        D-->>S: None
-        Note over S: defer; retry in run()
-    end
-
-    U->>S: node.run() schedules sub.run()
-    S->>D: wait_for_topic_async(name, timeout)
-    D-->>S: TopicInfo
-    S->>Pub: SUB connect + SUBSCRIBE topic
-

The constructor tries a non-blocking lookup first so that when a publisher is -already up, no polling is needed. The polling fallback only kicks in inside -sub.run() via [wait_for_topic_async][cortex.discovery.client.DiscoveryClient.wait_for_topic_async].

-

Receive loop

-
flowchart LR
-    Loop{{AsyncExecutor}} --> Recv[await recv_multipart copy=False]
-    Recv --> Frames[frames = topic, header, metadata, *buffers]
-    Frames --> Decode[Message.from_frames frames 1..]
-    Decode --> CB[await callback msg, header]
-    CB --> Yield[await asyncio.sleep 0]
-    Yield --> Loop
-
    -
  • copy=False means each frame is a zmq.Frame — the metadata and array - buffers are memoryview-able without a copy. See - cortex.utils.serialization.
  • -
  • The one-frame fast path (len(payload_frames) == 1) handles legacy - publishers still on the single-blob path — it falls back to - from_bytes on the single payload buffer.
  • -
-

Head-of-line blocking

-

The callback runs inline in the receive loop. A slow callback stalls -everything:

-
gantt
-    title Receive loop when callback is slow
-    dateFormat X
-    axisFormat %L ms
-    section Messages
-    recv m1       :0, 1
-    decode m1     :1, 2
-    callback m1 (slow!) :active, 2, 50
-    recv m2 (queued on HWM) :crit, 50, 51
-    decode m2     :51, 52
-    callback m2   :52, 55
-

If callbacks do meaningful work, dispatch them to a task or thread pool:

-
import asyncio
-
-async def on_image(msg, header):
-    asyncio.create_task(process_in_background(msg, header))
-
-

Or use a bounded queue + worker pattern. The roadmap item in -critique § 6 is to lift this into the framework.

-

Fingerprint verification

-

On connect the subscriber compares its class's fingerprint to the one in the -registry entry. Today a mismatch only logs a warning and proceeds anyway — -downstream decoding will then fail hard. Treat fingerprint warnings as errors -in your code.

-

Cleanup

-

Subscriber.close() stops the executor, closes the discovery client and SUB -socket, and flips is_connected to False. Safe to call multiple times; -errors are suppressed so teardown does not cascade.

-

Statistics and instrumentation

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
PropertyPublisherSubscriber
publish_count / receive_count
last_publish_time / last_receive_time
is_registered / is_connected
topic_info
-

None of these are atomic; treat them as coarse gauges.

-

Common pitfalls

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
SymptomCauseFix
First N messages not receivedZMQ "slow joiner": SUB not connected yet when PUB started publishingLet subscriber start first, or sleep briefly before first publish
Subscriber receives nothing, no errorsTopic name mismatch, or forgot to call node.run()Log both sides; run cortex-discovery --log-level DEBUG
publish() returns False repeatedlySubscriber can't keep up; SNDHWM reachedIncrease queue_size, or reduce publish rate
Mutating a received array "corrupts" laterDecoded arrays alias ZMQ frame memoryarr = arr.copy() before mutating
Two processes stomp each other's socketSame node_name + topic_nameUnique node names per process
-

See also

- - - - - - - - - - - - - - - - -
-
- - - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/site/components/serialization/index.html b/docs/site/components/serialization/index.html deleted file mode 100644 index 6d81917..0000000 --- a/docs/site/components/serialization/index.html +++ /dev/null @@ -1,1980 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Serialization - Cortex - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - -
- - -
- -
- - - - - - - - - -
-
- - - -
-
-
- - - - - - - -
-
-
- - - - - - - -
- -
- - - - - - -

Serialization

-
-

Source: cortex.utils.serialization, -cortex.utils.hashing

-
-

Two encodings live side by side: a multipart / out-of-band path that the -transport actually uses, and a single-blob path kept for the legacy -Message.to_bytes / decode API and tests. Both support the same Python -types; only their frame layout differs.

-

Supported types

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
TypeInline path (to_bytes)OOB path (to_frames)
None1 byte tagmsgpack nil
int, float, str, boolmsgpack PRIMITIVEmsgpack
bytestag + length + bytesmsgpack bin
list, tuple, dictmsgpack with ExtType arraysmsgpack with OOB descriptors
np.ndarrayExtType (inline bytes)OOB descriptor + extra frame
torch.TensorExtType (inline bytes)OOB descriptor + extra frame
-

The two paths, side by side

-
-
-
-
flowchart LR
-    V[values] --> E[_encode_transport_value]
-    E --> Meta[msgpack metadata<br/>OOB descriptors for arrays]
-    E --> Bufs[[buffer 0]]
-    E --> Bufs2[[buffer 1]]
-    Meta --> Out[(list of frames)]
-    Bufs --> Out
-    Bufs2 --> Out
-

The function of interest is -[serialize_message_frames][cortex.utils.serialization.serialize_message_frames]:

-
metadata_bytes, [buf0, buf1, ...] = serialize_message_frames(values)
-
-

Arrays stay contiguous; ZMQ hands the buffer straight to the kernel.

-
-
-
flowchart LR
-    V[values] --> P[msgpack.packb<br/>default=_msgpack_default]
-    P --> Ext[ExtType 1/2 for arrays/tensors<br/>bytes embedded]
-    Ext --> Blob[single bytes blob]
-

The single blob round-trips through serialize(value) → -deserialize(data). Useful for persisting to disk, caches, or when you -need a self-contained payload without tracking extra buffers.

-
-
-
-

OOB descriptors

-

An out-of-band descriptor is a small dict that takes the place of the array -inside the msgpack metadata:

-
# numpy
-{"__cortex_oob__": "numpy", "buffer": 0, "dtype": "<f4", "shape": [480, 640, 3]}
-
-# torch
-{"__cortex_oob__": "torch", "buffer": 1, "dtype": "<f4",
- "shape": [1, 3, 224, 224], "device": "cuda:0", "requires_grad": True}
-
-

The buffer index refers into the ZMQ frames that follow the metadata. -Nested structures (dict of arrays, list of tensors, etc.) are walked -recursively by _encode_transport_value / _decode_transport_value.

-

Zero-copy on the decode side

-
sequenceDiagram
-    participant Sub as Subscriber
-    participant ZMQ as zmq.Frame
-    participant MV as memoryview
-    participant NP as np.ndarray
-
-    Sub->>ZMQ: recv_multipart(copy=False)
-    ZMQ-->>Sub: frame with .buffer property
-    Sub->>MV: memoryview(frame.buffer)
-    Sub->>NP: np.frombuffer(mv, dtype).reshape(shape)
-    Note over NP: array aliases the ZMQ frame memory
-
-

Aliasing caveat

-

The returned NumPy array is a view over the ZMQ frame buffer. It is -safe to read as long as the frame lives, which is at least until your -callback returns. If you need to:

-
    -
  • mutate the array, or
  • -
  • keep it past the callback,
  • -
-

call arr = arr.copy() first. This is cheap compared to the savings on -the hot path.

-
-

PyTorch specifics

-
    -
  • Tensors are always moved to CPU for transport. Transport frames carry - the tensor's CPU bytes plus the original device string.
  • -
  • On decode, CUDA tensors are moved back to the original device when CUDA is - available; otherwise they stay on CPU.
  • -
  • requires_grad is preserved.
  • -
-

Fingerprinting

-

Separate but related: [compute_fingerprint(cls)][cortex.utils.hashing.compute_fingerprint] -computes a 64-bit identity from the module path, class name, and sorted -field:type pairs. Cached per-class in _fingerprint_cache. See -Concepts → Fingerprinting for the full story.

-

When to use each helper

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
HelperUse when
[serialize_message_frames][cortex.utils.serialization.serialize_message_frames]You're building a custom transport that speaks multipart
[deserialize_message_frames][cortex.utils.serialization.deserialize_message_frames]Decoding the above
[serialize(value)][cortex.utils.serialization.serialize] / [deserialize][cortex.utils.serialization.deserialize]Persisting a single value to disk / cache
[serialize_numpy][cortex.utils.serialization.serialize_numpy] / [deserialize_numpy][cortex.utils.serialization.deserialize_numpy]Raw array round-trip without msgpack overhead
Message.to_frames / Message.from_framesAnything inside Cortex itself
-

See also

- - - - - - - - - - - - - - - - -
-
- - - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/site/concepts/architecture/index.html b/docs/site/concepts/architecture/index.html deleted file mode 100644 index 6cec9dc..0000000 --- a/docs/site/concepts/architecture/index.html +++ /dev/null @@ -1,1805 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Architecture - Cortex - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - -
- - -
- -
- - - - - - - - - -
-
- - - -
-
-
- - - - - - - -
-
-
- - - -
-
- - - - - - - - -
- -
- - -
- - - - -
-
-
- - - -
- -
- - - - - - -

Architecture

-

Cortex has three moving parts: the discovery daemon, publisher nodes, -and subscriber nodes. They coordinate over ZeroMQ — a REQ/REP control plane -for discovery and a PUB/SUB data plane for messages.

-

High-level view

-
flowchart TB
-    subgraph CP[Control plane]
-        DD[Discovery daemon<br/><small>ipc:///tmp/cortex/discovery.sock</small>]
-    end
-
-    subgraph DP[Data plane]
-        direction LR
-        P[Publisher node] -- "PUB / SUB (IPC)" --> S[Subscriber node]
-    end
-
-    P -- REGISTER --> DD
-    S -- LOOKUP --> DD
-    DD -- TopicInfo --> S
-
-    classDef daemon fill:#6366f1,stroke:#312e81,color:#fff
-    classDef node fill:#0ea5e9,stroke:#0369a1,color:#fff
-    class DD daemon
-    class P,S node
-

Message journey

-

Tracing one frame end to end:

-
sequenceDiagram
-    autonumber
-    participant User as User code
-    participant Pub as Publisher
-    participant Sock as ZMQ PUB socket
-    participant Net as IPC
-    participant SSock as ZMQ SUB socket
-    participant Sub as Subscriber
-    participant CB as async callback
-
-    User->>Pub: publish(Message)
-    Pub->>Pub: build header (fingerprint, ts, seq)
-    Pub->>Pub: encode field values + OOB buffers
-    Pub->>Sock: send_multipart([topic, header, metadata, *buffers])
-    Sock->>Net: zero-copy handoff
-    Net->>SSock: frames delivered
-    SSock->>Sub: recv_multipart(copy=False)
-    Sub->>Sub: Message.from_frames(...)
-    Sub->>CB: await callback(msg, header)
-

Key invariant: array buffers ride as separate ZMQ frames, not inline in the -metadata. See Message wire format.

-

Process layout

-
flowchart LR
-    subgraph P1[Process: sensor]
-        N1[Node<br/>shared zmq.asyncio.Context]
-        PUB1[Publisher /sensor/a]
-        PUB2[Publisher /sensor/b]
-        T1[Timer 30 Hz]
-        N1 --> PUB1
-        N1 --> PUB2
-        N1 --> T1
-    end
-
-    subgraph P2[Process: processor]
-        N2[Node]
-        SUB1[Subscriber /sensor/a]
-        SUB2[Subscriber /sensor/b]
-        PUB3[Publisher /processed]
-        N2 --> SUB1
-        N2 --> SUB2
-        N2 --> PUB3
-    end
-
-    PUB1 -.->|IPC| SUB1
-    PUB2 -.->|IPC| SUB2
-

Each topic gets its own IPC socket under /tmp/cortex/topics/. A single Node -shares one zmq.asyncio.Context across all its publishers and subscribers to -avoid per-socket io thread overhead.

-

See also

- - - - - - - - - - - - - - - - -
-
- - - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/site/concepts/async-execution-model/index.html b/docs/site/concepts/async-execution-model/index.html deleted file mode 100644 index e0daf30..0000000 --- a/docs/site/concepts/async-execution-model/index.html +++ /dev/null @@ -1,1840 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Async execution model - Cortex - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - -
- - -
- -
- - - - - - - - - -
-
- - - -
-
-
- - - - - - - -
-
-
- - - -
-
- - - - - - - - -
- -
- - - -
-
- - - -
- -
- - - - - - -

Async execution model

-

Cortex nodes are asyncio-native. One event loop per process drives all -publishers, subscribers, and timers for that node. On Linux and macOS, -[cortex.run][cortex.utils.loop.run] prefers uvloop for lower tail latency.

-

Node task graph

-
flowchart TB
-    Loop(((asyncio event loop)))
-    Loop --> T1[Timer 1<br/>RateExecutor]
-    Loop --> T2[Timer 2<br/>RateExecutor]
-    Loop --> S1[Subscriber 1<br/>AsyncExecutor]
-    Loop --> S2[Subscriber 2<br/>AsyncExecutor]
-

Node.run() spawns one task per timer (RateExecutor) and one per -callback-bearing subscriber (AsyncExecutor). It then asyncio.gathers them -until cancelled.

-

RateExecutor cadence

-
sequenceDiagram
-    participant L as Event loop
-    participant R as RateExecutor
-    participant CB as callback
-
-    loop every interval
-        L->>R: resume
-        R->>CB: await callback()
-        R->>R: next_exec_time += interval
-        alt fell behind
-            R->>R: next_exec_time = now + interval
-        end
-        R->>L: sleep(next_exec_time - now)
-    end
-

Catch-up logic silently drops ticks when a callback overruns its period — -something to keep in mind for control loops.

-

AsyncExecutor receive loop

-
sequenceDiagram
-    participant L as Event loop
-    participant A as AsyncExecutor
-    participant S as SUB socket
-    participant CB as user callback
-
-    loop while running
-        L->>A: resume
-        A->>S: await recv_multipart(copy=False)
-        S-->>A: frames
-        A->>A: decode message
-        A->>CB: await callback(msg, header)
-        A->>L: sleep(0)  (yield)
-    end
-
-

Head-of-line blocking

-

A slow callback stalls the receive loop. Messages pile up on the SUB HWM -and get evicted. If you expect variable-latency work, offload callback -bodies to asyncio.create_task(...) or a thread pool.

-
-

Publish is sync-inside-async

-

The Publisher uses a sync zmq.Context (shadowed onto the node's async -context). publish() is a plain function call — no await. This avoids the -overhead of the async zmq integration on the send path.

-
-

Not thread-safe

-

A zmq.PUB socket is not safe to call from multiple threads or tasks -concurrently. Serialize calls to publish() per publisher.

-
-

uvloop

-

On Unix, importing cortex.run checks for uvloop and uses it if present. -Measured impact: modest throughput improvement, meaningful p99 latency -reduction on high-rate small messages.

-

See also

- - - - - - - - - - - - - - - - -
-
- - - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/site/concepts/discovery-protocol/index.html b/docs/site/concepts/discovery-protocol/index.html deleted file mode 100644 index 63ec022..0000000 --- a/docs/site/concepts/discovery-protocol/index.html +++ /dev/null @@ -1,1930 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Discovery protocol - Cortex - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - -
- - -
- -
- - - - - - - - - -
-
- - - -
-
-
- - - - - - - -
-
-
- - - - - - - -
- -
- - - - - - -

Discovery protocol

-

The discovery daemon speaks a tiny msgpack-over-REQ/REP protocol. It is not -on the data path — once a subscriber has the endpoint, messages flow -publisher → subscriber directly.

-

Commands

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
CommandPayload requiredReturns
REGISTER_TOPIC (1)[TopicInfo][cortex.discovery.protocol.TopicInfo]OK / ALREADY_EXISTS
UNREGISTER_TOPIC (2)topic_name or TopicInfo.nameOK / NOT_FOUND
LOOKUP_TOPIC (3)topic_nameOK + TopicInfo / NOT_FOUND
LIST_TOPICS (4)OK + list[TopicInfo]
SHUTDOWN (99)OK; daemon exits
-

Status codes: OK=0, NOT_FOUND=1, ALREADY_EXISTS=2, ERROR=3.

-

TopicInfo payload

-
@dataclass
-class TopicInfo:
-    name: str              # "/camera/image"
-    address: str           # "ipc:///tmp/cortex/topics/cam__camera_image.sock"
-    message_type: str      # "ImageMessage"
-    fingerprint: int       # 64-bit class fingerprint
-    publisher_node: str    # "cam"
-
-

Publisher register flow

-
sequenceDiagram
-    autonumber
-    participant P as Publisher
-    participant D as Daemon REP
-
-    P->>P: bind PUB socket on ipc:///tmp/cortex/topics/<node>__<topic>.sock
-    P->>D: REQ → DiscoveryRequest(REGISTER_TOPIC, TopicInfo{...})
-    D->>D: if topic_name absent: insert; else compare publisher_node
-    alt new
-        D-->>P: OK "Registered topic: /x"
-    else same publisher re-registering
-        D-->>P: OK (overwrite)
-    else different publisher, same topic
-        D-->>P: ALREADY_EXISTS
-    end
-

Subscriber lookup flow

-
sequenceDiagram
-    autonumber
-    participant S as Subscriber
-    participant D as Daemon REP
-    participant P as Publisher
-
-    S->>D: REQ → LOOKUP_TOPIC("/x")
-    alt present
-        D-->>S: OK + TopicInfo
-        S->>P: SUB connect + SUBSCRIBE "/x"
-    else missing
-        D-->>S: NOT_FOUND
-        Note over S: if wait_for_topic:<br/>poll every 500 ms until timeout
-        S->>D: retry LOOKUP_TOPIC
-    end
-

wait_for_topic_async implements the retry loop with asyncio.sleep so the -event loop keeps spinning.

-

REQ-socket recovery

-

ZMQ REQ sockets enter a bad state after a missed reply — they block further -sends. The client detects zmq.Again on timeout and rebuilds the socket:

-
flowchart TD
-    A[send request] -->|timeout| B[REQ socket stuck]
-    B --> C[close socket]
-    C --> D[recreate socket<br/>same endpoint]
-    D --> E[retry up to retries]
-

See [DiscoveryClient._reconnect][cortex.discovery.client.DiscoveryClient].

-
-

Fencepost in retries default

-

retries=1 today executes the loop exactly once — i.e. no retry. Bump to -retries=3 in client-side code if you need resilience.

-
-

Failure modes & how Cortex handles them

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ScenarioBehavior
Daemon not running when publisher startsRegister fails; publisher still publishes, but no subscriber can find it.
Daemon restartsAll state lost; publishers must re-register. Current design has no auto-re-register.
Publisher crashesRegistry keeps stale TopicInfo until someone UNREGISTERs.
Two publishers, same topicSecond registration rejected with ALREADY_EXISTS.
Subscriber looks up before publisherNOT_FOUND; caller may wait_for_topic to poll.
-

Roadmap items (see critique.md) to address these: leases with -heartbeats, multi-publisher support, and notify-on-change.

-

See also

- - - - - - - - - - - - - - - - -
-
- - - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/site/concepts/fingerprinting/index.html b/docs/site/concepts/fingerprinting/index.html deleted file mode 100644 index 7f544ed..0000000 --- a/docs/site/concepts/fingerprinting/index.html +++ /dev/null @@ -1,1853 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Fingerprinting - Cortex - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - -
- - -
- -
- - - - - - - - - -
-
- - - -
-
-
- - - - - - - -
-
-
- - - -
-
- - - - - - - - -
- -
- - - -
-
- - - -
- -
- - - - - - -

Fingerprinting

-

Every message class gets a 64-bit identifier derived from its name and -field schema. The fingerprint rides in the header of every published message -and does two jobs:

-
    -
  1. Type dispatchMessage.decode(bytes) looks up the right class in the - [MessageType][cortex.messages.base.MessageType] registry.
  2. -
  3. Compatibility check — subscribers verify that the topic they looked up - advertises the same fingerprint as the type they were written against.
  4. -
-

Derivation

-
flowchart LR
-    A[class.__module__ + qualname] --> C[canonical string]
-    B[sorted list of field:type] --> C
-    C --> H[SHA-256]
-    H --> F[first 8 bytes → u64 big-endian]
-

Pseudocode:

-
canonical = f"{cls.__module__}.{cls.__qualname__}|{','.join(sorted('name:type'))}"
-fingerprint = int.from_bytes(sha256(canonical.encode()).digest()[:8], "big")
-
-

The result is cached per-class in _fingerprint_cache, computed once lazily.

-

Registry

-

Message.__init_subclass__ auto-registers every concrete subclass into -[MessageType._registry][cortex.messages.base.MessageType] keyed by -fingerprint. Nothing else to do — decorating your dataclass with -@dataclass and inheriting from Message is enough.

-
from dataclasses import dataclass
-from cortex.messages.base import Message
-
-@dataclass
-class JointState(Message):
-    positions: list[float]
-    velocities: list[float]
-
-print(hex(JointState.fingerprint()))
-
-

When fingerprints change

-

The fingerprint is not stable across edits that touch:

-
    -
  • Module path or class name (cortex.messages.standard.ArrayMessage renamed - anywhere).
  • -
  • Field names.
  • -
  • Field type annotations as spelled (see the PEP 563 caveat below).
  • -
-

It is stable across:

-
    -
  • Adding/removing unrelated classes.
  • -
  • Reordering methods.
  • -
  • Changing docstrings or default values.
  • -
-

Subscriber check

-

On connect, the subscriber compares the topic's advertised fingerprint against -the one it computed from its message class:

-
sequenceDiagram
-    participant S as Subscriber
-    participant D as Discovery daemon
-
-    S->>D: LOOKUP /topic
-    D-->>S: TopicInfo(fingerprint=0xABCD...)
-    S->>S: compare with MyMessage.fingerprint()
-    alt mismatch
-        S-->>S: log warning, continue anyway
-    else match
-        S-->>S: connect and subscribe
-    end
-
-

Today: mismatch is a warning, not an error

-

A fingerprint mismatch currently only logs a warning — see critique.md. -Downstream decoding will fail hard. Until that is tightened, prefer to -re-exchange type definitions between processes rather than rely on this guard.

-
-

PEP 563 caveat

-

field.type may be a string (under from __future__ import annotations) -or a real type otherwise. The canonical string differs in the two cases, -so the same class can fingerprint differently across import environments.

-

When defining messages shared between processes, either use the same import -style in both, or rely on the runtime typing.get_type_hints(cls) equivalent -once that lands upstream.

-

See also

- - - - - - - - - - - - - - - - -
-
- - - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/site/concepts/message-wire-format/index.html b/docs/site/concepts/message-wire-format/index.html deleted file mode 100644 index dd81f57..0000000 --- a/docs/site/concepts/message-wire-format/index.html +++ /dev/null @@ -1,1868 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Message wire format - Cortex - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - -
- - -
- -
- - - - - - - - - -
-
- - - -
-
-
- - - - - - - -
-
-
- - - -
-
- - - - - - - - -
- -
- - - -
-
- - - -
- -
- - - - - - -

Message wire format

-

Cortex uses ZeroMQ multipart messages. Each published message is a list of -frames rather than a single blob. That lets array payloads ride as raw -contiguous buffers — no copy into a Python bytes, no re-copy by ZMQ.

-

Frames on the wire

-
flowchart LR
-    F0["Frame 0<br/>topic bytes"] --> F1
-    F1["Frame 1<br/>header (24B)<br/>fingerprint • ts_ns • seq"] --> F2
-    F2["Frame 2<br/>msgpack metadata<br/>(ordered field values)"] --> F3
-    F3["Frame 3..N<br/>raw array buffers<br/>(OOB, zero-copy)"]
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FrameContentsSize
0Topic name (UTF-8)variable
1[MessageHeader][cortex.messages.base.MessageHeader]24 bytes (3 × u64, big-endian)
2msgpack-packed ordered field values; arrays replaced by OOB descriptorssmall
3..Nnp.ndarray.tobytes() / tensor.numpy().tobytes(), contiguouspayload-sized
-

Header layout

-
offset 0        8       16       24
-       |fp u64 |ts u64 |seq u64 |
-        big-endian throughout
-
-
    -
  • fp — 64-bit message fingerprint, computed from class name and field schema.
  • -
  • ts — publisher wall-clock in nanoseconds (time.time_ns()).
  • -
  • seq — per-process, per-message-type monotonic counter.
  • -
-

Metadata (Frame 2)

-

Field values are packed in declaration order (not by name), so the receiver -reconstructs using the dataclass's cached field tuple. This removes per-message -field-name encoding.

-

Arrays and tensors appear in the metadata as small dict stand-ins called -OOB descriptors:

-
{
-  "__cortex_oob__": "numpy",
-  "buffer": 0,
-  "dtype": "<f4",
-  "shape": [480, 640, 3]
-}
-
-

The buffer index refers into Frames 3..N. The receiver reconstructs:

-
np.frombuffer(frame.buffer, dtype=np.dtype(desc["dtype"])).reshape(desc["shape"])
-
-

No copy. The resulting array aliases the ZMQ frame memory — copy it if you -need ownership or mutability (see Performance tuning).

-

Full encode/decode flow

-
sequenceDiagram
-    participant U as User
-    participant M as Message.to_frames
-    participant S as serialize_message_frames
-    participant E as _encode_transport_value
-    participant Z as ZMQ send_multipart
-
-    U->>M: build header + collect field values
-    M->>S: values in declaration order
-    S->>E: for each value, walk nested dicts/lists
-    E-->>S: scalar stays inline; array → OOB descriptor + buffer appended
-    S-->>M: (metadata_bytes, [buf0, buf1, ...])
-    M-->>Z: [topic, header, metadata, *buffers]
-

The legacy single-blob path

-

Message.to_bytes() / from_bytes() / Message.decode() still exist. They -pack everything into one msgpack blob using ExtType for arrays. That path -is retained for tests and opportunistic use; the transport always uses the -multipart path above.

-
-

Mismatch trap

-

Bytes captured from the wire cannot be fed to Message.decode() — the wire -format is multipart, not a single blob. Use Message.from_frames(frames).

-
-

See also

- - - - - - - - - - - - - - - - -
-
- - - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/site/concepts/transport-and-qos/index.html b/docs/site/concepts/transport-and-qos/index.html deleted file mode 100644 index e905493..0000000 --- a/docs/site/concepts/transport-and-qos/index.html +++ /dev/null @@ -1,1770 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Transport & QoS - Cortex - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - -
- - -
- -
- - - - - - - - - -
-
- - - -
-
-
- - - - - - - -
-
-
- - - -
-
- - - - - - - - -
- -
- - -
- - - - -
-
-
- - - -
- -
- - - - - - -

Transport & QoS

-

Stub — deep dive coming in a later pass.

-

Current socket settings

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
SocketOptionValueNotes
Publisher PUBSNDHWM10 (default queue_size)Drops under backpressure
Publisher PUBLINGER0Immediate close
Subscriber SUBRCVHWM10Oldest messages evicted when full
Subscriber SUBLINGER0
Daemon REPRCVTIMEO1000 msAllows Ctrl-C responsiveness
Daemon REPLINGER0
-

Today's delivery semantics

-
    -
  • Publisher uses zmq.NOBLOCK: if the send queue is full, the message is - silently dropped.
  • -
  • Subscriber HWM is a ring buffer: old messages are silently evicted on - overflow.
  • -
-

This is fine for best-effort telemetry. It is unsafe for control commands.

-

Planned QoS profiles

-

Taking inspiration from DDS, three profiles are enough for most robotics use:

-
    -
  • best_effort_latest — conflate; keep only newest (camera frames).
  • -
  • reliable_queue — publisher blocks or errors (control commands).
  • -
  • dropping_queue — current behavior with an exposed drop counter (telemetry).
  • -
-

See critique.md § 4 for rationale.

- - - - - - - - - - - - - - - -
-
- - - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/site/critique/index.html b/docs/site/critique/index.html deleted file mode 100644 index 49cdfe6..0000000 --- a/docs/site/critique/index.html +++ /dev/null @@ -1,2121 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Cortex Critique - Cortex - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - -
- - -
- -
- - - - - - - - - -
-
- - - -
-
-
- - - - - - - -
-
-
- - - - - - - -
- -
- - - - - - -

Cortex Critique

-

A bottom-up review of Cortex as it stands today, with a focus on its viability as a communication library for robotics. This complements design-review.md with concrete code-level findings and benchmark observations.

-

How Cortex works (bottom-up)

-

1. Fingerprinting — utils/hashing.py

-

A message class's identity is a 64-bit integer:

-
fingerprint = SHA-256(f"{module}.{qualname}|{','.join(sorted('field:type'))}")[:8]
-
-
    -
  • Computed lazily and cached in _fingerprint_cache.
  • -
  • field.type is a string when from __future__ import annotations is active and a real type otherwise. The fingerprint therefore depends on how the module was imported — fragile for cross-repo use.
  • -
  • Field ordering is sorted alphabetically in the fingerprint, but the wire layout uses dataclass declaration order. Two classes could theoretically fingerprint identically but interpret the wire differently.
  • -
-

2. Message base — messages/base.py

-

Each dataclass inheriting Message is auto-registered via __init_subclass__ into MessageType._registry[fingerprint] = cls.

-

Wire format (multipart transport, what publishers actually use):

-
Frame 0: topic bytes            (for PUB/SUB filter)
-Frame 1: 24-byte header         (fingerprint u64, timestamp_ns u64, sequence u64, big-endian)
-Frame 2: msgpack of ordered field values with OOB descriptors
-Frame 3..N: raw contiguous array buffers (zero-copy)
-
-

There is a second, legacy single-blob path (to_bytes / from_bytes) that embeds array bytes inside a single msgpack blob using ExtType. It is retained for Message.decode(...) and tests, but is not what the transport uses.

-

3. Serialization — utils/serialization.py

-

Two strategies coexist:

-
    -
  • _msgpack_default / _msgpack_ext_hook (inline): arrays/tensors get packed as msgpack ExtType inside the single blob. Used by the legacy path.
  • -
  • _encode_transport_value / _decode_transport_value (out-of-band): each array/tensor is replaced with a tiny dict {__cortex_oob__: "numpy", buffer: i, dtype, shape} and its raw bytes are appended as separate ZMQ frames. Reconstruction uses np.frombuffer(frame.buffer, dtype).reshape(shape) with no copy.
  • -
-

After the March 2026 optimizations: zero-copy decode, schema-ordered values (field names no longer repeated per message), and cached field-name tuples.

-

4. Discovery — discovery/daemon.py and discovery/client.py

-

Single-threaded zmq.REP over IPC at ipc:///tmp/cortex/discovery.sock.

-
    -
  • Registry is a plain dict[str, TopicInfo], enforcing one publisher per topic.
  • -
  • RCVTIMEO=1s so the run loop can poll _running for Ctrl-C.
  • -
  • Commands: REGISTER, UNREGISTER, LOOKUP, LIST, SHUTDOWN.
  • -
  • Request/response payloads are msgpack.
  • -
  • Client uses REQ with close-and-recreate on timeout (REQ sockets are stuck after a missed reply).
  • -
-

5. Publisher / Subscriber — core/publisher.py, core/subscriber.py

-
    -
  • Publisher: binds a zmq.PUB at ipc:///tmp/cortex/topics/<node>__<topic>.sock, registers via the discovery client, publishes multipart [topic, header, metadata, *buffers] with zmq.NOBLOCK. If the Node hands it an async context, it wraps a sync zmq.Context(self._context) around the same underlying zmq io threads so publishing stays synchronous.
  • -
  • Subscriber: uses an async context, looks up the topic (optionally waits), connects zmq.SUB, sets a topic filter, loops via AsyncExecutor doing recv_multipart(copy=False)Message.from_frames.
  • -
-

6. Node + Executors — core/node.py, core/executor.py

-

A Node owns a shared zmq.asyncio.Context, plus lists of publishers, subscribers, and timers. Each timer gets a RateExecutor(fn, rate_hz). node.run() creates asyncio tasks for every timer and every callback-subscriber, then asyncio.gather. RateExecutor uses perf_counter plus asyncio.sleep(max(0, next-now)). cortex.run prefers uvloop on Unix.

-

Benchmark results

-

Measured on this machine with the in-repo benchmark suite:

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
MetricValue
Small-payload latencymean 556 µs, p99 1075 µs
64KB latencymean 919 µs, p99 1.4 ms
Tiny array throughput21.8k msg/s
1MB array throughput7.7k msg/s, 8.0 GB/s
4MB array throughput2.25k msg/s, 9.4 GB/s
1080p RGB frames1422 fps, 8.8 GB/s
Raw wire+decode (inproc)35 µs roundtrip (4MB array)
-

The delta between the ~35 µs raw wire and ~550 µs end-to-end is asyncio scheduling, context-switch between publisher timer and subscriber recv, and Python callback dispatch. Serialization is close to memcpy-bandwidth on large payloads — the OOB transport is pulling its weight.

-

What can be improved

-

Design-level (biggest wins)

-
    -
  1. -

    Latency floor is too high for control loops. ~550 µs mean and ~1.5 ms p99 is dominated by asyncio + zmq.asyncio, not zmq itself. Control topics should be able to opt into a synchronous thread-plus-zmq.Poller receive path targeting <100 µs p99. Async should be the default, not the only option.

    -
  2. -
  3. -

    Discovery is a single REQ/REP chokepoint with stop-the-world semantics. On crashes, stale topic entries are never reclaimed — a crashed publisher's IPC file stays on disk and the registry keeps pointing at a dead socket. Add leases with heartbeats (publisher renews every N seconds; daemon evicts stale entries), or a peer-gossip model where every node beacons presence. The current daemon has no concurrency — one slow client blocks all others.

    -
  4. -
  5. -

    One-publisher-per-topic is a hard limit for robotics. Redundant IMUs, failover, and multi-source fusion are all blocked. The registry should accept N publishers per topic and subscribers should connect() to all of them — ZMQ SUB handles fan-in natively.

    -
  6. -
  7. -

    No backpressure semantics. pub.publish() is NOBLOCK and silently drops on HWM. Subscriber HWM=10 on SUB evicts old messages by default. Robotics needs per-topic QoS profiles similar to DDS:

    -
  8. -
  9. best_effort_latest — camera frames: drop old, keep newest (ZMQ_CONFLATE=1).
  10. -
  11. reliable_queue — commands: block or surface an error.
  12. -
  13. -

    dropping_queue — telemetry: current behavior, but with a drop counter.

    -
  14. -
  15. -

    No liveness or drop detection. A subscriber has no way to know the publisher died. Sequence numbers exist in the header but are never checked for gaps. Automatic gap-counting in Subscriber would be gold for debugging.

    -
  16. -
  17. -

    Callback execution blocks the receive loop. A 10 ms callback accumulates on SUB HWM and drops. Receive, decode, and user-callback execution should be decoupled with a bounded work queue and one or more worker coroutines/threads per subscriber. ROS 2 executors have this distinction for a reason.

    -
  18. -
  19. -

    Local-only transport in practice. Addresses are hardcoded ipc:// paths under /tmp. Multi-host robotics (robot ↔ base-station) needs TCP transport in discovery, NIC selection, and topology-aware addressing.

    -
  20. -
  21. -

    No shared memory for huge payloads. At 9 GB/s on 4 MB arrays, every subscriber gets a fresh copy. For multi-subscriber camera or LiDAR fan-out, a shared-memory transport (posix shm + ring buffer + zmq for control-plane notifications) would give true zero-copy.

    -
  22. -
-

Code-level issues

-
    -
  1. -

    publisher.py:91-95zmq.Context(self._context) creates a shadowed sync context sharing the async context's io threads. Correct, but subtle. zmq.PUB is not thread-safe — calling pub.publish() from multiple asyncio tasks on the same socket is undefined. Needs docs or a lock.

    -
  2. -
  3. -

    publisher.py:117-118 — the publisher unlinks any existing socket file on startup. If two publishers on the same host use the same node name + topic, the second silently steals the socket. Should fail loudly.

    -
  4. -
  5. -

    subscriber.py:155-160 — fingerprint mismatch logs a warning and proceeds anyway. That is a silent-data-corruption path. Should refuse to connect.

    -
  6. -
  7. -

    messages/base.py:109-129_sequence_counter is class-level, shared across every Publisher instance of that message type in the process. Two publishers of ArrayMessage interleave sequences — breaking per-topic drop detection. Move it onto the Publisher.

    -
  8. -
  9. -

    utils/hashing.py:34-38field.type is a string with PEP 563 and a real type otherwise; the resulting fingerprint differs across import environments. Use typing.get_type_hints(cls) consistently.

    -
  10. -
  11. -

    discovery/client.py:78-101retries=1 default means zero retries (loop runs once). Fencepost bug.

    -
  12. -
  13. -

    core/executor.py:119-147RateExecutor has both await asyncio.sleep(0) inside the loop and await asyncio.sleep(max(0, dt)) at the bottom. The first is redundant and creates unnecessary wakeups. Catch-up logic silently eats dropped ticks; control loops often need to know.

    -
  14. -
  15. -

    discovery/daemon.py:87 — RCVTIMEO=1s means Ctrl-C takes up to 1s to take effect and request throughput is throttled. A zmq.Poller with a shutdown PAIR socket gives clean immediate shutdown.

    -
  16. -
  17. -

    messages/standard.py:146-150ImageMessage.__post_init__ auto-fill is non-idempotent across deserialization round-trips. Minor.

    -
  18. -
  19. -

    discovery/daemon.py:168-177 — same-publisher re-registration is allowed; if its IPC path changed, existing subscribers are never told. Needs a lease or a "changed" notification.

    -
  20. -
  21. -

    No CI test for cross-process fingerprint stability. Given how much safety rides on fingerprints, every standard message type deserves a stored golden fingerprint asserted in CI.

    -
  22. -
  23. -

    from_bytes vs from_frames asymmetry is a trap. Message.decode(bytes) only handles the inline path. If anyone captures bytes from the wire (the multipart path) and calls decode(), it will fail silently. Unify the paths or rename decode.

    -
  24. -
  25. -

    No async publish. send_multipart briefly blocks on HWM/context switch; inside an async timer callback this is a hidden blocking call. An async publish variant would help.

    -
  26. -
-

Schema evolution

-
    -
  1. No optional fields, no versioning. For long-lived robotics deployments, add:
      -
    • field defaults (so fingerprints tolerate missing trailing fields on decode),
    • -
    • an msg_schema_version: int = 1 convention,
    • -
    • eventually, a real wire schema (FlatBuffers, Cap'n Proto, or generated-from-.fbs dataclasses).
    • -
    -
  2. -
-

Summary

-

Cortex is a well-built, honest small-system IPC library. The serialization is genuinely fast — hitting memcpy-bandwidth on 4 MB arrays with zero-copy OOB frames. The latency floor (~550 µs p50, ~1.5 ms p99) is limited by asyncio, not zmq. The discovery, QoS, liveness, and single-host assumptions are the real blockers for using this as robotics middleware.

-

Recommended path if adopting Cortex for robotics:

-
    -
  1. Add per-topic QoS profiles with drop counters (1-2 days).
  2. -
  3. Add a synchronous-threaded subscriber option for low-latency control (1 day).
  4. -
  5. Add heartbeats/leases and multi-publisher support to discovery (3-5 days).
  6. -
  7. Add TCP transport and host-aware discovery (2-3 days).
  8. -
  9. Then consider shared memory and schema evolution.
  10. -
- - - - - - - - - - - - - - - -
-
- - - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/site/gen_ref_pages.py b/docs/site/gen_ref_pages.py deleted file mode 100644 index a6c4f5a..0000000 --- a/docs/site/gen_ref_pages.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Generate one API reference page per module under ``src/cortex/``. - -Executed by ``mkdocs-gen-files`` during the build. Emits: - -- ``reference//.md`` for every non-dunder module, -- ``reference//index.md`` for every ``__init__.py``, -- ``reference/SUMMARY.md`` consumed by ``mkdocs-literate-nav``. - -Keeping this generated means adding a new module needs zero doc edits. -""" - -from pathlib import Path - -import mkdocs_gen_files - -# This script lives at ``docs/gen_ref_pages.py`` and is executed by -# mkdocs-gen-files with the mkdocs.yml directory as cwd. Anchor to the -# repo root so the generator finds ``src/cortex`` regardless of cwd. -REPO_ROOT = Path(__file__).resolve().parent.parent -SRC_ROOT = REPO_ROOT / "src" -PACKAGE = "cortex" - -nav = mkdocs_gen_files.Nav() - -for path in sorted((SRC_ROOT / PACKAGE).rglob("*.py")): - module_path = path.relative_to(SRC_ROOT).with_suffix("") - doc_path = Path("reference", *module_path.parts[1:]).with_suffix(".md") - parts = tuple(module_path.parts) - - if parts[-1] == "__init__": - parts = parts[:-1] - doc_path = doc_path.with_name("index.md") - elif parts[-1].startswith("_"): - continue - - nav_parts = parts[1:] if parts[1:] else ("cortex",) - nav[nav_parts] = doc_path.relative_to("reference").as_posix() - - identifier = ".".join(parts) if parts else PACKAGE - with mkdocs_gen_files.open(doc_path, "w") as f: - f.write(f"# `{identifier}`\n\n") - f.write(f"::: {identifier}\n") - - mkdocs_gen_files.set_edit_path(doc_path, path.relative_to(REPO_ROOT)) - -with mkdocs_gen_files.open("reference/SUMMARY.md", "w") as f: - f.writelines(nav.build_literate_nav()) diff --git a/docs/site/getting-started/discovery-daemon/index.html b/docs/site/getting-started/discovery-daemon/index.html deleted file mode 100644 index 7a784d0..0000000 --- a/docs/site/getting-started/discovery-daemon/index.html +++ /dev/null @@ -1,1799 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Running the Discovery Daemon - Cortex - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - -
- - -
- -
- - - - - - - - - -
-
- - - -
-
-
- - - - - - - -
-
-
- - - -
-
- - - - - - - - -
- -
- - -
- - - - -
-
-
- - - -
- -
- - - - - - -

Running the Discovery Daemon

-

The discovery daemon is a lightweight REP service that maintains the registry -of active topics. Publishers register on startup; subscribers look up the -endpoint and connect directly.

-

Start

-
-
-
-
cortex-discovery
-
-
-
-
python -m cortex.discovery.daemon
-
-
-
-
/etc/systemd/system/cortex-discovery.service
[Unit]
-Description=Cortex discovery daemon
-After=network.target
-
-[Service]
-Type=simple
-ExecStart=/usr/bin/env cortex-discovery
-Restart=on-failure
-RuntimeDirectory=cortex
-
-[Install]
-WantedBy=multi-user.target
-
-
-
-
-

Command-line options

- - - - - - - - - - - - - - - - - - - - -
FlagDefaultDescription
--addressipc:///tmp/cortex/discovery.sockZMQ endpoint to bind
--log-levelINFODEBUG / INFO / WARNING / ERROR
-

Lifecycle

-
stateDiagram-v2
-    [*] --> Starting: bind REP socket
-    Starting --> Running: socket ready
-    Running --> Running: handle REGISTER / LOOKUP / LIST / UNREGISTER
-    Running --> Stopping: SIGINT or SHUTDOWN command
-    Stopping --> [*]: close socket, unlink ipc file
-

Troubleshooting

-
-
"Address already in use"
-
Another daemon (or a stale socket file) is holding the path. -rm /tmp/cortex/discovery.sock and restart.
-
Subscribers time out looking up topics
-
Daemon not running, or publisher failed to register. Run with ---log-level DEBUG and watch for REGISTER / LOOKUP lines.
-
Daemon crash leaves stale entries
-
Today, entries are only removed on explicit UNREGISTER. A crashed -publisher's topic stays in the registry pointing at a dead socket. -Restarting the daemon clears all state.
-
- - - - - - - - - - - - - - - -
-
- - - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/site/getting-started/installation/index.html b/docs/site/getting-started/installation/index.html deleted file mode 100644 index 6c99709..0000000 --- a/docs/site/getting-started/installation/index.html +++ /dev/null @@ -1,1758 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Installation - Cortex - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - -
- - -
- -
- - - - - - - - - -
-
- - - -
-
-
- - - - - - - -
-
-
- - - -
-
- - - - - - - - -
- -
- - -
- - - - -
-
-
- - - -
- -
- - - - - - -

Installation

-

Requirements

-
    -
  • Python 3.10+
  • -
  • Linux or macOS (Windows works but without uvloop)
  • -
  • ZeroMQ shared library (bundled via pyzmq)
  • -
-

Install from source

-
git clone https://github.com/sudoRicheek/cortex.git
-cd cortex
-pip install -e ".[dev]"
-
-

Optional extras

-
-
-
-
pip install -e ".[torch]"
-
-

Enables [TensorMessage][cortex.messages.standard.TensorMessage] and -torch-aware serialization paths.

-
-
-
pip install -e ".[all]"
-
-
-
-
-

Verify

-
import cortex
-print(cortex.__version__)
-
-

If that prints a version string, you're ready. Continue to the -Quickstart.

- - - - - - - - - - - - - - - -
-
- - - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/site/getting-started/quickstart/index.html b/docs/site/getting-started/quickstart/index.html deleted file mode 100644 index 548b08f..0000000 --- a/docs/site/getting-started/quickstart/index.html +++ /dev/null @@ -1,1809 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Quickstart - Cortex - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - -
- - -
- -
- - - - - - - - - -
-
- - - -
-
-
- - - - - - - -
-
-
- - - -
-
- - - - - - - - -
- -
- - -
- - - - -
-
-
- - - -
- -
- - - - - - -

Quickstart

-

A three-terminal pub/sub loop in under two minutes.

-

1. Start the discovery daemon

-
cortex-discovery
-
-

Leave it running. This is the single service that maps topic names to -IPC endpoints.

-

2. Publisher

-
pub.py
import numpy as np
-import cortex
-from cortex import Node, ArrayMessage
-
-
-class SensorNode(Node):
-    def __init__(self):
-        super().__init__("sensor")
-        self.pub = self.create_publisher("/sensor/data", ArrayMessage)
-        self.count = 0
-        self.create_timer(0.1, self.tick)  # 10 Hz
-
-    async def tick(self):
-        data = np.random.randn(64, 64).astype("float32")
-        self.pub.publish(ArrayMessage(data=data, name=f"frame_{self.count}"))
-        self.count += 1
-
-
-async def main():
-    node = SensorNode()
-    try:
-        await node.run()
-    finally:
-        await node.close()
-
-
-if __name__ == "__main__":
-    cortex.run(main())
-
-
python pub.py
-
-

3. Subscriber

-
sub.py
import cortex
-from cortex import Node, ArrayMessage
-from cortex.messages.base import MessageHeader
-
-
-async def on_data(msg: ArrayMessage, header: MessageHeader):
-    print(f"[{header.sequence}] {msg.name} shape={msg.data.shape}")
-
-
-class ViewerNode(Node):
-    def __init__(self):
-        super().__init__("viewer")
-        self.create_subscriber("/sensor/data", ArrayMessage, callback=on_data)
-
-
-async def main():
-    node = ViewerNode()
-    try:
-        await node.run()
-    finally:
-        await node.close()
-
-
-if __name__ == "__main__":
-    cortex.run(main())
-
-
python sub.py
-
-

What just happened

-
sequenceDiagram
-    participant P as Publisher
-    participant D as Discovery daemon
-    participant S as Subscriber
-
-    P->>D: REGISTER /sensor/data -> ipc:///tmp/cortex/topics/...
-    S->>D: LOOKUP /sensor/data
-    D-->>S: ipc:///tmp/cortex/topics/...
-    S->>P: ZMQ SUB connect + SUBSCRIBE "/sensor/data"
-    loop 10 Hz
-        P->>S: multipart [topic, header, metadata, buffer]
-        S->>S: decode + await on_data(msg, header)
-    end
-

See Concepts → Architecture for the end-to-end -picture, or jump into a custom message tutorial.

- - - - - - - - - - - - - - - -
-
- - - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/site/guides/benchmarks/index.html b/docs/site/guides/benchmarks/index.html deleted file mode 100644 index ac64a7b..0000000 --- a/docs/site/guides/benchmarks/index.html +++ /dev/null @@ -1,1734 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Benchmarks - Cortex - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - -
- - -
- -
- - - - - - - - - -
-
- - - -
-
-
- - - - - - - -
-
-
- - - -
-
- - - - - - - - -
- -
- - -
- - - - -
-
-
- - - -
- -
- - - - - - -

Benchmarks

-

Cortex ships an in-repo benchmark suite at benchmarks/.

-

Run

-
# Terminal 1
-cortex-discovery
-
-# Terminal 2
-python benchmarks/bench_all.py --output results.json
-
-

Individual benchmarks:

-
    -
  • benchmarks/bench_latency.py — one-way publisher→subscriber latency.
  • -
  • benchmarks/bench_throughput.py — messages/sec and MB/sec.
  • -
  • benchmarks/bench_all.py — full matrix with summary and optional JSON dump.
  • -
-

Reading results

-
    -
  • p99 is what matters for real-time-ish workloads; mean can hide jitter.
  • -
  • For array workloads, MB/s approaching memcpy bandwidth is a good sign - that zero-copy transport is working.
  • -
  • Serialization overhead via inproc sockets with copy=False is reported - separately — that isolates the encode/decode path from the network path.
  • -
-

Tips

-
    -
  • Pin publisher and subscriber to separate cores for stable latency numbers.
  • -
  • Disable Turbo-Boost / set CPU governor to performance for reproducible - runs.
  • -
  • Always measure with the discovery daemon also running (it is off the hot - path but can steal a little cache).
  • -
- - - - - - - - - - - - - - - -
-
- - - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/site/guides/debugging/index.html b/docs/site/guides/debugging/index.html deleted file mode 100644 index d3a5521..0000000 --- a/docs/site/guides/debugging/index.html +++ /dev/null @@ -1,1808 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Debugging - Cortex - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - -
- - -
- -
- - - - - - - - - -
-
- - - -
-
-
- - - - - - - -
-
-
- - - - - - - -
- -
- - - - - - -

Debugging

-

Subscriber hangs on startup

-

Most likely: the daemon is not running, or the topic name is mistyped. -DiscoveryClient.wait_for_topic_async polls every 500 ms until the topic -appears or the timeout fires.

-
cortex-discovery --log-level DEBUG
-
-

Watch for LOOKUP topic: /x -> NOT FOUND.

-

Publisher "works" but subscriber receives nothing

-

ZMQ PUB drops messages for which no matching SUB is connected yet. If your -publisher starts first and publishes immediately, the first few messages are -lost — this is the classic ZMQ slow-joiner problem.

-

Workarounds:

-
    -
  • Have the publisher wait briefly after bind before publishing the first message.
  • -
  • Have the subscriber wait-for-topic (the default) so it comes up after the - publisher registered.
  • -
-

Stale /tmp/cortex/topics/*.sock files

-

If a publisher exits uncleanly, its IPC socket file remains. Cortex's -Publisher._setup_socket unlinks any existing file at the same path on the -next bind — so restarting the publisher fixes it. Otherwise:

-
rm /tmp/cortex/topics/<stale-socket>.sock
-
-

Daemon state survives restarts — but doesn't

-

The registry is in-memory. Restarting the daemon wipes all state; -publishers do not auto-re-register today. Restart your publishers after -restarting the daemon.

-

Fingerprint mismatch warning

-

If you see -Message type mismatch for /x: expected FooMessage, got BarMessage — -the topic was registered with a different message class. Either rename the -topic or align the classes.

-

Debug logging

-
import logging
-logging.basicConfig(level=logging.DEBUG)
-
-

Cortex uses standard logging. Interesting loggers: cortex.publisher, -cortex.subscriber, cortex.node, cortex.discovery, cortex.discovery.client.

- - - - - - - - - - - - - - - -
-
- - - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/site/guides/performance-tuning/index.html b/docs/site/guides/performance-tuning/index.html deleted file mode 100644 index 4e04066..0000000 --- a/docs/site/guides/performance-tuning/index.html +++ /dev/null @@ -1,1772 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Performance tuning - Cortex - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - -
- - -
- -
- - - - - - - - - -
-
- - - -
-
-
- - - - - - - -
-
-
- - - -
-
- - - - - - - - -
- -
- - -
- - - - -
-
-
- - - -
- -
- - - - - - -

Performance tuning

-

Current measured numbers on the repo's benchmark suite (single workstation):

- - - - - - - - - - - - - - - - - - - - - - - - - -
WorkloadThroughput / latency
Small payload latencymean 556 µs, p99 1075 µs
1MB array throughput7.7k msg/s, 8.0 GB/s
4MB array throughput2.25k msg/s, 9.4 GB/s
1080p RGB1422 fps, 8.8 GB/s
-

See Benchmarks guide to reproduce.

-

Copy-on-use

-

Decoded NumPy arrays alias the ZMQ frame memory. That is what makes -large-payload throughput close to memcpy bandwidth — but it means:

-
    -
  • If you intend to mutate the array, arr = arr.copy() first.
  • -
  • If you intend to hold the array past the callback, copy it first.
  • -
-

Queue sizing

-

Per-socket HWM defaults to 10. Increase queue_size on high-rate producers -whose subscribers are known to be slow — but remember that ZMQ drops silently -at the HWM.

-

When to prefer the inline path

-

Single tiny messages (primitives only, < 1 KB) see no benefit from multipart. -The inline to_bytes path is still fine there. Publishers always use -multipart today.

-

uvloop

-

Installed by default on Unix. Drops tail latency on high-rate small messages -noticeably. No action needed.

- - - - - - - - - - - - - - - -
-
- - - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/site/index.html b/docs/site/index.html deleted file mode 100644 index 6a4eecb..0000000 --- a/docs/site/index.html +++ /dev/null @@ -1,1758 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - Cortex - Cortex - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - -
- - -
- -
- - - - - - - - - -
-
- - - -
-
-
- - - - - - - -
-
-
- - - -
-
- - - - - - - - -
- -
- - -
- - - - -
-
-
- - - -
- -
- - - - - - -

Cortex

-

A lightweight Python framework for inter-process communication over ZeroMQ.

-

Cortex is a pub/sub layer designed to feel obvious. Nodes publish typed messages on named topics; subscribers receive them via async callbacks. A tiny discovery daemon tells subscribers where to connect. Native support for NumPy arrays and PyTorch tensors keeps robotics- and ML-shaped payloads fast.

-
-
    -
  • -

    Getting started

    -

    Install, start the daemon, publish your first message in under two minutes.

    -
  • -
  • -

    Concepts

    -

    How the wire format, fingerprinting, discovery handshake, and async execution fit together.

    -
  • -
  • -

    Components

    -

    Deep dives into the Messages, Discovery, and Core modules.

    -
  • -
  • -

    API reference

    -

    Auto-generated from the source. Always matches the code on main.

    -
  • -
-
-

Highlights

-
    -
  • Publisher / Subscriber pattern over ZeroMQ PUB/SUB sockets.
  • -
  • Discovery service for automatic topic → endpoint resolution.
  • -
  • IPC transport with zero-copy frames for large NumPy / PyTorch payloads.
  • -
  • 64-bit fingerprint hashing for fast message-type identification.
  • -
  • uvloop-backed async on Linux/macOS for lower tail latency.
  • -
-

Minimal example

-
-
-
-
import numpy as np
-import cortex
-from cortex import Node, ArrayMessage
-
-
-class Cam(Node):
-    def __init__(self):
-        super().__init__("cam")
-        self.pub = self.create_publisher("/cam/frame", ArrayMessage)
-        self.create_timer(1 / 30, self.tick)
-
-    async def tick(self):
-        self.pub.publish(ArrayMessage(data=np.random.randn(480, 640).astype("f4")))
-
-
-cortex.run(Cam().run())
-
-
-
-
import cortex
-from cortex import Node, ArrayMessage
-from cortex.messages.base import MessageHeader
-
-
-async def on_frame(msg: ArrayMessage, header: MessageHeader):
-    print(f"seq={header.sequence} shape={msg.data.shape}")
-
-
-class Viewer(Node):
-    def __init__(self):
-        super().__init__("viewer")
-        self.create_subscriber("/cam/frame", ArrayMessage, callback=on_frame)
-
-
-cortex.run(Viewer().run())
-
-
-
-
-

Project status

-

Cortex targets single-host process graphs today. See design-review.md -and critique.md for an honest account of current limits and the -roadmap toward multi-host robotics use.

- - - - - - - - - - - - - - - -
-
- - - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/site/mkdocs.yml b/docs/site/mkdocs.yml deleted file mode 100644 index b72ae17..0000000 --- a/docs/site/mkdocs.yml +++ /dev/null @@ -1,140 +0,0 @@ -site_name: Cortex -site_description: Lightweight Python pub/sub over ZeroMQ, for robotics and beyond. -site_url: https://sudoRicheek.github.io/cortex/ -repo_url: https://github.com/sudoRicheek/cortex -repo_name: sudoRicheek/cortex -edit_uri: edit/main/docs/ - -docs_dir: . -site_dir: site -exclude_docs: | - mkdocs.yml - gen_ref_pages.py - site/ - -theme: - name: material - features: - - navigation.tabs - - navigation.sections - - navigation.indexes - - navigation.top - - navigation.footer - - content.code.copy - - content.code.annotate - - content.tabs.link - - search.suggest - - search.highlight - - toc.follow - palette: - - media: "(prefers-color-scheme: dark)" - scheme: slate - primary: indigo - accent: indigo - toggle: - icon: material/brightness-4 - name: Switch to light mode - - media: "(prefers-color-scheme: light)" - scheme: default - primary: indigo - accent: indigo - toggle: - icon: material/brightness-7 - name: Switch to dark mode - icon: - repo: fontawesome/brands/github - -markdown_extensions: - - admonition - - attr_list - - md_in_html - - def_list - - footnotes - - tables - - toc: - permalink: true - permalink_title: Anchor link - - pymdownx.details - - pymdownx.superfences: - custom_fences: - - name: mermaid - class: mermaid - format: !!python/name:pymdownx.superfences.fence_code_format - - pymdownx.tabbed: - alternate_style: true - - pymdownx.snippets - - pymdownx.highlight: - anchor_linenums: true - line_spans: __span - pygments_lang_class: true - - pymdownx.inlinehilite - - pymdownx.tasklist: - custom_checkbox: true - - pymdownx.emoji: - emoji_index: !!python/name:zensical.extensions.emoji.twemoji - emoji_generator: !!python/name:zensical.extensions.emoji.to_svg - -plugins: - - search - - section-index - - literate-nav: - nav_file: SUMMARY.md - - gen-files: - scripts: - - gen_ref_pages.py - - mkdocstrings: - default_handler: python - handlers: - python: - paths: [../src] - options: - docstring_style: google - show_source: true - show_root_heading: true - show_root_full_path: false - show_object_full_path: false - show_category_heading: true - members_order: source - show_signature_annotations: true - separate_signature: true - show_if_no_docstring: false - heading_level: 2 - filters: - - "!^_" - - "!^__" - -nav: - - Home: index.md - - Getting started: - - getting-started/installation.md - - getting-started/quickstart.md - - getting-started/discovery-daemon.md - - Concepts: - - concepts/architecture.md - - concepts/message-wire-format.md - - concepts/fingerprinting.md - - concepts/discovery-protocol.md - - concepts/transport-and-qos.md - - concepts/async-execution-model.md - - Components: - - components/messages.md - - components/discovery.md - - components/publisher-subscriber.md - - components/node-and-executors.md - - components/serialization.md - - Tutorials: - - tutorials/custom-messages.md - - tutorials/multi-node-system.md - - tutorials/numpy-and-images.md - - tutorials/pytorch-tensors.md - - Guides: - - guides/performance-tuning.md - - guides/benchmarks.md - - guides/debugging.md - - Critique: critique.md - - API reference: reference/ - -extra: - social: - - icon: fontawesome/brands/github - link: https://github.com/sudoRicheek/cortex diff --git a/docs/site/objects.inv b/docs/site/objects.inv deleted file mode 100644 index f2135e7b8cf342bae3bef9143f22efee22481b76..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 127 zcmXAeu?oUK5Cwx}@)HlzTUe&G7eTStA lightweight Python framework for inter-process communication over ZeroMQ.

Cortex is a pub/sub layer designed to feel obvious. Nodes publish typed messages on named topics; subscribers receive them via async callbacks. A tiny discovery daemon tells subscribers where to connect. Native support for NumPy arrays and PyTorch tensors keeps robotics- and ML-shaped payloads fast.

  • Getting started

    Install, start the daemon, publish your first message in under two minutes.

  • Concepts

    How the wire format, fingerprinting, discovery handshake, and async execution fit together.

  • Components

    Deep dives into the Messages, Discovery, and Core modules.

  • API reference

    Auto-generated from the source. Always matches the code on main.

","path":["Cortex"],"tags":[]},{"location":"#highlights","level":2,"title":"Highlights","text":"
  • Publisher / Subscriber pattern over ZeroMQ PUB/SUB sockets.
  • Discovery service for automatic topic → endpoint resolution.
  • IPC transport with zero-copy frames for large NumPy / PyTorch payloads.
  • 64-bit fingerprint hashing for fast message-type identification.
  • uvloop-backed async on Linux/macOS for lower tail latency.
","path":["Cortex"],"tags":[]},{"location":"#minimal-example","level":2,"title":"Minimal example","text":"PublisherSubscriber
import numpy as np\nimport cortex\nfrom cortex import Node, ArrayMessage\n\n\nclass Cam(Node):\n    def __init__(self):\n        super().__init__(\"cam\")\n        self.pub = self.create_publisher(\"/cam/frame\", ArrayMessage)\n        self.create_timer(1 / 30, self.tick)\n\n    async def tick(self):\n        self.pub.publish(ArrayMessage(data=np.random.randn(480, 640).astype(\"f4\")))\n\n\ncortex.run(Cam().run())\n
import cortex\nfrom cortex import Node, ArrayMessage\nfrom cortex.messages.base import MessageHeader\n\n\nasync def on_frame(msg: ArrayMessage, header: MessageHeader):\n    print(f\"seq={header.sequence} shape={msg.data.shape}\")\n\n\nclass Viewer(Node):\n    def __init__(self):\n        super().__init__(\"viewer\")\n        self.create_subscriber(\"/cam/frame\", ArrayMessage, callback=on_frame)\n\n\ncortex.run(Viewer().run())\n
","path":["Cortex"],"tags":[]},{"location":"#project-status","level":2,"title":"Project status","text":"

Cortex targets single-host process graphs today. See design-review.md and critique.md for an honest account of current limits and the roadmap toward multi-host robotics use.

","path":["Cortex"],"tags":[]},{"location":"critique/","level":1,"title":"Cortex Critique","text":"

A bottom-up review of Cortex as it stands today, with a focus on its viability as a communication library for robotics. This complements design-review.md with concrete code-level findings and benchmark observations.

","path":["Cortex Critique"],"tags":[]},{"location":"critique/#how-cortex-works-bottom-up","level":2,"title":"How Cortex works (bottom-up)","text":"","path":["Cortex Critique"],"tags":[]},{"location":"critique/#1-fingerprinting-utilshashingpy","level":3,"title":"1. Fingerprinting — utils/hashing.py","text":"

A message class's identity is a 64-bit integer:

fingerprint = SHA-256(f\"{module}.{qualname}|{','.join(sorted('field:type'))}\")[:8]\n
  • Computed lazily and cached in _fingerprint_cache.
  • field.type is a string when from __future__ import annotations is active and a real type otherwise. The fingerprint therefore depends on how the module was imported — fragile for cross-repo use.
  • Field ordering is sorted alphabetically in the fingerprint, but the wire layout uses dataclass declaration order. Two classes could theoretically fingerprint identically but interpret the wire differently.
","path":["Cortex Critique"],"tags":[]},{"location":"critique/#2-message-base-messagesbasepy","level":3,"title":"2. Message base — messages/base.py","text":"

Each dataclass inheriting Message is auto-registered via __init_subclass__ into MessageType._registry[fingerprint] = cls.

Wire format (multipart transport, what publishers actually use):

Frame 0: topic bytes            (for PUB/SUB filter)\nFrame 1: 24-byte header         (fingerprint u64, timestamp_ns u64, sequence u64, big-endian)\nFrame 2: msgpack of ordered field values with OOB descriptors\nFrame 3..N: raw contiguous array buffers (zero-copy)\n

There is a second, legacy single-blob path (to_bytes / from_bytes) that embeds array bytes inside a single msgpack blob using ExtType. It is retained for Message.decode(...) and tests, but is not what the transport uses.

","path":["Cortex Critique"],"tags":[]},{"location":"critique/#3-serialization-utilsserializationpy","level":3,"title":"3. Serialization — utils/serialization.py","text":"

Two strategies coexist:

  • _msgpack_default / _msgpack_ext_hook (inline): arrays/tensors get packed as msgpack ExtType inside the single blob. Used by the legacy path.
  • _encode_transport_value / _decode_transport_value (out-of-band): each array/tensor is replaced with a tiny dict {__cortex_oob__: \"numpy\", buffer: i, dtype, shape} and its raw bytes are appended as separate ZMQ frames. Reconstruction uses np.frombuffer(frame.buffer, dtype).reshape(shape) with no copy.

After the March 2026 optimizations: zero-copy decode, schema-ordered values (field names no longer repeated per message), and cached field-name tuples.

","path":["Cortex Critique"],"tags":[]},{"location":"critique/#4-discovery-discoverydaemonpy-and-discoveryclientpy","level":3,"title":"4. Discovery — discovery/daemon.py and discovery/client.py","text":"

Single-threaded zmq.REP over IPC at ipc:///tmp/cortex/discovery.sock.

  • Registry is a plain dict[str, TopicInfo], enforcing one publisher per topic.
  • RCVTIMEO=1s so the run loop can poll _running for Ctrl-C.
  • Commands: REGISTER, UNREGISTER, LOOKUP, LIST, SHUTDOWN.
  • Request/response payloads are msgpack.
  • Client uses REQ with close-and-recreate on timeout (REQ sockets are stuck after a missed reply).
","path":["Cortex Critique"],"tags":[]},{"location":"critique/#5-publisher-subscriber-corepublisherpy-coresubscriberpy","level":3,"title":"5. Publisher / Subscriber — core/publisher.py, core/subscriber.py","text":"
  • Publisher: binds a zmq.PUB at ipc:///tmp/cortex/topics/<node>__<topic>.sock, registers via the discovery client, publishes multipart [topic, header, metadata, *buffers] with zmq.NOBLOCK. If the Node hands it an async context, it wraps a sync zmq.Context(self._context) around the same underlying zmq io threads so publishing stays synchronous.
  • Subscriber: uses an async context, looks up the topic (optionally waits), connects zmq.SUB, sets a topic filter, loops via AsyncExecutor doing recv_multipart(copy=False)Message.from_frames.
","path":["Cortex Critique"],"tags":[]},{"location":"critique/#6-node-executors-corenodepy-coreexecutorpy","level":3,"title":"6. Node + Executors — core/node.py, core/executor.py","text":"

A Node owns a shared zmq.asyncio.Context, plus lists of publishers, subscribers, and timers. Each timer gets a RateExecutor(fn, rate_hz). node.run() creates asyncio tasks for every timer and every callback-subscriber, then asyncio.gather. RateExecutor uses perf_counter plus asyncio.sleep(max(0, next-now)). cortex.run prefers uvloop on Unix.

","path":["Cortex Critique"],"tags":[]},{"location":"critique/#benchmark-results","level":2,"title":"Benchmark results","text":"

Measured on this machine with the in-repo benchmark suite:

Metric Value Small-payload latency mean 556 µs, p99 1075 µs 64KB latency mean 919 µs, p99 1.4 ms Tiny array throughput 21.8k msg/s 1MB array throughput 7.7k msg/s, 8.0 GB/s 4MB array throughput 2.25k msg/s, 9.4 GB/s 1080p RGB frames 1422 fps, 8.8 GB/s Raw wire+decode (inproc) 35 µs roundtrip (4MB array)

The delta between the ~35 µs raw wire and ~550 µs end-to-end is asyncio scheduling, context-switch between publisher timer and subscriber recv, and Python callback dispatch. Serialization is close to memcpy-bandwidth on large payloads — the OOB transport is pulling its weight.

","path":["Cortex Critique"],"tags":[]},{"location":"critique/#what-can-be-improved","level":2,"title":"What can be improved","text":"","path":["Cortex Critique"],"tags":[]},{"location":"critique/#design-level-biggest-wins","level":3,"title":"Design-level (biggest wins)","text":"
  1. Latency floor is too high for control loops. ~550 µs mean and ~1.5 ms p99 is dominated by asyncio + zmq.asyncio, not zmq itself. Control topics should be able to opt into a synchronous thread-plus-zmq.Poller receive path targeting <100 µs p99. Async should be the default, not the only option.

  2. Discovery is a single REQ/REP chokepoint with stop-the-world semantics. On crashes, stale topic entries are never reclaimed — a crashed publisher's IPC file stays on disk and the registry keeps pointing at a dead socket. Add leases with heartbeats (publisher renews every N seconds; daemon evicts stale entries), or a peer-gossip model where every node beacons presence. The current daemon has no concurrency — one slow client blocks all others.

  3. One-publisher-per-topic is a hard limit for robotics. Redundant IMUs, failover, and multi-source fusion are all blocked. The registry should accept N publishers per topic and subscribers should connect() to all of them — ZMQ SUB handles fan-in natively.

  4. No backpressure semantics. pub.publish() is NOBLOCK and silently drops on HWM. Subscriber HWM=10 on SUB evicts old messages by default. Robotics needs per-topic QoS profiles similar to DDS:

  5. best_effort_latest — camera frames: drop old, keep newest (ZMQ_CONFLATE=1).
  6. reliable_queue — commands: block or surface an error.
  7. dropping_queue — telemetry: current behavior, but with a drop counter.

  8. No liveness or drop detection. A subscriber has no way to know the publisher died. Sequence numbers exist in the header but are never checked for gaps. Automatic gap-counting in Subscriber would be gold for debugging.

  9. Callback execution blocks the receive loop. A 10 ms callback accumulates on SUB HWM and drops. Receive, decode, and user-callback execution should be decoupled with a bounded work queue and one or more worker coroutines/threads per subscriber. ROS 2 executors have this distinction for a reason.

  10. Local-only transport in practice. Addresses are hardcoded ipc:// paths under /tmp. Multi-host robotics (robot ↔ base-station) needs TCP transport in discovery, NIC selection, and topology-aware addressing.

  11. No shared memory for huge payloads. At 9 GB/s on 4 MB arrays, every subscriber gets a fresh copy. For multi-subscriber camera or LiDAR fan-out, a shared-memory transport (posix shm + ring buffer + zmq for control-plane notifications) would give true zero-copy.

","path":["Cortex Critique"],"tags":[]},{"location":"critique/#code-level-issues","level":3,"title":"Code-level issues","text":"
  1. publisher.py:91-95zmq.Context(self._context) creates a shadowed sync context sharing the async context's io threads. Correct, but subtle. zmq.PUB is not thread-safe — calling pub.publish() from multiple asyncio tasks on the same socket is undefined. Needs docs or a lock.

  2. publisher.py:117-118 — the publisher unlinks any existing socket file on startup. If two publishers on the same host use the same node name + topic, the second silently steals the socket. Should fail loudly.

  3. subscriber.py:155-160 — fingerprint mismatch logs a warning and proceeds anyway. That is a silent-data-corruption path. Should refuse to connect.

  4. messages/base.py:109-129_sequence_counter is class-level, shared across every Publisher instance of that message type in the process. Two publishers of ArrayMessage interleave sequences — breaking per-topic drop detection. Move it onto the Publisher.

  5. utils/hashing.py:34-38field.type is a string with PEP 563 and a real type otherwise; the resulting fingerprint differs across import environments. Use typing.get_type_hints(cls) consistently.

  6. discovery/client.py:78-101retries=1 default means zero retries (loop runs once). Fencepost bug.

  7. core/executor.py:119-147RateExecutor has both await asyncio.sleep(0) inside the loop and await asyncio.sleep(max(0, dt)) at the bottom. The first is redundant and creates unnecessary wakeups. Catch-up logic silently eats dropped ticks; control loops often need to know.

  8. discovery/daemon.py:87 — RCVTIMEO=1s means Ctrl-C takes up to 1s to take effect and request throughput is throttled. A zmq.Poller with a shutdown PAIR socket gives clean immediate shutdown.

  9. messages/standard.py:146-150ImageMessage.__post_init__ auto-fill is non-idempotent across deserialization round-trips. Minor.

  10. discovery/daemon.py:168-177 — same-publisher re-registration is allowed; if its IPC path changed, existing subscribers are never told. Needs a lease or a \"changed\" notification.

  11. No CI test for cross-process fingerprint stability. Given how much safety rides on fingerprints, every standard message type deserves a stored golden fingerprint asserted in CI.

  12. from_bytes vs from_frames asymmetry is a trap. Message.decode(bytes) only handles the inline path. If anyone captures bytes from the wire (the multipart path) and calls decode(), it will fail silently. Unify the paths or rename decode.

  13. No async publish. send_multipart briefly blocks on HWM/context switch; inside an async timer callback this is a hidden blocking call. An async publish variant would help.

","path":["Cortex Critique"],"tags":[]},{"location":"critique/#schema-evolution","level":3,"title":"Schema evolution","text":"
  1. No optional fields, no versioning. For long-lived robotics deployments, add:
    • field defaults (so fingerprints tolerate missing trailing fields on decode),
    • an msg_schema_version: int = 1 convention,
    • eventually, a real wire schema (FlatBuffers, Cap'n Proto, or generated-from-.fbs dataclasses).
","path":["Cortex Critique"],"tags":[]},{"location":"critique/#summary","level":2,"title":"Summary","text":"

Cortex is a well-built, honest small-system IPC library. The serialization is genuinely fast — hitting memcpy-bandwidth on 4 MB arrays with zero-copy OOB frames. The latency floor (~550 µs p50, ~1.5 ms p99) is limited by asyncio, not zmq. The discovery, QoS, liveness, and single-host assumptions are the real blockers for using this as robotics middleware.

Recommended path if adopting Cortex for robotics:

  1. Add per-topic QoS profiles with drop counters (1-2 days).
  2. Add a synchronous-threaded subscriber option for low-latency control (1 day).
  3. Add heartbeats/leases and multi-publisher support to discovery (3-5 days).
  4. Add TCP transport and host-aware discovery (2-3 days).
  5. Then consider shared memory and schema evolution.
","path":["Cortex Critique"],"tags":[]},{"location":"components/discovery/","level":1,"title":"Discovery","text":"

Source: cortex.discovery.daemon, cortex.discovery.client, cortex.discovery.protocol

Discovery is Cortex's control plane: a single long-lived process that maps topic names to ZMQ endpoints. It sits off the data path — once a subscriber has an endpoint, messages flow publisher → subscriber directly without the daemon's involvement.

","path":["Components","Discovery"],"tags":[]},{"location":"components/discovery/#moving-parts","level":2,"title":"Moving parts","text":"
flowchart LR\n    subgraph DP[discovery package]\n        PR[protocol.py<br/>DiscoveryRequest /<br/>DiscoveryResponse /<br/>TopicInfo]\n        DM[daemon.py<br/>DiscoveryDaemon<br/>ZMQ REP loop]\n        CL[client.py<br/>DiscoveryClient<br/>ZMQ REQ wrapper]\n    end\n\n    CL -- msgpack REQ --> DM\n    DM -- msgpack REP --> CL\n    PR -.-> DM\n    PR -.-> CL

Everyone agrees on the wire format via protocol.py. The daemon runs a single-threaded REP loop. The client speaks REQ from every publisher and subscriber in the graph.

","path":["Components","Discovery"],"tags":[]},{"location":"components/discovery/#daemon","level":2,"title":"Daemon","text":"

Implemented in DiscoveryDaemon.

Key behaviors:

  • Binds zmq.REP at ipc:///tmp/cortex/discovery.sock by default.
  • Maintains _topics: dict[str, TopicInfo] — one publisher per topic.
  • RCVTIMEO=1000 on the socket so the loop can check _running for clean Ctrl-C. This also means the daemon is naturally single-request-at-a-time — a slow client blocks all others.
","path":["Components","Discovery"],"tags":[]},{"location":"components/discovery/#state-transitions","level":3,"title":"State transitions","text":"
stateDiagram-v2\n    [*] --> Starting\n    Starting --> Running: bind OK\n    Running --> Running: REGISTER → insert\n    Running --> Running: LOOKUP → read\n    Running --> Running: UNREGISTER → delete\n    Running --> Running: LIST → snapshot\n    Running --> Stopping: SIGINT / SHUTDOWN\n    Stopping --> [*]: close socket, unlink .sock
","path":["Components","Discovery"],"tags":[]},{"location":"components/discovery/#registry-semantics","level":3,"title":"Registry semantics","text":"Case Result New topic Insert → OK Same topic, same publisher_node Overwrite → OK (re-registration) Same topic, different publisher_node Reject → ALREADY_EXISTS UNREGISTER missing topic NOT_FOUND","path":["Components","Discovery"],"tags":[]},{"location":"components/discovery/#client","level":2,"title":"Client","text":"

Implemented in DiscoveryClient.

Thin REQ wrapper around the protocol. Important operational detail: REQ sockets stick after a timeout — they block subsequent sends waiting for a reply that never came. The client handles this by closing and recreating the socket on every timeout (_reconnect). Callers don't see it.

","path":["Components","Discovery"],"tags":[]},{"location":"components/discovery/#req-timeout-recovery","level":3,"title":"REQ timeout recovery","text":"
flowchart TD\n    S[send request] --> W[wait RCVTIMEO]\n    W -->|reply| OK[return DiscoveryResponse]\n    W -->|timeout| T[zmq.Again]\n    T --> C[close REQ socket]\n    C --> N[create fresh REQ<br/>same endpoint]\n    N -->|attempts < retries| S\n    N -->|exhausted| F[raise TimeoutError]
","path":["Components","Discovery"],"tags":[]},{"location":"components/discovery/#polling-helpers","level":3,"title":"Polling helpers","text":"
  • lookup_topic(name) — one-shot, returns None on miss.
  • wait_for_topic(name, timeout, poll_interval) — blocking poll loop (time.sleep).
  • wait_for_topic_async(name, timeout, poll_interval) — async poll loop (asyncio.sleep). This is what Subscriber uses when wait_for_topic=True.
","path":["Components","Discovery"],"tags":[]},{"location":"components/discovery/#protocol","level":2,"title":"Protocol","text":"

Implemented in cortex.discovery.protocol.

Type Purpose DiscoveryCommand REGISTER_TOPIC / UNREGISTER_TOPIC / LOOKUP_TOPIC / LIST_TOPICS / SHUTDOWN DiscoveryStatus OK / NOT_FOUND / ALREADY_EXISTS / ERROR TopicInfo name, address, message_type, fingerprint, publisher_node DiscoveryRequest command + optional topic_info / topic_name DiscoveryResponse status, message, topic_info, topics

All payloads are msgpack. TopicInfo is nested as a packed sub-blob so discovery responses stay flat.

","path":["Components","Discovery"],"tags":[]},{"location":"components/discovery/#known-limitations","level":2,"title":"Known limitations","text":"

Summarized here, detailed in critique.md:

  • One-publisher-per-topic.
  • No heartbeats or leases — crashed publishers leave stale entries.
  • Single-threaded REP — slow client starves others.
  • retries=1 in the client is a fencepost; effective retries today is zero.
  • Daemon state lost on restart; publishers do not auto-re-register.
","path":["Components","Discovery"],"tags":[]},{"location":"components/discovery/#see-also","level":2,"title":"See also","text":"
  • Concepts → Discovery protocol
  • Getting started → Running the discovery daemon
  • Critique
","path":["Components","Discovery"],"tags":[]},{"location":"components/messages/","level":1,"title":"Messages","text":"

Source: cortex.messages.base, cortex.messages.standard

Messages are just @dataclasses that inherit from Message. Registering with the type system, computing a fingerprint, and (de)serialization all happen automatically.

","path":["Components","Messages"],"tags":[]},{"location":"components/messages/#anatomy-of-a-message","level":2,"title":"Anatomy of a message","text":"
classDiagram\n    class Message {\n        +fingerprint() int\n        +to_bytes() bytes\n        +to_frames() list\n        +from_bytes(data) tuple\n        +from_frames(frames) tuple\n        +decode(bytes) tuple [static]\n        -_build_header()\n        -_field_names() tuple\n        -_field_values() list\n        -_next_sequence() int\n    }\n    class MessageHeader {\n        +fingerprint: int\n        +timestamp_ns: int\n        +sequence: int\n        +to_bytes() bytes\n        +from_bytes(data) MessageHeader\n        +size() int\n    }\n    class MessageType {\n        +register(cls)\n        +get(fingerprint) type\n        +get_all() dict\n    }\n    Message ..> MessageHeader : emits\n    Message ..> MessageType : auto-registers on subclass
","path":["Components","Messages"],"tags":[]},{"location":"components/messages/#defining-a-custom-message","level":2,"title":"Defining a custom message","text":"
from dataclasses import dataclass\nimport numpy as np\nfrom cortex.messages.base import Message\n\n@dataclass\nclass JointTrajectory(Message):\n    timestamp: float\n    positions: np.ndarray   # shape (N,)\n    velocities: np.ndarray  # shape (N,)\n    frame_id: str = \"\"\n

That is the entire contract. The class is registered into MessageType._registry by fingerprint at import time, and gains:

  • JointTrajectory.fingerprint() — 64-bit ID.
  • msg.to_frames() / JointTrajectory.from_frames(frames) — the transport path.
  • msg.to_bytes() / JointTrajectory.from_bytes(data) — the legacy blob path.
  • Message.decode(blob) — class dispatch via fingerprint registry.
","path":["Components","Messages"],"tags":[]},{"location":"components/messages/#sequence-numbering","level":2,"title":"Sequence numbering","text":"

Class-level counter

Message._sequence_counter is shared across all publisher instances of the same message class in the process. Two ArrayMessage publishers interleave sequence numbers. Per-topic gap detection therefore needs a per-publisher counter today; see critique.md § 12.

","path":["Components","Messages"],"tags":[]},{"location":"components/messages/#built-in-messages","level":2,"title":"Built-in messages","text":"Class Use for StringMessage Plain strings IntMessage / FloatMessage Single scalars BytesMessage Opaque binary DictMessage Nested dicts with arrays/tensors ListMessage Mixed-type lists ArrayMessage Single NumPy array + name / frame_id MultiArrayMessage dict[str, np.ndarray] (e.g. points+colors) TensorMessage PyTorch tensor (preserves device/grad) MultiTensorMessage Named tensor bundle (model I/O) ImageMessage Image + encoding + width/height PointCloudMessage XYZ + optional RGB / intensity / normals PoseMessage 6-DoF pose (position + quaternion) TransformMessage 4×4 homogeneous transform TimestampMessage / HeaderMessage ROS-style stamps","path":["Components","Messages"],"tags":[]},{"location":"components/messages/#encode-decode-lifecycle","level":2,"title":"Encode / decode lifecycle","text":"
flowchart LR\n    A[User builds dataclass] --> B[Publisher.publish]\n    B --> C[message.to_frames]\n    C --> D[[ZMQ multipart send]]\n    D --> E[[ZMQ multipart recv]]\n    E --> F[Message.from_frames]\n    F --> G[user callback msg, header]
","path":["Components","Messages"],"tags":[]},{"location":"components/messages/#see-also","level":2,"title":"See also","text":"
  • Concept: message wire format
  • Concept: fingerprinting
  • Tutorial: custom messages
","path":["Components","Messages"],"tags":[]},{"location":"components/node-and-executors/","level":1,"title":"Node & Executors","text":"

Source: cortex.core.node, cortex.core.executor

A Node is the user-facing composition unit: it owns a shared ZMQ async context and a collection of publishers, subscribers, and timers. Executors provide the scheduling primitives that timers and subscriber receive loops run on.

","path":["Components","Node & Executors"],"tags":[]},{"location":"components/node-and-executors/#responsibilities","level":2,"title":"Responsibilities","text":"
flowchart TB\n    subgraph NodeResp[Node]\n        CTX[shared zmq.asyncio.Context]\n        PUBS[Publishers dict]\n        SUBS[Subscribers dict]\n        TIMERS[Timers list]\n    end\n\n    NodeResp -- create_publisher --> P[Publisher]\n    NodeResp -- create_subscriber --> S[Subscriber]\n    NodeResp -- create_timer --> RE[RateExecutor]\n    NodeResp -- run / close --> Lifecycle\n\n    P -. uses .-> CTX\n    S -. uses .-> CTX

One node = one process boundary in practice. Nothing stops you running multiple nodes in the same process (asyncio.gather([n.run() for n in nodes]), see examples/multi_node_system.py), but remember they share the same event loop — a slow callback in one still blocks the others.

","path":["Components","Node & Executors"],"tags":[]},{"location":"components/node-and-executors/#lifecycle","level":2,"title":"Lifecycle","text":"
stateDiagram-v2\n    [*] --> Constructed: Node(name)\n    Constructed --> Configured: create_publisher/subscriber/timer\n    Configured --> Running: await node.run()\n    Running --> Running: timers fire, callbacks dispatch\n    Running --> Stopping: node.stop() or cancel\n    Stopping --> Closed: await node.close()\n    Closed --> [*]: context terminated
","path":["Components","Node & Executors"],"tags":[]},{"location":"components/node-and-executors/#noderun","level":3,"title":"node.run()","text":"

Spawns one asyncio task per timer and one per callback-bearing subscriber, then asyncio.gathers them. Returns when all tasks complete or the node is stopped.

async with Node(\"my_node\") as node:\n    node.create_publisher(\"/x\", IntMessage)\n    node.create_subscriber(\"/y\", IntMessage, callback=on_y)\n    await node.run()   # blocks until cancelled\n# __aexit__ calls close() automatically\n
","path":["Components","Node & Executors"],"tags":[]},{"location":"components/node-and-executors/#nodeclose","level":3,"title":"node.close()","text":"

Stops all executors, cancels outstanding tasks, closes every publisher and subscriber (each of which unregisters/unbinds their own socket), and terminates the shared ZMQ context. Idempotent.

","path":["Components","Node & Executors"],"tags":[]},{"location":"components/node-and-executors/#executors","level":2,"title":"Executors","text":"

Two flavours, both subclasses of BaseExecutor.

classDiagram\n    class BaseExecutor {\n        <<abstract>>\n        +func: AsyncCallback\n        +start()\n        +stop()\n        +run(*args, **kwargs)\n        #_run_impl()*\n    }\n    class AsyncExecutor {\n        +_run_impl()\n    }\n    class RateExecutor {\n        +rate_hz: float\n        +interval: float\n        +_run_impl()\n    }\n    BaseExecutor <|-- AsyncExecutor\n    BaseExecutor <|-- RateExecutor
","path":["Components","Node & Executors"],"tags":[]},{"location":"components/node-and-executors/#asyncexecutor","level":3,"title":"AsyncExecutor","text":"

\"Run this coroutine as fast as possible, yielding between iterations.\"

flowchart LR\n    Start --> Check{running?}\n    Check -- no --> End\n    Check -- yes --> Call[await func]\n    Call -- exception --> Log[log error]\n    Log --> Sleep\n    Call --> Sleep[await sleep 0]\n    Sleep --> Check

Used by Subscriber.run to drive the receive-dispatch loop.

","path":["Components","Node & Executors"],"tags":[]},{"location":"components/node-and-executors/#rateexecutor","level":3,"title":"RateExecutor","text":"

\"Run this coroutine at a constant rate, catching up on overruns.\"

flowchart TD\n    Start[next = perf_counter] --> Loop{running?}\n    Loop -- no --> End\n    Loop -- yes --> Now[now = perf_counter]\n    Now --> Due{now >= next?}\n    Due -- yes --> Call[await func]\n    Call --> Advance[next += interval]\n    Advance --> Behind{next < now?}\n    Behind -- yes --> Reset[next = now + interval]\n    Behind -- no --> Wait\n    Reset --> Wait\n    Due -- no --> Wait[await sleep next - now]\n    Wait --> Loop

The catch-up branch silently drops ticks — if your 100 Hz callback takes 20 ms once, you do not get two callbacks back-to-back; you skip one tick.

Redundant yield

Today there is an await asyncio.sleep(0) inside the loop and await asyncio.sleep(max(0, dt)) at the bottom. That generates an extra wakeup per tick. See critique § 15.

","path":["Components","Node & Executors"],"tags":[]},{"location":"components/node-and-executors/#timer-usage","level":2,"title":"Timer usage","text":"
node.create_timer(1.0 / 30, self.publish_frame)   # 30 Hz\nnode.create_timer(1.0, self.log_stats)            # 1 Hz\n

Timers are plain async functions — no decorator, no magic. They run in the same event loop as subscriber callbacks, so the same head-of-line caveat applies.

","path":["Components","Node & Executors"],"tags":[]},{"location":"components/node-and-executors/#shared-zmq-context","level":2,"title":"Shared ZMQ context","text":"

Every publisher and subscriber created through a node reuses the node's zmq.asyncio.Context. This means:

  • Socket creation is cheap.
  • io threads are shared across all sockets in the node.
  • Terminating the node's context cleanly shuts down all its sockets.

Do not create your own context inside callbacks; you'll leak resources and defeat the shared-io-thread optimization.

","path":["Components","Node & Executors"],"tags":[]},{"location":"components/node-and-executors/#minimal-complete-node","level":2,"title":"Minimal complete node","text":"
from dataclasses import dataclass\nimport numpy as np\nimport cortex\nfrom cortex import Node, Message\nfrom cortex.messages.base import MessageHeader\n\n\n@dataclass\nclass Ping(Message):\n    payload: np.ndarray\n    counter: int\n\n\nclass Echo(Node):\n    def __init__(self):\n        super().__init__(\"echo\")\n        self.pub = self.create_publisher(\"/pong\", Ping)\n        self.create_subscriber(\"/ping\", Ping, callback=self.on_ping)\n        self._n = 0\n\n    async def on_ping(self, msg: Ping, header: MessageHeader):\n        self._n += 1\n        self.pub.publish(Ping(payload=msg.payload, counter=self._n))\n\n\nasync def main():\n    async with Echo() as node:\n        await node.run()\n\n\nif __name__ == \"__main__\":\n    cortex.run(main())\n
","path":["Components","Node & Executors"],"tags":[]},{"location":"components/node-and-executors/#see-also","level":2,"title":"See also","text":"
  • cortex.core.node
  • cortex.core.executor
  • Concepts → Async execution model
  • Components → Publisher & Subscriber
","path":["Components","Node & Executors"],"tags":[]},{"location":"components/publisher-subscriber/","level":1,"title":"Publisher & Subscriber","text":"

Source: cortex.core.publisher, cortex.core.subscriber

The data-plane workhorses. A Publisher binds a ZMQ PUB socket and registers with discovery; a Subscriber looks up the endpoint, connects a SUB socket, and drives an async receive loop. Discovery is consulted once per topic on startup — it is not on the hot path.

","path":["Components","Publisher & Subscriber"],"tags":[]},{"location":"components/publisher-subscriber/#relationship-to-the-rest-of-the-stack","level":2,"title":"Relationship to the rest of the stack","text":"
flowchart LR\n    Node -.owns.-> P[Publisher]\n    Node -.owns.-> S[Subscriber]\n    P -- register --> DC1[DiscoveryClient]\n    S -- lookup --> DC2[DiscoveryClient]\n    P -- send_multipart --> Sock1[(zmq.PUB<br/>IPC)]\n    Sock1 -. IPC .-> Sock2[(zmq.SUB)]\n    S -- recv_multipart --> Sock2\n    M[Message] -- to_frames --> P\n    S -- from_frames --> M
","path":["Components","Publisher & Subscriber"],"tags":[]},{"location":"components/publisher-subscriber/#publisher","level":2,"title":"Publisher","text":"","path":["Components","Publisher & Subscriber"],"tags":[]},{"location":"components/publisher-subscriber/#construction","level":3,"title":"Construction","text":"

Always create via Node.create_publisher — direct construction works but skips the shared ZMQ context reuse and the node-level registration bookkeeping.

pub = node.create_publisher(\n    topic_name=\"/camera/image\",   # must start with \"/\"\n    message_type=ImageMessage,    # fingerprint is taken from this class\n    queue_size=100,               # SNDHWM; drops under backpressure\n)\n
","path":["Components","Publisher & Subscriber"],"tags":[]},{"location":"components/publisher-subscriber/#startup-sequence","level":3,"title":"Startup sequence","text":"
sequenceDiagram\n    autonumber\n    participant U as User\n    participant Pub as Publisher\n    participant FS as /tmp/cortex/topics/\n    participant ZMQ as zmq.PUB\n    participant D as Discovery daemon\n\n    U->>Pub: __init__(topic, msg_cls, ...)\n    Pub->>Pub: address = generate_ipc_address(topic, node)\n    Pub->>FS: mkdir -p; unlink stale .sock\n    Pub->>ZMQ: socket(PUB); setsockopt HWM/LINGER; bind(address)\n    Pub->>D: REGISTER TopicInfo{name, address, fingerprint, node}\n    D-->>Pub: OK / ALREADY_EXISTS\n    Note over Pub: ready; user can publish()

Two things worth calling out:

  1. The IPC address is derived deterministically from node_name and topic_name via generate_ipc_address: ipc:///tmp/cortex/topics/<node>__<topic-with-slashes-as-underscores>.sock.
  2. _setup_socket unlinks any existing file at that path before binding. That protects against crash-leftover sockets, but also means two publishers configured with the same node_name + topic_name in the same process tree will silently stomp each other — see critique § 10.
","path":["Components","Publisher & Subscriber"],"tags":[]},{"location":"components/publisher-subscriber/#publish-path","level":3,"title":"Publish path","text":"
flowchart LR\n    Msg[Message dataclass] --> H[build MessageHeader<br/>fp, ts, seq]\n    Msg --> V[serialize_message_frames<br/>values]\n    H --> F[[frame 1: header 24B]]\n    V --> F2[[frame 2: msgpack metadata]]\n    V --> FN[[frames 3..N: array buffers]]\n    T[[frame 0: topic bytes]]\n    F --> Send\n    F2 --> Send\n    FN --> Send\n    T --> Send\n    Send[send_multipart NOBLOCK] -->|success| Pub[publish count++]\n    Send -->|zmq.Again| Drop[return False]

publish() is synchronous and returns a boolean:

  • True — handed to ZMQ successfully.
  • Falsezmq.Again, queue full, message dropped.

Any other exception is logged and swallowed; publish still returns False. For robotics code this \"fire and forget\" is intentional — the caller decides whether to retry based on the return value and the topic's role.

","path":["Components","Publisher & Subscriber"],"tags":[]},{"location":"components/publisher-subscriber/#async-context-quirk","level":3,"title":"Async context quirk","text":"

Node owns a zmq.asyncio.Context. The Publisher constructor detects this and wraps a sync zmq.Context around the same underlying io threads:

if isinstance(self._context, zmq.asyncio.Context):\n    self._context: zmq.Context = zmq.Context(self._context)\n

This keeps publish() a normal function call instead of forcing every publish to be awaited. It is the right performance choice, but it has consequences:

zmq.PUB is not thread-safe

Do not call publish() on the same Publisher from multiple threads (or multiple asyncio tasks that could race on send_multipart). Serialize per-publisher calls yourself if you fan out work.

","path":["Components","Publisher & Subscriber"],"tags":[]},{"location":"components/publisher-subscriber/#lifecycle-and-cleanup","level":3,"title":"Lifecycle and cleanup","text":"
stateDiagram-v2\n    [*] --> Bound: bind + register\n    Bound --> Publishing: publish() calls\n    Publishing --> Publishing: more messages\n    Publishing --> Closed: close()\n    Bound --> Closed: close()\n    Closed --> [*]: unregister,<br/>unlink .sock file

Publisher.close() is best-effort: it unregisters from the daemon (silently tolerates a dead daemon), closes the socket, and removes the IPC file. Exceptions from any one step do not block the others.

","path":["Components","Publisher & Subscriber"],"tags":[]},{"location":"components/publisher-subscriber/#statistics","level":3,"title":"Statistics","text":"

publisher.publish_count, publisher.last_publish_time, and publisher.is_registered are exposed for instrumentation. They update on the hot path with no locking — read them from the same task that calls publish() for deterministic numbers.

","path":["Components","Publisher & Subscriber"],"tags":[]},{"location":"components/publisher-subscriber/#subscriber","level":2,"title":"Subscriber","text":"","path":["Components","Publisher & Subscriber"],"tags":[]},{"location":"components/publisher-subscriber/#construction_1","level":3,"title":"Construction","text":"
sub = node.create_subscriber(\n    topic_name=\"/camera/image\",\n    message_type=ImageMessage,\n    callback=on_image,          # async def callback(msg, header)\n    queue_size=10,              # RCVHWM\n    wait_for_topic=True,        # poll until topic appears\n    topic_timeout=30.0,         # abort wait after N seconds\n)\n

If callback is None, the subscriber is passive — call await sub.receive() manually. With a callback, Node.run() will drive the receive loop.

","path":["Components","Publisher & Subscriber"],"tags":[]},{"location":"components/publisher-subscriber/#startup-sequence_1","level":3,"title":"Startup sequence","text":"
sequenceDiagram\n    autonumber\n    participant U as User\n    participant S as Subscriber\n    participant D as DiscoveryClient\n    participant Pub as publisher IPC\n\n    U->>S: __init__(...)\n    S->>D: lookup_topic(name)  # non-blocking\n    alt found immediately\n        D-->>S: TopicInfo\n        S->>S: verify fingerprint\n        S->>Pub: SUB connect + SUBSCRIBE topic\n        Note over S: is_connected = True\n    else not found\n        D-->>S: None\n        Note over S: defer; retry in run()\n    end\n\n    U->>S: node.run() schedules sub.run()\n    S->>D: wait_for_topic_async(name, timeout)\n    D-->>S: TopicInfo\n    S->>Pub: SUB connect + SUBSCRIBE topic

The constructor tries a non-blocking lookup first so that when a publisher is already up, no polling is needed. The polling fallback only kicks in inside sub.run() via wait_for_topic_async.

","path":["Components","Publisher & Subscriber"],"tags":[]},{"location":"components/publisher-subscriber/#receive-loop","level":3,"title":"Receive loop","text":"
flowchart LR\n    Loop{{AsyncExecutor}} --> Recv[await recv_multipart copy=False]\n    Recv --> Frames[frames = topic, header, metadata, *buffers]\n    Frames --> Decode[Message.from_frames frames 1..]\n    Decode --> CB[await callback msg, header]\n    CB --> Yield[await asyncio.sleep 0]\n    Yield --> Loop
  • copy=False means each frame is a zmq.Frame — the metadata and array buffers are memoryview-able without a copy. See cortex.utils.serialization.
  • The one-frame fast path (len(payload_frames) == 1) handles legacy publishers still on the single-blob path — it falls back to from_bytes on the single payload buffer.
","path":["Components","Publisher & Subscriber"],"tags":[]},{"location":"components/publisher-subscriber/#head-of-line-blocking","level":3,"title":"Head-of-line blocking","text":"

The callback runs inline in the receive loop. A slow callback stalls everything:

gantt\n    title Receive loop when callback is slow\n    dateFormat X\n    axisFormat %L ms\n    section Messages\n    recv m1       :0, 1\n    decode m1     :1, 2\n    callback m1 (slow!) :active, 2, 50\n    recv m2 (queued on HWM) :crit, 50, 51\n    decode m2     :51, 52\n    callback m2   :52, 55

If callbacks do meaningful work, dispatch them to a task or thread pool:

import asyncio\n\nasync def on_image(msg, header):\n    asyncio.create_task(process_in_background(msg, header))\n

Or use a bounded queue + worker pattern. The roadmap item in critique § 6 is to lift this into the framework.

","path":["Components","Publisher & Subscriber"],"tags":[]},{"location":"components/publisher-subscriber/#fingerprint-verification","level":3,"title":"Fingerprint verification","text":"

On connect the subscriber compares its class's fingerprint to the one in the registry entry. Today a mismatch only logs a warning and proceeds anyway — downstream decoding will then fail hard. Treat fingerprint warnings as errors in your code.

","path":["Components","Publisher & Subscriber"],"tags":[]},{"location":"components/publisher-subscriber/#cleanup","level":3,"title":"Cleanup","text":"

Subscriber.close() stops the executor, closes the discovery client and SUB socket, and flips is_connected to False. Safe to call multiple times; errors are suppressed so teardown does not cascade.

","path":["Components","Publisher & Subscriber"],"tags":[]},{"location":"components/publisher-subscriber/#statistics-and-instrumentation","level":2,"title":"Statistics and instrumentation","text":"Property Publisher Subscriber publish_count / receive_count ✓ ✓ last_publish_time / last_receive_time ✓ ✓ is_registered / is_connected ✓ ✓ topic_info

None of these are atomic; treat them as coarse gauges.

","path":["Components","Publisher & Subscriber"],"tags":[]},{"location":"components/publisher-subscriber/#common-pitfalls","level":2,"title":"Common pitfalls","text":"Symptom Cause Fix First N messages not received ZMQ \"slow joiner\": SUB not connected yet when PUB started publishing Let subscriber start first, or sleep briefly before first publish Subscriber receives nothing, no errors Topic name mismatch, or forgot to call node.run() Log both sides; run cortex-discovery --log-level DEBUG publish() returns False repeatedly Subscriber can't keep up; SNDHWM reached Increase queue_size, or reduce publish rate Mutating a received array \"corrupts\" later Decoded arrays alias ZMQ frame memory arr = arr.copy() before mutating Two processes stomp each other's socket Same node_name + topic_name Unique node names per process","path":["Components","Publisher & Subscriber"],"tags":[]},{"location":"components/publisher-subscriber/#see-also","level":2,"title":"See also","text":"
  • cortex.core.publisher
  • cortex.core.subscriber
  • Concepts → Async execution model
  • Concepts → Message wire format
  • Guides → Debugging
","path":["Components","Publisher & Subscriber"],"tags":[]},{"location":"components/serialization/","level":1,"title":"Serialization","text":"

Source: cortex.utils.serialization, cortex.utils.hashing

Two encodings live side by side: a multipart / out-of-band path that the transport actually uses, and a single-blob path kept for the legacy Message.to_bytes / decode API and tests. Both support the same Python types; only their frame layout differs.

","path":["Components","Serialization"],"tags":[]},{"location":"components/serialization/#supported-types","level":2,"title":"Supported types","text":"Type Inline path (to_bytes) OOB path (to_frames) None 1 byte tag msgpack nil int, float, str, bool msgpack PRIMITIVE msgpack bytes tag + length + bytes msgpack bin list, tuple, dict msgpack with ExtType arrays msgpack with OOB descriptors np.ndarray ExtType (inline bytes) OOB descriptor + extra frame torch.Tensor ExtType (inline bytes) OOB descriptor + extra frame","path":["Components","Serialization"],"tags":[]},{"location":"components/serialization/#the-two-paths-side-by-side","level":2,"title":"The two paths, side by side","text":"OOB multipart (used on the wire)Inline blob (legacy / Message.decode)
flowchart LR\n    V[values] --> E[_encode_transport_value]\n    E --> Meta[msgpack metadata<br/>OOB descriptors for arrays]\n    E --> Bufs[[buffer 0]]\n    E --> Bufs2[[buffer 1]]\n    Meta --> Out[(list of frames)]\n    Bufs --> Out\n    Bufs2 --> Out

The function of interest is serialize_message_frames:

metadata_bytes, [buf0, buf1, ...] = serialize_message_frames(values)\n

Arrays stay contiguous; ZMQ hands the buffer straight to the kernel.

flowchart LR\n    V[values] --> P[msgpack.packb<br/>default=_msgpack_default]\n    P --> Ext[ExtType 1/2 for arrays/tensors<br/>bytes embedded]\n    Ext --> Blob[single bytes blob]

The single blob round-trips through serialize(value)deserialize(data). Useful for persisting to disk, caches, or when you need a self-contained payload without tracking extra buffers.

","path":["Components","Serialization"],"tags":[]},{"location":"components/serialization/#oob-descriptors","level":2,"title":"OOB descriptors","text":"

An out-of-band descriptor is a small dict that takes the place of the array inside the msgpack metadata:

# numpy\n{\"__cortex_oob__\": \"numpy\", \"buffer\": 0, \"dtype\": \"<f4\", \"shape\": [480, 640, 3]}\n\n# torch\n{\"__cortex_oob__\": \"torch\", \"buffer\": 1, \"dtype\": \"<f4\",\n \"shape\": [1, 3, 224, 224], \"device\": \"cuda:0\", \"requires_grad\": True}\n

The buffer index refers into the ZMQ frames that follow the metadata. Nested structures (dict of arrays, list of tensors, etc.) are walked recursively by _encode_transport_value / _decode_transport_value.

","path":["Components","Serialization"],"tags":[]},{"location":"components/serialization/#zero-copy-on-the-decode-side","level":2,"title":"Zero-copy on the decode side","text":"
sequenceDiagram\n    participant Sub as Subscriber\n    participant ZMQ as zmq.Frame\n    participant MV as memoryview\n    participant NP as np.ndarray\n\n    Sub->>ZMQ: recv_multipart(copy=False)\n    ZMQ-->>Sub: frame with .buffer property\n    Sub->>MV: memoryview(frame.buffer)\n    Sub->>NP: np.frombuffer(mv, dtype).reshape(shape)\n    Note over NP: array aliases the ZMQ frame memory

Aliasing caveat

The returned NumPy array is a view over the ZMQ frame buffer. It is safe to read as long as the frame lives, which is at least until your callback returns. If you need to:

  • mutate the array, or
  • keep it past the callback,

call arr = arr.copy() first. This is cheap compared to the savings on the hot path.

","path":["Components","Serialization"],"tags":[]},{"location":"components/serialization/#pytorch-specifics","level":2,"title":"PyTorch specifics","text":"
  • Tensors are always moved to CPU for transport. Transport frames carry the tensor's CPU bytes plus the original device string.
  • On decode, CUDA tensors are moved back to the original device when CUDA is available; otherwise they stay on CPU.
  • requires_grad is preserved.
","path":["Components","Serialization"],"tags":[]},{"location":"components/serialization/#fingerprinting","level":2,"title":"Fingerprinting","text":"

Separate but related: compute_fingerprint(cls) computes a 64-bit identity from the module path, class name, and sorted field:type pairs. Cached per-class in _fingerprint_cache. See Concepts → Fingerprinting for the full story.

","path":["Components","Serialization"],"tags":[]},{"location":"components/serialization/#when-to-use-each-helper","level":2,"title":"When to use each helper","text":"Helper Use when serialize_message_frames You're building a custom transport that speaks multipart deserialize_message_frames Decoding the above serialize(value) / deserialize Persisting a single value to disk / cache serialize_numpy / deserialize_numpy Raw array round-trip without msgpack overhead Message.to_frames / Message.from_frames Anything inside Cortex itself","path":["Components","Serialization"],"tags":[]},{"location":"components/serialization/#see-also","level":2,"title":"See also","text":"
  • Concepts → Message wire format
  • Concepts → Fingerprinting
  • Guides → Performance tuning
","path":["Components","Serialization"],"tags":[]},{"location":"concepts/architecture/","level":1,"title":"Architecture","text":"

Cortex has three moving parts: the discovery daemon, publisher nodes, and subscriber nodes. They coordinate over ZeroMQ — a REQ/REP control plane for discovery and a PUB/SUB data plane for messages.

","path":["Concepts","Architecture"],"tags":[]},{"location":"concepts/architecture/#high-level-view","level":2,"title":"High-level view","text":"
flowchart TB\n    subgraph CP[Control plane]\n        DD[Discovery daemon<br/><small>ipc:///tmp/cortex/discovery.sock</small>]\n    end\n\n    subgraph DP[Data plane]\n        direction LR\n        P[Publisher node] -- \"PUB / SUB (IPC)\" --> S[Subscriber node]\n    end\n\n    P -- REGISTER --> DD\n    S -- LOOKUP --> DD\n    DD -- TopicInfo --> S\n\n    classDef daemon fill:#6366f1,stroke:#312e81,color:#fff\n    classDef node fill:#0ea5e9,stroke:#0369a1,color:#fff\n    class DD daemon\n    class P,S node
","path":["Concepts","Architecture"],"tags":[]},{"location":"concepts/architecture/#message-journey","level":2,"title":"Message journey","text":"

Tracing one frame end to end:

sequenceDiagram\n    autonumber\n    participant User as User code\n    participant Pub as Publisher\n    participant Sock as ZMQ PUB socket\n    participant Net as IPC\n    participant SSock as ZMQ SUB socket\n    participant Sub as Subscriber\n    participant CB as async callback\n\n    User->>Pub: publish(Message)\n    Pub->>Pub: build header (fingerprint, ts, seq)\n    Pub->>Pub: encode field values + OOB buffers\n    Pub->>Sock: send_multipart([topic, header, metadata, *buffers])\n    Sock->>Net: zero-copy handoff\n    Net->>SSock: frames delivered\n    SSock->>Sub: recv_multipart(copy=False)\n    Sub->>Sub: Message.from_frames(...)\n    Sub->>CB: await callback(msg, header)

Key invariant: array buffers ride as separate ZMQ frames, not inline in the metadata. See Message wire format.

","path":["Concepts","Architecture"],"tags":[]},{"location":"concepts/architecture/#process-layout","level":2,"title":"Process layout","text":"
flowchart LR\n    subgraph P1[Process: sensor]\n        N1[Node<br/>shared zmq.asyncio.Context]\n        PUB1[Publisher /sensor/a]\n        PUB2[Publisher /sensor/b]\n        T1[Timer 30 Hz]\n        N1 --> PUB1\n        N1 --> PUB2\n        N1 --> T1\n    end\n\n    subgraph P2[Process: processor]\n        N2[Node]\n        SUB1[Subscriber /sensor/a]\n        SUB2[Subscriber /sensor/b]\n        PUB3[Publisher /processed]\n        N2 --> SUB1\n        N2 --> SUB2\n        N2 --> PUB3\n    end\n\n    PUB1 -.->|IPC| SUB1\n    PUB2 -.->|IPC| SUB2

Each topic gets its own IPC socket under /tmp/cortex/topics/. A single Node shares one zmq.asyncio.Context across all its publishers and subscribers to avoid per-socket io thread overhead.

","path":["Concepts","Architecture"],"tags":[]},{"location":"concepts/architecture/#see-also","level":2,"title":"See also","text":"
  • Message wire format
  • Fingerprinting
  • Discovery protocol
  • Async execution model
","path":["Concepts","Architecture"],"tags":[]},{"location":"concepts/async-execution-model/","level":1,"title":"Async execution model","text":"

Cortex nodes are asyncio-native. One event loop per process drives all publishers, subscribers, and timers for that node. On Linux and macOS, cortex.run prefers uvloop for lower tail latency.

","path":["Concepts","Async execution model"],"tags":[]},{"location":"concepts/async-execution-model/#node-task-graph","level":2,"title":"Node task graph","text":"
flowchart TB\n    Loop(((asyncio event loop)))\n    Loop --> T1[Timer 1<br/>RateExecutor]\n    Loop --> T2[Timer 2<br/>RateExecutor]\n    Loop --> S1[Subscriber 1<br/>AsyncExecutor]\n    Loop --> S2[Subscriber 2<br/>AsyncExecutor]

Node.run() spawns one task per timer (RateExecutor) and one per callback-bearing subscriber (AsyncExecutor). It then asyncio.gathers them until cancelled.

","path":["Concepts","Async execution model"],"tags":[]},{"location":"concepts/async-execution-model/#rateexecutor-cadence","level":2,"title":"RateExecutor cadence","text":"
sequenceDiagram\n    participant L as Event loop\n    participant R as RateExecutor\n    participant CB as callback\n\n    loop every interval\n        L->>R: resume\n        R->>CB: await callback()\n        R->>R: next_exec_time += interval\n        alt fell behind\n            R->>R: next_exec_time = now + interval\n        end\n        R->>L: sleep(next_exec_time - now)\n    end

Catch-up logic silently drops ticks when a callback overruns its period — something to keep in mind for control loops.

","path":["Concepts","Async execution model"],"tags":[]},{"location":"concepts/async-execution-model/#asyncexecutor-receive-loop","level":2,"title":"AsyncExecutor receive loop","text":"
sequenceDiagram\n    participant L as Event loop\n    participant A as AsyncExecutor\n    participant S as SUB socket\n    participant CB as user callback\n\n    loop while running\n        L->>A: resume\n        A->>S: await recv_multipart(copy=False)\n        S-->>A: frames\n        A->>A: decode message\n        A->>CB: await callback(msg, header)\n        A->>L: sleep(0)  (yield)\n    end

Head-of-line blocking

A slow callback stalls the receive loop. Messages pile up on the SUB HWM and get evicted. If you expect variable-latency work, offload callback bodies to asyncio.create_task(...) or a thread pool.

","path":["Concepts","Async execution model"],"tags":[]},{"location":"concepts/async-execution-model/#publish-is-sync-inside-async","level":2,"title":"Publish is sync-inside-async","text":"

The Publisher uses a sync zmq.Context (shadowed onto the node's async context). publish() is a plain function call — no await. This avoids the overhead of the async zmq integration on the send path.

Not thread-safe

A zmq.PUB socket is not safe to call from multiple threads or tasks concurrently. Serialize calls to publish() per publisher.

","path":["Concepts","Async execution model"],"tags":[]},{"location":"concepts/async-execution-model/#uvloop","level":2,"title":"uvloop","text":"

On Unix, importing cortex.run checks for uvloop and uses it if present. Measured impact: modest throughput improvement, meaningful p99 latency reduction on high-rate small messages.

","path":["Concepts","Async execution model"],"tags":[]},{"location":"concepts/async-execution-model/#see-also","level":2,"title":"See also","text":"
  • cortex.core.executor
  • cortex.core.node
  • Components → Node & Executors
","path":["Concepts","Async execution model"],"tags":[]},{"location":"concepts/discovery-protocol/","level":1,"title":"Discovery protocol","text":"

The discovery daemon speaks a tiny msgpack-over-REQ/REP protocol. It is not on the data path — once a subscriber has the endpoint, messages flow publisher → subscriber directly.

","path":["Concepts","Discovery protocol"],"tags":[]},{"location":"concepts/discovery-protocol/#commands","level":2,"title":"Commands","text":"Command Payload required Returns REGISTER_TOPIC (1) TopicInfo OK / ALREADY_EXISTS UNREGISTER_TOPIC (2) topic_name or TopicInfo.name OK / NOT_FOUND LOOKUP_TOPIC (3) topic_name OK + TopicInfo / NOT_FOUND LIST_TOPICS (4) — OK + list[TopicInfo] SHUTDOWN (99) — OK; daemon exits

Status codes: OK=0, NOT_FOUND=1, ALREADY_EXISTS=2, ERROR=3.

","path":["Concepts","Discovery protocol"],"tags":[]},{"location":"concepts/discovery-protocol/#topicinfo-payload","level":2,"title":"TopicInfo payload","text":"
@dataclass\nclass TopicInfo:\n    name: str              # \"/camera/image\"\n    address: str           # \"ipc:///tmp/cortex/topics/cam__camera_image.sock\"\n    message_type: str      # \"ImageMessage\"\n    fingerprint: int       # 64-bit class fingerprint\n    publisher_node: str    # \"cam\"\n
","path":["Concepts","Discovery protocol"],"tags":[]},{"location":"concepts/discovery-protocol/#publisher-register-flow","level":2,"title":"Publisher register flow","text":"
sequenceDiagram\n    autonumber\n    participant P as Publisher\n    participant D as Daemon REP\n\n    P->>P: bind PUB socket on ipc:///tmp/cortex/topics/<node>__<topic>.sock\n    P->>D: REQ → DiscoveryRequest(REGISTER_TOPIC, TopicInfo{...})\n    D->>D: if topic_name absent: insert; else compare publisher_node\n    alt new\n        D-->>P: OK \"Registered topic: /x\"\n    else same publisher re-registering\n        D-->>P: OK (overwrite)\n    else different publisher, same topic\n        D-->>P: ALREADY_EXISTS\n    end
","path":["Concepts","Discovery protocol"],"tags":[]},{"location":"concepts/discovery-protocol/#subscriber-lookup-flow","level":2,"title":"Subscriber lookup flow","text":"
sequenceDiagram\n    autonumber\n    participant S as Subscriber\n    participant D as Daemon REP\n    participant P as Publisher\n\n    S->>D: REQ → LOOKUP_TOPIC(\"/x\")\n    alt present\n        D-->>S: OK + TopicInfo\n        S->>P: SUB connect + SUBSCRIBE \"/x\"\n    else missing\n        D-->>S: NOT_FOUND\n        Note over S: if wait_for_topic:<br/>poll every 500 ms until timeout\n        S->>D: retry LOOKUP_TOPIC\n    end

wait_for_topic_async implements the retry loop with asyncio.sleep so the event loop keeps spinning.

","path":["Concepts","Discovery protocol"],"tags":[]},{"location":"concepts/discovery-protocol/#req-socket-recovery","level":2,"title":"REQ-socket recovery","text":"

ZMQ REQ sockets enter a bad state after a missed reply — they block further sends. The client detects zmq.Again on timeout and rebuilds the socket:

flowchart TD\n    A[send request] -->|timeout| B[REQ socket stuck]\n    B --> C[close socket]\n    C --> D[recreate socket<br/>same endpoint]\n    D --> E[retry up to retries]

See DiscoveryClient._reconnect.

Fencepost in retries default

retries=1 today executes the loop exactly once — i.e. no retry. Bump to retries=3 in client-side code if you need resilience.

","path":["Concepts","Discovery protocol"],"tags":[]},{"location":"concepts/discovery-protocol/#failure-modes-how-cortex-handles-them","level":2,"title":"Failure modes & how Cortex handles them","text":"Scenario Behavior Daemon not running when publisher starts Register fails; publisher still publishes, but no subscriber can find it. Daemon restarts All state lost; publishers must re-register. Current design has no auto-re-register. Publisher crashes Registry keeps stale TopicInfo until someone UNREGISTERs. Two publishers, same topic Second registration rejected with ALREADY_EXISTS. Subscriber looks up before publisher NOT_FOUND; caller may wait_for_topic to poll.

Roadmap items (see critique.md) to address these: leases with heartbeats, multi-publisher support, and notify-on-change.

","path":["Concepts","Discovery protocol"],"tags":[]},{"location":"concepts/discovery-protocol/#see-also","level":2,"title":"See also","text":"
  • cortex.discovery.protocol
  • cortex.discovery.client
  • cortex.discovery.daemon
  • Components → Discovery
","path":["Concepts","Discovery protocol"],"tags":[]},{"location":"concepts/fingerprinting/","level":1,"title":"Fingerprinting","text":"

Every message class gets a 64-bit identifier derived from its name and field schema. The fingerprint rides in the header of every published message and does two jobs:

  1. Type dispatch — Message.decode(bytes) looks up the right class in the MessageType registry.
  2. Compatibility check — subscribers verify that the topic they looked up advertises the same fingerprint as the type they were written against.
","path":["Concepts","Fingerprinting"],"tags":[]},{"location":"concepts/fingerprinting/#derivation","level":2,"title":"Derivation","text":"
flowchart LR\n    A[class.__module__ + qualname] --> C[canonical string]\n    B[sorted list of field:type] --> C\n    C --> H[SHA-256]\n    H --> F[first 8 bytes → u64 big-endian]

Pseudocode:

canonical = f\"{cls.__module__}.{cls.__qualname__}|{','.join(sorted('name:type'))}\"\nfingerprint = int.from_bytes(sha256(canonical.encode()).digest()[:8], \"big\")\n

The result is cached per-class in _fingerprint_cache, computed once lazily.

","path":["Concepts","Fingerprinting"],"tags":[]},{"location":"concepts/fingerprinting/#registry","level":2,"title":"Registry","text":"

Message.__init_subclass__ auto-registers every concrete subclass into MessageType._registry keyed by fingerprint. Nothing else to do — decorating your dataclass with @dataclass and inheriting from Message is enough.

from dataclasses import dataclass\nfrom cortex.messages.base import Message\n\n@dataclass\nclass JointState(Message):\n    positions: list[float]\n    velocities: list[float]\n\nprint(hex(JointState.fingerprint()))\n
","path":["Concepts","Fingerprinting"],"tags":[]},{"location":"concepts/fingerprinting/#when-fingerprints-change","level":2,"title":"When fingerprints change","text":"

The fingerprint is not stable across edits that touch:

  • Module path or class name (cortex.messages.standard.ArrayMessage renamed anywhere).
  • Field names.
  • Field type annotations as spelled (see the PEP 563 caveat below).

It is stable across:

  • Adding/removing unrelated classes.
  • Reordering methods.
  • Changing docstrings or default values.
","path":["Concepts","Fingerprinting"],"tags":[]},{"location":"concepts/fingerprinting/#subscriber-check","level":2,"title":"Subscriber check","text":"

On connect, the subscriber compares the topic's advertised fingerprint against the one it computed from its message class:

sequenceDiagram\n    participant S as Subscriber\n    participant D as Discovery daemon\n\n    S->>D: LOOKUP /topic\n    D-->>S: TopicInfo(fingerprint=0xABCD...)\n    S->>S: compare with MyMessage.fingerprint()\n    alt mismatch\n        S-->>S: log warning, continue anyway\n    else match\n        S-->>S: connect and subscribe\n    end

Today: mismatch is a warning, not an error

A fingerprint mismatch currently only logs a warning — see critique.md. Downstream decoding will fail hard. Until that is tightened, prefer to re-exchange type definitions between processes rather than rely on this guard.

","path":["Concepts","Fingerprinting"],"tags":[]},{"location":"concepts/fingerprinting/#pep-563-caveat","level":2,"title":"PEP 563 caveat","text":"

field.type may be a string (under from __future__ import annotations) or a real type otherwise. The canonical string differs in the two cases, so the same class can fingerprint differently across import environments.

When defining messages shared between processes, either use the same import style in both, or rely on the runtime typing.get_type_hints(cls) equivalent once that lands upstream.

","path":["Concepts","Fingerprinting"],"tags":[]},{"location":"concepts/fingerprinting/#see-also","level":2,"title":"See also","text":"
  • cortex.utils.hashingcompute_fingerprint, cache helpers
  • Message wire format
  • Critique § code-level issue 13
","path":["Concepts","Fingerprinting"],"tags":[]},{"location":"concepts/message-wire-format/","level":1,"title":"Message wire format","text":"

Cortex uses ZeroMQ multipart messages. Each published message is a list of frames rather than a single blob. That lets array payloads ride as raw contiguous buffers — no copy into a Python bytes, no re-copy by ZMQ.

","path":["Concepts","Message wire format"],"tags":[]},{"location":"concepts/message-wire-format/#frames-on-the-wire","level":2,"title":"Frames on the wire","text":"
flowchart LR\n    F0[\"Frame 0<br/>topic bytes\"] --> F1\n    F1[\"Frame 1<br/>header (24B)<br/>fingerprint • ts_ns • seq\"] --> F2\n    F2[\"Frame 2<br/>msgpack metadata<br/>(ordered field values)\"] --> F3\n    F3[\"Frame 3..N<br/>raw array buffers<br/>(OOB, zero-copy)\"]
Frame Contents Size 0 Topic name (UTF-8) variable 1 MessageHeader 24 bytes (3 × u64, big-endian) 2 msgpack-packed ordered field values; arrays replaced by OOB descriptors small 3..N np.ndarray.tobytes() / tensor.numpy().tobytes(), contiguous payload-sized","path":["Concepts","Message wire format"],"tags":[]},{"location":"concepts/message-wire-format/#header-layout","level":2,"title":"Header layout","text":"
offset 0        8       16       24\n       |fp u64 |ts u64 |seq u64 |\n        big-endian throughout\n
  • fp — 64-bit message fingerprint, computed from class name and field schema.
  • ts — publisher wall-clock in nanoseconds (time.time_ns()).
  • seq — per-process, per-message-type monotonic counter.
","path":["Concepts","Message wire format"],"tags":[]},{"location":"concepts/message-wire-format/#metadata-frame-2","level":2,"title":"Metadata (Frame 2)","text":"

Field values are packed in declaration order (not by name), so the receiver reconstructs using the dataclass's cached field tuple. This removes per-message field-name encoding.

Arrays and tensors appear in the metadata as small dict stand-ins called OOB descriptors:

{\n  \"__cortex_oob__\": \"numpy\",\n  \"buffer\": 0,\n  \"dtype\": \"<f4\",\n  \"shape\": [480, 640, 3]\n}\n

The buffer index refers into Frames 3..N. The receiver reconstructs:

np.frombuffer(frame.buffer, dtype=np.dtype(desc[\"dtype\"])).reshape(desc[\"shape\"])\n

No copy. The resulting array aliases the ZMQ frame memory — copy it if you need ownership or mutability (see Performance tuning).

","path":["Concepts","Message wire format"],"tags":[]},{"location":"concepts/message-wire-format/#full-encodedecode-flow","level":2,"title":"Full encode/decode flow","text":"
sequenceDiagram\n    participant U as User\n    participant M as Message.to_frames\n    participant S as serialize_message_frames\n    participant E as _encode_transport_value\n    participant Z as ZMQ send_multipart\n\n    U->>M: build header + collect field values\n    M->>S: values in declaration order\n    S->>E: for each value, walk nested dicts/lists\n    E-->>S: scalar stays inline; array → OOB descriptor + buffer appended\n    S-->>M: (metadata_bytes, [buf0, buf1, ...])\n    M-->>Z: [topic, header, metadata, *buffers]
","path":["Concepts","Message wire format"],"tags":[]},{"location":"concepts/message-wire-format/#the-legacy-single-blob-path","level":2,"title":"The legacy single-blob path","text":"

Message.to_bytes() / from_bytes() / Message.decode() still exist. They pack everything into one msgpack blob using ExtType for arrays. That path is retained for tests and opportunistic use; the transport always uses the multipart path above.

Mismatch trap

Bytes captured from the wire cannot be fed to Message.decode() — the wire format is multipart, not a single blob. Use Message.from_frames(frames).

","path":["Concepts","Message wire format"],"tags":[]},{"location":"concepts/message-wire-format/#see-also","level":2,"title":"See also","text":"
  • Fingerprinting
  • cortex.utils.serialization — encoding helpers
  • cortex.messages.baseMessage, MessageHeader
","path":["Concepts","Message wire format"],"tags":[]},{"location":"concepts/transport-and-qos/","level":1,"title":"Transport & QoS","text":"

Stub — deep dive coming in a later pass.

","path":["Concepts","Transport & QoS"],"tags":[]},{"location":"concepts/transport-and-qos/#current-socket-settings","level":2,"title":"Current socket settings","text":"Socket Option Value Notes Publisher PUB SNDHWM 10 (default queue_size) Drops under backpressure Publisher PUB LINGER 0 Immediate close Subscriber SUB RCVHWM 10 Oldest messages evicted when full Subscriber SUB LINGER 0 Daemon REP RCVTIMEO 1000 ms Allows Ctrl-C responsiveness Daemon REP LINGER 0","path":["Concepts","Transport & QoS"],"tags":[]},{"location":"concepts/transport-and-qos/#todays-delivery-semantics","level":2,"title":"Today's delivery semantics","text":"
  • Publisher uses zmq.NOBLOCK: if the send queue is full, the message is silently dropped.
  • Subscriber HWM is a ring buffer: old messages are silently evicted on overflow.

This is fine for best-effort telemetry. It is unsafe for control commands.

","path":["Concepts","Transport & QoS"],"tags":[]},{"location":"concepts/transport-and-qos/#planned-qos-profiles","level":2,"title":"Planned QoS profiles","text":"

Taking inspiration from DDS, three profiles are enough for most robotics use:

  • best_effort_latest — conflate; keep only newest (camera frames).
  • reliable_queue — publisher blocks or errors (control commands).
  • dropping_queue — current behavior with an exposed drop counter (telemetry).

See critique.md § 4 for rationale.

","path":["Concepts","Transport & QoS"],"tags":[]},{"location":"getting-started/discovery-daemon/","level":1,"title":"Running the Discovery Daemon","text":"

The discovery daemon is a lightweight REP service that maintains the registry of active topics. Publishers register on startup; subscribers look up the endpoint and connect directly.

","path":["Getting started","Running the Discovery Daemon"],"tags":[]},{"location":"getting-started/discovery-daemon/#start","level":2,"title":"Start","text":"As a scriptAs a moduleAs a systemd service
cortex-discovery\n
python -m cortex.discovery.daemon\n
/etc/systemd/system/cortex-discovery.service
[Unit]\nDescription=Cortex discovery daemon\nAfter=network.target\n\n[Service]\nType=simple\nExecStart=/usr/bin/env cortex-discovery\nRestart=on-failure\nRuntimeDirectory=cortex\n\n[Install]\nWantedBy=multi-user.target\n
","path":["Getting started","Running the Discovery Daemon"],"tags":[]},{"location":"getting-started/discovery-daemon/#command-line-options","level":2,"title":"Command-line options","text":"Flag Default Description --address ipc:///tmp/cortex/discovery.sock ZMQ endpoint to bind --log-level INFO DEBUG / INFO / WARNING / ERROR","path":["Getting started","Running the Discovery Daemon"],"tags":[]},{"location":"getting-started/discovery-daemon/#lifecycle","level":2,"title":"Lifecycle","text":"
stateDiagram-v2\n    [*] --> Starting: bind REP socket\n    Starting --> Running: socket ready\n    Running --> Running: handle REGISTER / LOOKUP / LIST / UNREGISTER\n    Running --> Stopping: SIGINT or SHUTDOWN command\n    Stopping --> [*]: close socket, unlink ipc file
","path":["Getting started","Running the Discovery Daemon"],"tags":[]},{"location":"getting-started/discovery-daemon/#troubleshooting","level":2,"title":"Troubleshooting","text":"\"Address already in use\" Another daemon (or a stale socket file) is holding the path. rm /tmp/cortex/discovery.sock and restart. Subscribers time out looking up topics Daemon not running, or publisher failed to register. Run with --log-level DEBUG and watch for REGISTER / LOOKUP lines. Daemon crash leaves stale entries Today, entries are only removed on explicit UNREGISTER. A crashed publisher's topic stays in the registry pointing at a dead socket. Restarting the daemon clears all state.","path":["Getting started","Running the Discovery Daemon"],"tags":[]},{"location":"getting-started/installation/","level":1,"title":"Installation","text":"","path":["Getting started","Installation"],"tags":[]},{"location":"getting-started/installation/#requirements","level":2,"title":"Requirements","text":"
  • Python 3.10+
  • Linux or macOS (Windows works but without uvloop)
  • ZeroMQ shared library (bundled via pyzmq)
","path":["Getting started","Installation"],"tags":[]},{"location":"getting-started/installation/#install-from-source","level":2,"title":"Install from source","text":"
git clone https://github.com/sudoRicheek/cortex.git\ncd cortex\npip install -e \".[dev]\"\n
","path":["Getting started","Installation"],"tags":[]},{"location":"getting-started/installation/#optional-extras","level":2,"title":"Optional extras","text":"PyTorch supportEverything
pip install -e \".[torch]\"\n

Enables TensorMessage and torch-aware serialization paths.

pip install -e \".[all]\"\n
","path":["Getting started","Installation"],"tags":[]},{"location":"getting-started/installation/#verify","level":2,"title":"Verify","text":"
import cortex\nprint(cortex.__version__)\n

If that prints a version string, you're ready. Continue to the Quickstart.

","path":["Getting started","Installation"],"tags":[]},{"location":"getting-started/quickstart/","level":1,"title":"Quickstart","text":"

A three-terminal pub/sub loop in under two minutes.

","path":["Getting started","Quickstart"],"tags":[]},{"location":"getting-started/quickstart/#1-start-the-discovery-daemon","level":2,"title":"1. Start the discovery daemon","text":"
cortex-discovery\n

Leave it running. This is the single service that maps topic names to IPC endpoints.

","path":["Getting started","Quickstart"],"tags":[]},{"location":"getting-started/quickstart/#2-publisher","level":2,"title":"2. Publisher","text":"pub.py
import numpy as np\nimport cortex\nfrom cortex import Node, ArrayMessage\n\n\nclass SensorNode(Node):\n    def __init__(self):\n        super().__init__(\"sensor\")\n        self.pub = self.create_publisher(\"/sensor/data\", ArrayMessage)\n        self.count = 0\n        self.create_timer(0.1, self.tick)  # 10 Hz\n\n    async def tick(self):\n        data = np.random.randn(64, 64).astype(\"float32\")\n        self.pub.publish(ArrayMessage(data=data, name=f\"frame_{self.count}\"))\n        self.count += 1\n\n\nasync def main():\n    node = SensorNode()\n    try:\n        await node.run()\n    finally:\n        await node.close()\n\n\nif __name__ == \"__main__\":\n    cortex.run(main())\n
python pub.py\n
","path":["Getting started","Quickstart"],"tags":[]},{"location":"getting-started/quickstart/#3-subscriber","level":2,"title":"3. Subscriber","text":"sub.py
import cortex\nfrom cortex import Node, ArrayMessage\nfrom cortex.messages.base import MessageHeader\n\n\nasync def on_data(msg: ArrayMessage, header: MessageHeader):\n    print(f\"[{header.sequence}] {msg.name} shape={msg.data.shape}\")\n\n\nclass ViewerNode(Node):\n    def __init__(self):\n        super().__init__(\"viewer\")\n        self.create_subscriber(\"/sensor/data\", ArrayMessage, callback=on_data)\n\n\nasync def main():\n    node = ViewerNode()\n    try:\n        await node.run()\n    finally:\n        await node.close()\n\n\nif __name__ == \"__main__\":\n    cortex.run(main())\n
python sub.py\n
","path":["Getting started","Quickstart"],"tags":[]},{"location":"getting-started/quickstart/#what-just-happened","level":2,"title":"What just happened","text":"
sequenceDiagram\n    participant P as Publisher\n    participant D as Discovery daemon\n    participant S as Subscriber\n\n    P->>D: REGISTER /sensor/data -> ipc:///tmp/cortex/topics/...\n    S->>D: LOOKUP /sensor/data\n    D-->>S: ipc:///tmp/cortex/topics/...\n    S->>P: ZMQ SUB connect + SUBSCRIBE \"/sensor/data\"\n    loop 10 Hz\n        P->>S: multipart [topic, header, metadata, buffer]\n        S->>S: decode + await on_data(msg, header)\n    end

See Concepts → Architecture for the end-to-end picture, or jump into a custom message tutorial.

","path":["Getting started","Quickstart"],"tags":[]},{"location":"guides/benchmarks/","level":1,"title":"Benchmarks","text":"

Cortex ships an in-repo benchmark suite at benchmarks/.

","path":["Guides","Benchmarks"],"tags":[]},{"location":"guides/benchmarks/#run","level":2,"title":"Run","text":"
# Terminal 1\ncortex-discovery\n\n# Terminal 2\npython benchmarks/bench_all.py --output results.json\n

Individual benchmarks:

  • benchmarks/bench_latency.py — one-way publisher→subscriber latency.
  • benchmarks/bench_throughput.py — messages/sec and MB/sec.
  • benchmarks/bench_all.py — full matrix with summary and optional JSON dump.
","path":["Guides","Benchmarks"],"tags":[]},{"location":"guides/benchmarks/#reading-results","level":2,"title":"Reading results","text":"
  • p99 is what matters for real-time-ish workloads; mean can hide jitter.
  • For array workloads, MB/s approaching memcpy bandwidth is a good sign that zero-copy transport is working.
  • Serialization overhead via inproc sockets with copy=False is reported separately — that isolates the encode/decode path from the network path.
","path":["Guides","Benchmarks"],"tags":[]},{"location":"guides/benchmarks/#tips","level":2,"title":"Tips","text":"
  • Pin publisher and subscriber to separate cores for stable latency numbers.
  • Disable Turbo-Boost / set CPU governor to performance for reproducible runs.
  • Always measure with the discovery daemon also running (it is off the hot path but can steal a little cache).
","path":["Guides","Benchmarks"],"tags":[]},{"location":"guides/debugging/","level":1,"title":"Debugging","text":"","path":["Guides","Debugging"],"tags":[]},{"location":"guides/debugging/#subscriber-hangs-on-startup","level":2,"title":"Subscriber hangs on startup","text":"

Most likely: the daemon is not running, or the topic name is mistyped. DiscoveryClient.wait_for_topic_async polls every 500 ms until the topic appears or the timeout fires.

cortex-discovery --log-level DEBUG\n

Watch for LOOKUP topic: /x -> NOT FOUND.

","path":["Guides","Debugging"],"tags":[]},{"location":"guides/debugging/#publisher-works-but-subscriber-receives-nothing","level":2,"title":"Publisher \"works\" but subscriber receives nothing","text":"

ZMQ PUB drops messages for which no matching SUB is connected yet. If your publisher starts first and publishes immediately, the first few messages are lost — this is the classic ZMQ slow-joiner problem.

Workarounds:

  • Have the publisher wait briefly after bind before publishing the first message.
  • Have the subscriber wait-for-topic (the default) so it comes up after the publisher registered.
","path":["Guides","Debugging"],"tags":[]},{"location":"guides/debugging/#stale-tmpcortextopicssock-files","level":2,"title":"Stale /tmp/cortex/topics/*.sock files","text":"

If a publisher exits uncleanly, its IPC socket file remains. Cortex's Publisher._setup_socket unlinks any existing file at the same path on the next bind — so restarting the publisher fixes it. Otherwise:

rm /tmp/cortex/topics/<stale-socket>.sock\n
","path":["Guides","Debugging"],"tags":[]},{"location":"guides/debugging/#daemon-state-survives-restarts-but-doesnt","level":2,"title":"Daemon state survives restarts — but doesn't","text":"

The registry is in-memory. Restarting the daemon wipes all state; publishers do not auto-re-register today. Restart your publishers after restarting the daemon.

","path":["Guides","Debugging"],"tags":[]},{"location":"guides/debugging/#fingerprint-mismatch-warning","level":2,"title":"Fingerprint mismatch warning","text":"

If you see Message type mismatch for /x: expected FooMessage, got BarMessage — the topic was registered with a different message class. Either rename the topic or align the classes.

","path":["Guides","Debugging"],"tags":[]},{"location":"guides/debugging/#debug-logging","level":2,"title":"Debug logging","text":"
import logging\nlogging.basicConfig(level=logging.DEBUG)\n

Cortex uses standard logging. Interesting loggers: cortex.publisher, cortex.subscriber, cortex.node, cortex.discovery, cortex.discovery.client.

","path":["Guides","Debugging"],"tags":[]},{"location":"guides/performance-tuning/","level":1,"title":"Performance tuning","text":"

Current measured numbers on the repo's benchmark suite (single workstation):

Workload Throughput / latency Small payload latency mean 556 µs, p99 1075 µs 1MB array throughput 7.7k msg/s, 8.0 GB/s 4MB array throughput 2.25k msg/s, 9.4 GB/s 1080p RGB 1422 fps, 8.8 GB/s

See Benchmarks guide to reproduce.

","path":["Guides","Performance tuning"],"tags":[]},{"location":"guides/performance-tuning/#copy-on-use","level":2,"title":"Copy-on-use","text":"

Decoded NumPy arrays alias the ZMQ frame memory. That is what makes large-payload throughput close to memcpy bandwidth — but it means:

  • If you intend to mutate the array, arr = arr.copy() first.
  • If you intend to hold the array past the callback, copy it first.
","path":["Guides","Performance tuning"],"tags":[]},{"location":"guides/performance-tuning/#queue-sizing","level":2,"title":"Queue sizing","text":"

Per-socket HWM defaults to 10. Increase queue_size on high-rate producers whose subscribers are known to be slow — but remember that ZMQ drops silently at the HWM.

","path":["Guides","Performance tuning"],"tags":[]},{"location":"guides/performance-tuning/#when-to-prefer-the-inline-path","level":2,"title":"When to prefer the inline path","text":"

Single tiny messages (primitives only, < 1 KB) see no benefit from multipart. The inline to_bytes path is still fine there. Publishers always use multipart today.

","path":["Guides","Performance tuning"],"tags":[]},{"location":"guides/performance-tuning/#uvloop","level":2,"title":"uvloop","text":"

Installed by default on Unix. Drops tail latency on high-rate small messages noticeably. No action needed.

","path":["Guides","Performance tuning"],"tags":[]},{"location":"tutorials/custom-messages/","level":1,"title":"Custom messages","text":"

A message in Cortex is any dataclass that inherits from Message. Auto-registration, fingerprinting, and (de)serialization are all derived from the dataclass definition — you write the schema once, publishers and subscribers speak the same wire format.

","path":["Tutorials","Custom messages"],"tags":[]},{"location":"tutorials/custom-messages/#define","level":2,"title":"Define","text":"messages.py
from dataclasses import dataclass\nimport numpy as np\nfrom cortex.messages.base import Message\n\n\n@dataclass\nclass RobotState(Message):\n    timestamp: float\n    position: np.ndarray      # shape (3,)\n    velocity: np.ndarray      # shape (3,)\n    joint_angles: np.ndarray  # shape (N,)\n    is_moving: bool\n    frame_id: str = \"base_link\"\n

Shared module

Put your message definitions in a module both the publisher and subscriber import. The fingerprint is computed from module.qualname + field names/types; an identical re-declaration in two different modules produces different fingerprints.

","path":["Tutorials","Custom messages"],"tags":[]},{"location":"tutorials/custom-messages/#publish","level":2,"title":"Publish","text":"publisher.py
import numpy as np\nimport cortex\nfrom cortex import Node\nfrom messages import RobotState\n\n\nclass StateBroadcaster(Node):\n    def __init__(self):\n        super().__init__(\"robot\")\n        self.pub = self.create_publisher(\"/robot/state\", RobotState)\n        self.create_timer(1 / 100, self.tick)  # 100 Hz\n        self._t0 = 0.0\n\n    async def tick(self):\n        self._t0 += 0.01\n        self.pub.publish(RobotState(\n            timestamp=self._t0,\n            position=np.array([self._t0, 0.0, 0.5], dtype=\"f4\"),\n            velocity=np.array([1.0, 0.0, 0.0], dtype=\"f4\"),\n            joint_angles=np.zeros(7, dtype=\"f4\"),\n            is_moving=True,\n        ))\n\n\nif __name__ == \"__main__\":\n    cortex.run(StateBroadcaster().run())\n
","path":["Tutorials","Custom messages"],"tags":[]},{"location":"tutorials/custom-messages/#subscribe","level":2,"title":"Subscribe","text":"subscriber.py
import cortex\nfrom cortex import Node\nfrom cortex.messages.base import MessageHeader\nfrom messages import RobotState   # same import, same fingerprint\n\n\nasync def on_state(msg: RobotState, header: MessageHeader):\n    if header.sequence % 100 == 0:\n        print(f\"t={msg.timestamp:.3f} pos={msg.position}\")\n\n\nclass Monitor(Node):\n    def __init__(self):\n        super().__init__(\"monitor\")\n        self.create_subscriber(\"/robot/state\", RobotState, callback=on_state)\n\n\nif __name__ == \"__main__\":\n    cortex.run(Monitor().run())\n
","path":["Tutorials","Custom messages"],"tags":[]},{"location":"tutorials/custom-messages/#how-the-dataclass-becomes-a-wire-message","level":2,"title":"How the dataclass becomes a wire message","text":"
flowchart LR\n    DC[dataclass fields] --> FP[fingerprint]\n    DC --> ORD[declaration order]\n    ORD --> Enc[serialize_message_frames<br/>values in order]\n    Enc --> Meta[metadata frame]\n    Enc --> Bufs[array frames]\n    FP --> Hdr[24-byte header]\n    Hdr --> Wire[(multipart send)]\n    Meta --> Wire\n    Bufs --> Wire

See Concepts → Message wire format for the full picture.

","path":["Tutorials","Custom messages"],"tags":[]},{"location":"tutorials/custom-messages/#supported-field-types","level":2,"title":"Supported field types","text":"Field type Notes int / float / bool / str Plain msgpack primitives bytes msgpack bin list[...] / tuple[...] Walked recursively dict[str, Any] Walked recursively; arrays inside are still OOB np.ndarray OOB frame; zero-copy decode torch.Tensor OOB frame; CPU-transported, device restored on decode Optional nested Message Not first-class today — flatten instead","path":["Tutorials","Custom messages"],"tags":[]},{"location":"tutorials/custom-messages/#evolution-what-breaks-the-fingerprint","level":2,"title":"Evolution: what breaks the fingerprint","text":"

Changing any of these changes the fingerprint and makes old and new publishers/subscribers incompatible:

  • Renaming the class, its module, or any field
  • Adding a field (even with a default)
  • Removing a field
  • Changing a field's annotation text

Safe to change without breaking:

  • Reordering methods, adding methods
  • Editing docstrings or defaults
  • Changing unrelated classes in the same module

See critique § 22 for the roadmap on first-class schema evolution.

","path":["Tutorials","Custom messages"],"tags":[]},{"location":"tutorials/custom-messages/#see-also","level":2,"title":"See also","text":"
  • Concepts → Fingerprinting
  • Components → Messages
  • Tutorials → Multi-node system for custom messages used across multiple nodes
","path":["Tutorials","Custom messages"],"tags":[]},{"location":"tutorials/multi-node-system/","level":1,"title":"Multi-node system","text":"

A walk-through of examples/multi_node_system.py — a sensor → processor → monitor pipeline with custom messages, multiple publishers and subscribers, and periodic status reporting.

","path":["Tutorials","Multi-node system"],"tags":[]},{"location":"tutorials/multi-node-system/#topology","level":2,"title":"Topology","text":"
flowchart LR\n    subgraph Sensors[Sensor nodes]\n        S1[\"sensor_lidar<br/>10 Hz\"]\n        S2[\"sensor_camera<br/>10 Hz\"]\n    end\n    Proc[processor]\n    Mon[monitor]\n\n    S1 -- \"/sensor/lidar/raw\" --> Proc\n    S2 -- \"/sensor/camera/raw\" --> Proc\n    Proc -- \"/processed/data\" --> Mon\n    Mon -- \"/system/status<br/>1 Hz\" --> World((world))

Four nodes run in a single Python process, each on the same asyncio event loop via asyncio.gather. Cortex's IPC transport does not care that they share a process — the data still rides through real ZMQ sockets.

","path":["Tutorials","Multi-node system"],"tags":[]},{"location":"tutorials/multi-node-system/#message-schema","level":2,"title":"Message schema","text":"

Three custom message types share a module so every node gets the same fingerprints:

@dataclass\nclass SensorReading(Message):\n    sensor_id: str\n    timestamp: float\n    values: np.ndarray\n    temperature: float\n\n@dataclass\nclass ProcessedData(Message):\n    source_sensor: str\n    timestamp: float\n    filtered_values: np.ndarray\n    statistics: dict   # {mean, std, min, max}\n\n@dataclass\nclass SystemStatus(Message):\n    timestamp: float\n    num_sensors: int\n    processing_rate_hz: float\n    total_messages: int\n
","path":["Tutorials","Multi-node system"],"tags":[]},{"location":"tutorials/multi-node-system/#sensor-node","level":2,"title":"Sensor node","text":"
class SensorNode(Node):\n    def __init__(self, sensor_id: str, publish_rate: float = 10.0):\n        super().__init__(f\"sensor_{sensor_id}\")\n        self.reading_pub = self.create_publisher(\n            f\"/sensor/{sensor_id}/raw\", SensorReading\n        )\n        self.create_timer(1.0 / publish_rate, self._publish_reading)\n\n    async def _publish_reading(self):\n        t = time.time()\n        values = np.sin(np.linspace(0, 2*np.pi, 100) + t) + 0.1*np.random.randn(100)\n        self.reading_pub.publish(SensorReading(\n            sensor_id=self.sensor_id,\n            timestamp=t,\n            values=values.astype(\"f4\"),\n            temperature=25.0 + 0.5*np.random.randn(),\n        ))\n
","path":["Tutorials","Multi-node system"],"tags":[]},{"location":"tutorials/multi-node-system/#processor-node","level":2,"title":"Processor node","text":"

Subscribes to every sensor and republishes filtered data:

class ProcessorNode(Node):\n    def __init__(self, sensor_ids: list[str]):\n        super().__init__(\"processor\")\n        for sid in sensor_ids:\n            self.create_subscriber(\n                f\"/sensor/{sid}/raw\", SensorReading, callback=self._on_reading\n            )\n        self.processed_pub = self.create_publisher(\"/processed/data\", ProcessedData)\n\n    async def _on_reading(self, msg: SensorReading, header: MessageHeader):\n        filtered = np.convolve(msg.values, np.ones(5) / 5, mode=\"same\")\n        self.processed_pub.publish(ProcessedData(\n            source_sensor=msg.sensor_id,\n            timestamp=msg.timestamp,\n            filtered_values=filtered.astype(\"f4\"),\n            statistics={\n                \"mean\": float(filtered.mean()),\n                \"std\":  float(filtered.std()),\n                \"min\":  float(filtered.min()),\n                \"max\":  float(filtered.max()),\n            },\n        ))\n
","path":["Tutorials","Multi-node system"],"tags":[]},{"location":"tutorials/multi-node-system/#monitor-node","level":2,"title":"Monitor node","text":"

Tracks throughput and publishes a periodic status message:

class MonitorNode(Node):\n    def __init__(self):\n        super().__init__(\"monitor\")\n        self.create_subscriber(\"/processed/data\", ProcessedData, callback=self._on_processed)\n        self.status_pub = self.create_publisher(\"/system/status\", SystemStatus)\n        self.create_timer(1.0, self._publish_status)\n
","path":["Tutorials","Multi-node system"],"tags":[]},{"location":"tutorials/multi-node-system/#running-the-whole-graph","level":2,"title":"Running the whole graph","text":"
async def main():\n    sensor_nodes = [SensorNode(sid, publish_rate=10.0) for sid in [\"lidar\", \"camera\"]]\n    processor_node = ProcessorNode([\"lidar\", \"camera\"])\n    monitor_node = MonitorNode()\n    all_nodes = [*sensor_nodes, processor_node, monitor_node]\n\n    await asyncio.sleep(1.0)  # let topics register and subscribers connect\n\n    try:\n        await asyncio.gather(*[n.run() for n in all_nodes])\n    finally:\n        for n in all_nodes:\n            await n.close()\n
","path":["Tutorials","Multi-node system"],"tags":[]},{"location":"tutorials/multi-node-system/#timeline","level":2,"title":"Timeline","text":"
sequenceDiagram\n    participant L as lidar\n    participant C as camera\n    participant P as processor\n    participant M as monitor\n\n    par at 10 Hz\n        L->>P: SensorReading\n    and\n        C->>P: SensorReading\n    end\n    P->>M: ProcessedData (per reading)\n    Note over M: counts per second\n    M->>M: publish SystemStatus (1 Hz)
","path":["Tutorials","Multi-node system"],"tags":[]},{"location":"tutorials/multi-node-system/#run-it-yourself","level":2,"title":"Run it yourself","text":"
# Terminal 1\ncortex-discovery\n\n# Terminal 2\npython examples/multi_node_system.py\n

Expected output:

[processor] Sensor lidar: mean=0.012, std=0.708\n[processor] Sensor camera: mean=-0.034, std=0.711\n[monitor] System status: 192 messages, 19.2 Hz processing rate\n
","path":["Tutorials","Multi-node system"],"tags":[]},{"location":"tutorials/multi-node-system/#see-also","level":2,"title":"See also","text":"
  • Tutorials → Custom messages
  • Components → Publisher & Subscriber
  • Components → Node & Executors
","path":["Tutorials","Multi-node system"],"tags":[]},{"location":"tutorials/numpy-and-images/","level":1,"title":"NumPy arrays & images","text":"

Cortex treats NumPy arrays as first-class payloads. Array bytes travel as separate ZMQ frames and are reconstructed with np.frombuffer on the receiver — no intermediate bytes object, no extra copy.

","path":["Tutorials","NumPy arrays & images"],"tags":[]},{"location":"tutorials/numpy-and-images/#pattern-publisher-that-emits-synthetic-frames","level":2,"title":"Pattern: publisher that emits synthetic frames","text":"camera.py
import numpy as np\nimport cortex\nfrom cortex import Node, ArrayMessage\n\n\nclass Camera(Node):\n    def __init__(self):\n        super().__init__(\"camera\")\n        self.pub = self.create_publisher(\"/cam/frame\", ArrayMessage)\n        self.create_timer(1 / 30, self.tick)  # 30 fps\n        self._i = 0\n\n    async def tick(self):\n        # Synthetic 640x480 RGB frame\n        frame = (np.random.rand(480, 640, 3) * 255).astype(\"uint8\")\n        self.pub.publish(ArrayMessage(data=frame, name=f\"f{self._i}\", frame_id=\"camera\"))\n        self._i += 1\n\n\ncortex.run(Camera().run())\n
","path":["Tutorials","NumPy arrays & images"],"tags":[]},{"location":"tutorials/numpy-and-images/#pattern-subscriber-that-processes-frames","level":2,"title":"Pattern: subscriber that processes frames","text":"viewer.py
import numpy as np\nimport cortex\nfrom cortex import Node, ArrayMessage\nfrom cortex.messages.base import MessageHeader\n\n\nasync def on_frame(msg: ArrayMessage, header: MessageHeader):\n    # msg.data aliases the ZMQ frame buffer — copy before mutating\n    frame = msg.data.copy()\n    frame[..., 0] = 0   # zero out red channel\n    print(f\"[{header.sequence}] {msg.name} mean={frame.mean():.1f}\")\n\n\nclass Viewer(Node):\n    def __init__(self):\n        super().__init__(\"viewer\")\n        self.create_subscriber(\"/cam/frame\", ArrayMessage, callback=on_frame)\n\n\ncortex.run(Viewer().run())\n
","path":["Tutorials","NumPy arrays & images"],"tags":[]},{"location":"tutorials/numpy-and-images/#aliasing-rule-of-thumb","level":2,"title":"Aliasing rule of thumb","text":"
flowchart LR\n    A[recv multipart<br/>copy=False] --> B[np.frombuffer view]\n    B --> C{Do you...}\n    C -->|only read inside callback| OK[Use as-is: fastest]\n    C -->|mutate| CP[arr = arr.copy]\n    C -->|keep past callback| CP\n    C -->|pass to another thread| CP\n    CP --> Safe[safe, owned copy]
","path":["Tutorials","NumPy arrays & images"],"tags":[]},{"location":"tutorials/numpy-and-images/#imagemessage-specifics","level":2,"title":"ImageMessage specifics","text":"

ImageMessage carries an encoding string plus optional width / height (auto-filled from the array shape):

from cortex.messages.standard import ImageMessage\n\nmsg = ImageMessage(data=frame, encoding=\"rgb8\")  # width/height filled on __post_init__\npub.publish(msg)\n

Encodings are free-form strings — Cortex does no validation or conversion. Downstream code decides what rgb8 / bgr8 / mono8 mean.

","path":["Tutorials","NumPy arrays & images"],"tags":[]},{"location":"tutorials/numpy-and-images/#zero-copy-footprint","level":2,"title":"Zero-copy footprint","text":"

A 1080p RGB frame is ~6 MB. On the benchmark suite:

  • Allocation on encode: zero (array is passed by view).
  • Allocation on decode: zero (array is a view into the ZMQ frame).
  • Throughput: ~1400 fps on a modern workstation.
","path":["Tutorials","NumPy arrays & images"],"tags":[]},{"location":"tutorials/numpy-and-images/#see-also","level":2,"title":"See also","text":"
  • Concepts → Message wire format
  • Components → Serialization
  • Guides → Performance tuning
","path":["Tutorials","NumPy arrays & images"],"tags":[]},{"location":"tutorials/pytorch-tensors/","level":1,"title":"PyTorch tensors","text":"

TensorMessage lets you pipe tensors between processes with the same zero-copy multipart transport used for NumPy arrays. Device and requires_grad metadata are preserved; the bytes travel via the CPU side of the tensor.

","path":["Tutorials","PyTorch tensors"],"tags":[]},{"location":"tutorials/pytorch-tensors/#publish","level":2,"title":"Publish","text":"inference_producer.py
import torch\nimport cortex\nfrom cortex import Node, TensorMessage\n\n\nclass Inference(Node):\n    def __init__(self):\n        super().__init__(\"inference\")\n        self.pub = self.create_publisher(\"/model/features\", TensorMessage)\n        self.create_timer(1 / 30, self.tick)\n\n    async def tick(self):\n        # Fake feature tensor; could be output of a real model\n        feats = torch.randn(4, 256, 7, 7, device=\"cuda\" if torch.cuda.is_available() else \"cpu\")\n        self.pub.publish(TensorMessage(data=feats, name=\"layer4_feats\"))\n\n\ncortex.run(Inference().run())\n
","path":["Tutorials","PyTorch tensors"],"tags":[]},{"location":"tutorials/pytorch-tensors/#subscribe","level":2,"title":"Subscribe","text":"downstream_consumer.py
import cortex\nfrom cortex import Node, TensorMessage\nfrom cortex.messages.base import MessageHeader\n\n\nasync def on_features(msg: TensorMessage, header: MessageHeader):\n    t = msg.data\n    print(f\"{msg.name}: shape={tuple(t.shape)} device={t.device} grad={t.requires_grad}\")\n\n\nclass Consumer(Node):\n    def __init__(self):\n        super().__init__(\"consumer\")\n        self.create_subscriber(\"/model/features\", TensorMessage, callback=on_features)\n\n\ncortex.run(Consumer().run())\n
","path":["Tutorials","PyTorch tensors"],"tags":[]},{"location":"tutorials/pytorch-tensors/#what-gets-preserved","level":2,"title":"What gets preserved","text":"
flowchart LR\n    A[torch.Tensor<br/>cuda:0, grad=True] --> B[encode: .detach.cpu.numpy<br/>contiguous]\n    B --> C[OOB frame + metadata<br/>device_str, requires_grad, dtype, shape]\n    C -. IPC .-> D[decode: np.frombuffer<br/>torch.from_numpy]\n    D --> E{cuda available?}\n    E -- yes --> F[move to device_str]\n    E -- no --> G[stay on CPU]\n    F --> H[requires_grad_ True if flagged]\n    G --> H
Attribute Transported dtype ✓ exact shapedevice ✓ string; restored on decode if available requires_gradgrad (the actual gradient) ✗ not sent autograd graph ✗ not sent (detach() is implicit)","path":["Tutorials","PyTorch tensors"],"tags":[]},{"location":"tutorials/pytorch-tensors/#multi-tensor-payloads","level":2,"title":"Multi-tensor payloads","text":"

When you need several tensors together — e.g. a model's inputs and outputs — use MultiTensorMessage:

from cortex.messages.standard import MultiTensorMessage\n\nmsg = MultiTensorMessage(tensors={\n    \"image\": image_tensor,\n    \"features\": feat_tensor,\n    \"logits\": logit_tensor,\n})\npub.publish(msg)\n

Each tensor gets its own OOB frame; no bytes are copied into a container.

","path":["Tutorials","PyTorch tensors"],"tags":[]},{"location":"tutorials/pytorch-tensors/#caveats","level":2,"title":"Caveats","text":"

CPU detour is mandatory

Even for two processes on the same GPU, tensors are DMA'd to CPU on send and back to GPU on receive. That is a copy on each side. Cortex does not currently support CUDA IPC — for tight in-process handoffs, prefer a torch.multiprocessing queue or shared CUDA memory.

Install with the torch extra

TensorMessage raises on construction if PyTorch is not installed. Use pip install -e \".[torch]\".

","path":["Tutorials","PyTorch tensors"],"tags":[]},{"location":"tutorials/pytorch-tensors/#see-also","level":2,"title":"See also","text":"
  • Concepts → Message wire format
  • Components → Serialization
  • Tutorials → NumPy arrays & images
","path":["Tutorials","PyTorch tensors"],"tags":[]}]} \ No newline at end of file diff --git a/docs/site/sitemap.xml b/docs/site/sitemap.xml deleted file mode 100644 index e6706d7..0000000 --- a/docs/site/sitemap.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - - https://sudoRicheek.github.io/cortex/ - - - https://sudoRicheek.github.io/cortex/getting-started/installation/ - - - https://sudoRicheek.github.io/cortex/getting-started/quickstart/ - - - https://sudoRicheek.github.io/cortex/getting-started/discovery-daemon/ - - - https://sudoRicheek.github.io/cortex/concepts/architecture/ - - - https://sudoRicheek.github.io/cortex/concepts/message-wire-format/ - - - https://sudoRicheek.github.io/cortex/concepts/fingerprinting/ - - - https://sudoRicheek.github.io/cortex/concepts/discovery-protocol/ - - - https://sudoRicheek.github.io/cortex/concepts/transport-and-qos/ - - - https://sudoRicheek.github.io/cortex/concepts/async-execution-model/ - - - https://sudoRicheek.github.io/cortex/components/messages/ - - - https://sudoRicheek.github.io/cortex/components/discovery/ - - - https://sudoRicheek.github.io/cortex/components/publisher-subscriber/ - - - https://sudoRicheek.github.io/cortex/components/node-and-executors/ - - - https://sudoRicheek.github.io/cortex/components/serialization/ - - - https://sudoRicheek.github.io/cortex/tutorials/custom-messages/ - - - https://sudoRicheek.github.io/cortex/tutorials/multi-node-system/ - - - https://sudoRicheek.github.io/cortex/tutorials/numpy-and-images/ - - - https://sudoRicheek.github.io/cortex/tutorials/pytorch-tensors/ - - - https://sudoRicheek.github.io/cortex/guides/performance-tuning/ - - - https://sudoRicheek.github.io/cortex/guides/benchmarks/ - - - https://sudoRicheek.github.io/cortex/guides/debugging/ - - - https://sudoRicheek.github.io/cortex/critique/ - - \ No newline at end of file diff --git a/docs/site/tutorials/custom-messages/index.html b/docs/site/tutorials/custom-messages/index.html deleted file mode 100644 index b71c7db..0000000 --- a/docs/site/tutorials/custom-messages/index.html +++ /dev/null @@ -1,1942 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Custom messages - Cortex - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - -
- - -
- -
- - - - - - - - - -
-
- - - -
-
-
- - - - - - - -
-
-
- - - - - - - -
- -
- - - - - - -

Custom messages

-

A message in Cortex is any dataclass that inherits from -[Message][cortex.messages.base.Message]. Auto-registration, fingerprinting, -and (de)serialization are all derived from the dataclass definition — you -write the schema once, publishers and subscribers speak the same wire format.

-

Define

-
messages.py
from dataclasses import dataclass
-import numpy as np
-from cortex.messages.base import Message
-
-
-@dataclass
-class RobotState(Message):
-    timestamp: float
-    position: np.ndarray      # shape (3,)
-    velocity: np.ndarray      # shape (3,)
-    joint_angles: np.ndarray  # shape (N,)
-    is_moving: bool
-    frame_id: str = "base_link"
-
-
-

Shared module

-

Put your message definitions in a module both the publisher and -subscriber import. The fingerprint is computed from -module.qualname + field names/types; an identical re-declaration in -two different modules produces different fingerprints.

-
-

Publish

-
publisher.py
import numpy as np
-import cortex
-from cortex import Node
-from messages import RobotState
-
-
-class StateBroadcaster(Node):
-    def __init__(self):
-        super().__init__("robot")
-        self.pub = self.create_publisher("/robot/state", RobotState)
-        self.create_timer(1 / 100, self.tick)  # 100 Hz
-        self._t0 = 0.0
-
-    async def tick(self):
-        self._t0 += 0.01
-        self.pub.publish(RobotState(
-            timestamp=self._t0,
-            position=np.array([self._t0, 0.0, 0.5], dtype="f4"),
-            velocity=np.array([1.0, 0.0, 0.0], dtype="f4"),
-            joint_angles=np.zeros(7, dtype="f4"),
-            is_moving=True,
-        ))
-
-
-if __name__ == "__main__":
-    cortex.run(StateBroadcaster().run())
-
-

Subscribe

-
subscriber.py
import cortex
-from cortex import Node
-from cortex.messages.base import MessageHeader
-from messages import RobotState   # same import, same fingerprint
-
-
-async def on_state(msg: RobotState, header: MessageHeader):
-    if header.sequence % 100 == 0:
-        print(f"t={msg.timestamp:.3f} pos={msg.position}")
-
-
-class Monitor(Node):
-    def __init__(self):
-        super().__init__("monitor")
-        self.create_subscriber("/robot/state", RobotState, callback=on_state)
-
-
-if __name__ == "__main__":
-    cortex.run(Monitor().run())
-
-

How the dataclass becomes a wire message

-
flowchart LR
-    DC[dataclass fields] --> FP[fingerprint]
-    DC --> ORD[declaration order]
-    ORD --> Enc[serialize_message_frames<br/>values in order]
-    Enc --> Meta[metadata frame]
-    Enc --> Bufs[array frames]
-    FP --> Hdr[24-byte header]
-    Hdr --> Wire[(multipart send)]
-    Meta --> Wire
-    Bufs --> Wire
-

See Concepts → Message wire format for -the full picture.

-

Supported field types

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Field typeNotes
int / float / bool / strPlain msgpack primitives
bytesmsgpack bin
list[...] / tuple[...]Walked recursively
dict[str, Any]Walked recursively; arrays inside are still OOB
np.ndarrayOOB frame; zero-copy decode
torch.TensorOOB frame; CPU-transported, device restored on decode
Optional nested MessageNot first-class today — flatten instead
-

Evolution: what breaks the fingerprint

-

Changing any of these changes the fingerprint and makes old and new -publishers/subscribers incompatible:

-
    -
  • Renaming the class, its module, or any field
  • -
  • Adding a field (even with a default)
  • -
  • Removing a field
  • -
  • Changing a field's annotation text
  • -
-

Safe to change without breaking:

-
    -
  • Reordering methods, adding methods
  • -
  • Editing docstrings or defaults
  • -
  • Changing unrelated classes in the same module
  • -
-

See critique § 22 for the roadmap on first-class schema -evolution.

-

See also

- - - - - - - - - - - - - - - - -
-
- - - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/site/tutorials/multi-node-system/index.html b/docs/site/tutorials/multi-node-system/index.html deleted file mode 100644 index ede4042..0000000 --- a/docs/site/tutorials/multi-node-system/index.html +++ /dev/null @@ -1,1980 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Multi-node system - Cortex - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - -
- - -
- -
- - - - - - - - - -
-
- - - -
-
-
- - - - - - - -
-
-
- - - -
-
- - - - - - - - -
- -
- - - -
-
- - - -
- -
- - - - - - -

Multi-node system

-

A walk-through of examples/multi_node_system.py — -a sensor → processor → monitor pipeline with custom messages, multiple -publishers and subscribers, and periodic status reporting.

-

Topology

-
flowchart LR
-    subgraph Sensors[Sensor nodes]
-        S1["sensor_lidar<br/>10 Hz"]
-        S2["sensor_camera<br/>10 Hz"]
-    end
-    Proc[processor]
-    Mon[monitor]
-
-    S1 -- "/sensor/lidar/raw" --> Proc
-    S2 -- "/sensor/camera/raw" --> Proc
-    Proc -- "/processed/data" --> Mon
-    Mon -- "/system/status<br/>1 Hz" --> World((world))
-

Four nodes run in a single Python process, each on the same asyncio event -loop via asyncio.gather. Cortex's IPC transport does not care that they -share a process — the data still rides through real ZMQ sockets.

-

Message schema

-

Three custom message types share a module so every node gets the same -fingerprints:

-
@dataclass
-class SensorReading(Message):
-    sensor_id: str
-    timestamp: float
-    values: np.ndarray
-    temperature: float
-
-@dataclass
-class ProcessedData(Message):
-    source_sensor: str
-    timestamp: float
-    filtered_values: np.ndarray
-    statistics: dict   # {mean, std, min, max}
-
-@dataclass
-class SystemStatus(Message):
-    timestamp: float
-    num_sensors: int
-    processing_rate_hz: float
-    total_messages: int
-
-

Sensor node

-
class SensorNode(Node):
-    def __init__(self, sensor_id: str, publish_rate: float = 10.0):
-        super().__init__(f"sensor_{sensor_id}")
-        self.reading_pub = self.create_publisher(
-            f"/sensor/{sensor_id}/raw", SensorReading
-        )
-        self.create_timer(1.0 / publish_rate, self._publish_reading)
-
-    async def _publish_reading(self):
-        t = time.time()
-        values = np.sin(np.linspace(0, 2*np.pi, 100) + t) + 0.1*np.random.randn(100)
-        self.reading_pub.publish(SensorReading(
-            sensor_id=self.sensor_id,
-            timestamp=t,
-            values=values.astype("f4"),
-            temperature=25.0 + 0.5*np.random.randn(),
-        ))
-
-

Processor node

-

Subscribes to every sensor and republishes filtered data:

-
class ProcessorNode(Node):
-    def __init__(self, sensor_ids: list[str]):
-        super().__init__("processor")
-        for sid in sensor_ids:
-            self.create_subscriber(
-                f"/sensor/{sid}/raw", SensorReading, callback=self._on_reading
-            )
-        self.processed_pub = self.create_publisher("/processed/data", ProcessedData)
-
-    async def _on_reading(self, msg: SensorReading, header: MessageHeader):
-        filtered = np.convolve(msg.values, np.ones(5) / 5, mode="same")
-        self.processed_pub.publish(ProcessedData(
-            source_sensor=msg.sensor_id,
-            timestamp=msg.timestamp,
-            filtered_values=filtered.astype("f4"),
-            statistics={
-                "mean": float(filtered.mean()),
-                "std":  float(filtered.std()),
-                "min":  float(filtered.min()),
-                "max":  float(filtered.max()),
-            },
-        ))
-
-

Monitor node

-

Tracks throughput and publishes a periodic status message:

-
class MonitorNode(Node):
-    def __init__(self):
-        super().__init__("monitor")
-        self.create_subscriber("/processed/data", ProcessedData, callback=self._on_processed)
-        self.status_pub = self.create_publisher("/system/status", SystemStatus)
-        self.create_timer(1.0, self._publish_status)
-
-

Running the whole graph

-
async def main():
-    sensor_nodes = [SensorNode(sid, publish_rate=10.0) for sid in ["lidar", "camera"]]
-    processor_node = ProcessorNode(["lidar", "camera"])
-    monitor_node = MonitorNode()
-    all_nodes = [*sensor_nodes, processor_node, monitor_node]
-
-    await asyncio.sleep(1.0)  # let topics register and subscribers connect
-
-    try:
-        await asyncio.gather(*[n.run() for n in all_nodes])
-    finally:
-        for n in all_nodes:
-            await n.close()
-
-

Timeline

-
sequenceDiagram
-    participant L as lidar
-    participant C as camera
-    participant P as processor
-    participant M as monitor
-
-    par at 10 Hz
-        L->>P: SensorReading
-    and
-        C->>P: SensorReading
-    end
-    P->>M: ProcessedData (per reading)
-    Note over M: counts per second
-    M->>M: publish SystemStatus (1 Hz)
-

Run it yourself

-
# Terminal 1
-cortex-discovery
-
-# Terminal 2
-python examples/multi_node_system.py
-
-

Expected output:

-
[processor] Sensor lidar: mean=0.012, std=0.708
-[processor] Sensor camera: mean=-0.034, std=0.711
-[monitor] System status: 192 messages, 19.2 Hz processing rate
-
-

See also

- - - - - - - - - - - - - - - - -
-
- - - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/site/tutorials/numpy-and-images/index.html b/docs/site/tutorials/numpy-and-images/index.html deleted file mode 100644 index 6527e90..0000000 --- a/docs/site/tutorials/numpy-and-images/index.html +++ /dev/null @@ -1,1849 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - NumPy arrays & images - Cortex - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - -
- - -
- -
- - - - - - - - - -
-
- - - -
-
-
- - - - - - - -
-
-
- - - - - - - -
- -
- - - - - - -

NumPy arrays & images

-

Cortex treats NumPy arrays as first-class payloads. Array bytes travel as -separate ZMQ frames and are reconstructed with np.frombuffer on the -receiver — no intermediate bytes object, no extra copy.

-

Pattern: publisher that emits synthetic frames

-
camera.py
import numpy as np
-import cortex
-from cortex import Node, ArrayMessage
-
-
-class Camera(Node):
-    def __init__(self):
-        super().__init__("camera")
-        self.pub = self.create_publisher("/cam/frame", ArrayMessage)
-        self.create_timer(1 / 30, self.tick)  # 30 fps
-        self._i = 0
-
-    async def tick(self):
-        # Synthetic 640x480 RGB frame
-        frame = (np.random.rand(480, 640, 3) * 255).astype("uint8")
-        self.pub.publish(ArrayMessage(data=frame, name=f"f{self._i}", frame_id="camera"))
-        self._i += 1
-
-
-cortex.run(Camera().run())
-
-

Pattern: subscriber that processes frames

-
viewer.py
import numpy as np
-import cortex
-from cortex import Node, ArrayMessage
-from cortex.messages.base import MessageHeader
-
-
-async def on_frame(msg: ArrayMessage, header: MessageHeader):
-    # msg.data aliases the ZMQ frame buffer — copy before mutating
-    frame = msg.data.copy()
-    frame[..., 0] = 0   # zero out red channel
-    print(f"[{header.sequence}] {msg.name} mean={frame.mean():.1f}")
-
-
-class Viewer(Node):
-    def __init__(self):
-        super().__init__("viewer")
-        self.create_subscriber("/cam/frame", ArrayMessage, callback=on_frame)
-
-
-cortex.run(Viewer().run())
-
-

Aliasing rule of thumb

-
flowchart LR
-    A[recv multipart<br/>copy=False] --> B[np.frombuffer view]
-    B --> C{Do you...}
-    C -->|only read inside callback| OK[Use as-is: fastest]
-    C -->|mutate| CP[arr = arr.copy]
-    C -->|keep past callback| CP
-    C -->|pass to another thread| CP
-    CP --> Safe[safe, owned copy]
-

ImageMessage specifics

-

[ImageMessage][cortex.messages.standard.ImageMessage] carries an encoding -string plus optional width / height (auto-filled from the array shape):

-
from cortex.messages.standard import ImageMessage
-
-msg = ImageMessage(data=frame, encoding="rgb8")  # width/height filled on __post_init__
-pub.publish(msg)
-
-

Encodings are free-form strings — Cortex does no validation or conversion. -Downstream code decides what rgb8 / bgr8 / mono8 mean.

-

Zero-copy footprint

-

A 1080p RGB frame is ~6 MB. On the benchmark suite:

-
    -
  • Allocation on encode: zero (array is passed by view).
  • -
  • Allocation on decode: zero (array is a view into the ZMQ frame).
  • -
  • Throughput: ~1400 fps on a modern workstation.
  • -
-

See also

- - - - - - - - - - - - - - - - -
-
- - - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/site/tutorials/pytorch-tensors/index.html b/docs/site/tutorials/pytorch-tensors/index.html deleted file mode 100644 index 74c29c2..0000000 --- a/docs/site/tutorials/pytorch-tensors/index.html +++ /dev/null @@ -1,1889 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - PyTorch tensors - Cortex - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - - - -
- - -
- -
- - - - - - - - - -
-
- - - -
-
-
- - - - - - - -
-
-
- - - -
-
- - - - - - - - -
- -
- - -
- - - - -
-
-
- - - -
- -
- - - - - - -

PyTorch tensors

-

[TensorMessage][cortex.messages.standard.TensorMessage] lets you pipe -tensors between processes with the same zero-copy multipart transport used -for NumPy arrays. Device and requires_grad metadata are preserved; the -bytes travel via the CPU side of the tensor.

-

Publish

-
inference_producer.py
import torch
-import cortex
-from cortex import Node, TensorMessage
-
-
-class Inference(Node):
-    def __init__(self):
-        super().__init__("inference")
-        self.pub = self.create_publisher("/model/features", TensorMessage)
-        self.create_timer(1 / 30, self.tick)
-
-    async def tick(self):
-        # Fake feature tensor; could be output of a real model
-        feats = torch.randn(4, 256, 7, 7, device="cuda" if torch.cuda.is_available() else "cpu")
-        self.pub.publish(TensorMessage(data=feats, name="layer4_feats"))
-
-
-cortex.run(Inference().run())
-
-

Subscribe

-
downstream_consumer.py
import cortex
-from cortex import Node, TensorMessage
-from cortex.messages.base import MessageHeader
-
-
-async def on_features(msg: TensorMessage, header: MessageHeader):
-    t = msg.data
-    print(f"{msg.name}: shape={tuple(t.shape)} device={t.device} grad={t.requires_grad}")
-
-
-class Consumer(Node):
-    def __init__(self):
-        super().__init__("consumer")
-        self.create_subscriber("/model/features", TensorMessage, callback=on_features)
-
-
-cortex.run(Consumer().run())
-
-

What gets preserved

-
flowchart LR
-    A[torch.Tensor<br/>cuda:0, grad=True] --> B[encode: .detach.cpu.numpy<br/>contiguous]
-    B --> C[OOB frame + metadata<br/>device_str, requires_grad, dtype, shape]
-    C -. IPC .-> D[decode: np.frombuffer<br/>torch.from_numpy]
-    D --> E{cuda available?}
-    E -- yes --> F[move to device_str]
-    E -- no --> G[stay on CPU]
-    F --> H[requires_grad_ True if flagged]
-    G --> H
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
AttributeTransported
dtype✓ exact
shape
device✓ string; restored on decode if available
requires_grad
grad (the actual gradient)✗ not sent
autograd graph✗ not sent (detach() is implicit)
-

Multi-tensor payloads

-

When you need several tensors together — e.g. a model's inputs and outputs -— use [MultiTensorMessage][cortex.messages.standard.MultiTensorMessage]:

-
from cortex.messages.standard import MultiTensorMessage
-
-msg = MultiTensorMessage(tensors={
-    "image": image_tensor,
-    "features": feat_tensor,
-    "logits": logit_tensor,
-})
-pub.publish(msg)
-
-

Each tensor gets its own OOB frame; no bytes are copied into a container.

-

Caveats

-
-

CPU detour is mandatory

-

Even for two processes on the same GPU, tensors are DMA'd to CPU on send -and back to GPU on receive. That is a copy on each side. Cortex does not -currently support CUDA IPC — for tight in-process handoffs, prefer a -torch.multiprocessing queue or shared CUDA memory.

-
-
-

Install with the torch extra

-

TensorMessage raises on construction if PyTorch is not installed. Use -pip install -e ".[torch]".

-
-

See also

- - - - - - - - - - - - - - - - -
-
- - - - - -
- - - -
- - - -
-
-
-
- - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/tutorials/custom-messages.md b/docs/tutorials/custom-messages.md index 4c9e280..fec14eb 100644 --- a/docs/tutorials/custom-messages.md +++ b/docs/tutorials/custom-messages.md @@ -1,9 +1,6 @@ # Custom messages -A **message** in Cortex is any dataclass that inherits from -[`Message`][cortex.messages.base.Message]. Auto-registration, fingerprinting, -and (de)serialization are all derived from the dataclass definition — you -write the schema once, publishers and subscribers speak the same wire format. +A message is any `@dataclass` that inherits from [`Message`][cortex.messages.base.Message]. Registration, fingerprinting, and (de)serialization are derived from the dataclass definition. ## Define @@ -24,10 +21,7 @@ class RobotState(Message): ``` !!! tip "Shared module" - Put your message definitions in a module **both** the publisher and - subscriber import. The fingerprint is computed from - `module.qualname` + field names/types; an identical re-declaration in - two different modules produces **different** fingerprints. + Put message definitions in a module **both** the publisher and subscriber import. The fingerprint is `module.qualname` + field names/types — declaring the same class in two different modules yields two different fingerprints. ## Publish @@ -84,7 +78,7 @@ if __name__ == "__main__": cortex.run(Monitor().run()) ``` -## How the dataclass becomes a wire message +## Dataclass → wire ```mermaid flowchart LR @@ -99,42 +93,37 @@ flowchart LR Bufs --> Wire ``` -See [Concepts → Message wire format](../concepts/message-wire-format.md) for -the full picture. +See [Message wire format](../concepts/message-wire-format.md) for the full encoding. ## Supported field types | Field type | Notes | | ------------------------------- | ------------------------------------------------------- | -| `int` / `float` / `bool` / `str`| Plain msgpack primitives | +| `int` / `float` / `bool` / `str`| msgpack primitives | | `bytes` | msgpack bin | -| `list[...]` / `tuple[...]` | Walked recursively | -| `dict[str, Any]` | Walked recursively; arrays inside are still OOB | +| `list[...]` / `tuple[...]` | walked recursively | +| `dict[str, Any]` | walked recursively; arrays inside still go OOB | | `np.ndarray` | OOB frame; zero-copy decode | | `torch.Tensor` | OOB frame; CPU-transported, device restored on decode | -| Optional nested `Message` | Not first-class today — flatten instead | +| Nested `Message` | not supported — flatten instead | -## Evolution: what breaks the fingerprint +## What changes the fingerprint -Changing any of these **changes the fingerprint** and makes old and new -publishers/subscribers incompatible: +Any of these makes old and new publishers/subscribers incompatible: - Renaming the class, its module, or any field - Adding a field (even with a default) - Removing a field - Changing a field's annotation text -Safe to change without breaking: +Safe to change: - Reordering methods, adding methods - Editing docstrings or defaults - Changing unrelated classes in the same module -See [critique § 22](../critique.md) for the roadmap on first-class schema -evolution. - ## See also - [Concepts → Fingerprinting](../concepts/fingerprinting.md) - [Components → Messages](../components/messages.md) -- [Tutorials → Multi-node system](multi-node-system.md) for custom messages used across multiple nodes +- [Tutorials → Multi-node system](multi-node-system.md) diff --git a/docs/tutorials/multi-node-system.md b/docs/tutorials/multi-node-system.md index 6f6dbe8..3163545 100644 --- a/docs/tutorials/multi-node-system.md +++ b/docs/tutorials/multi-node-system.md @@ -1,8 +1,6 @@ # Multi-node system -A walk-through of [`examples/multi_node_system.py`](https://github.com/sudoRicheek/cortex/blob/main/examples/multi_node_system.py) — -a sensor → processor → monitor pipeline with custom messages, multiple -publishers and subscribers, and periodic status reporting. +Walk-through of [`examples/multi_node_system.py`](https://github.com/sudoRicheek/cortex/blob/main/examples/multi_node_system.py) — a sensor → processor → monitor pipeline with custom messages and multiple pub/subs. ## Topology @@ -21,14 +19,11 @@ flowchart LR Mon -- "/system/status
1 Hz" --> World((world)) ``` -Four nodes run in a single Python process, each on the same asyncio event -loop via `asyncio.gather`. Cortex's IPC transport does not care that they -share a process — the data still rides through real ZMQ sockets. +Four nodes share one asyncio event loop via `asyncio.gather`. Cortex IPC works just as well between nodes in one process as between separate processes — data still rides ZMQ sockets either way. ## Message schema -Three custom message types share a module so every node gets the same -fingerprints: +Three dataclasses shared between every node so fingerprints match: ```python @dataclass @@ -75,9 +70,9 @@ class SensorNode(Node): )) ``` -## Processor node +## Processor -Subscribes to every sensor and republishes filtered data: +Subscribes to every sensor, filters, republishes: ```python class ProcessorNode(Node): @@ -104,9 +99,9 @@ class ProcessorNode(Node): )) ``` -## Monitor node +## Monitor -Tracks throughput and publishes a periodic status message: +Tracks throughput, emits status at 1 Hz: ```python class MonitorNode(Node): @@ -117,7 +112,7 @@ class MonitorNode(Node): self.create_timer(1.0, self._publish_status) ``` -## Running the whole graph +## Run the graph ```python async def main(): @@ -154,13 +149,13 @@ sequenceDiagram M->>M: publish SystemStatus (1 Hz) ``` -## Run it yourself +## Run it ```bash -# Terminal 1 +# terminal 1 cortex-discovery -# Terminal 2 +# terminal 2 python examples/multi_node_system.py ``` diff --git a/docs/tutorials/numpy-and-images.md b/docs/tutorials/numpy-and-images.md index 04e7e15..f33ffef 100644 --- a/docs/tutorials/numpy-and-images.md +++ b/docs/tutorials/numpy-and-images.md @@ -1,15 +1,14 @@ # NumPy arrays & images -Cortex treats NumPy arrays as first-class payloads. Array bytes travel as -separate ZMQ frames and are reconstructed with `np.frombuffer` on the -receiver — no intermediate `bytes` object, no extra copy. +NumPy arrays are first-class payloads. Array bytes travel as separate ZMQ frames and are reconstructed with `np.frombuffer` on the receiver — no intermediate `bytes`, no copy. -## Pattern: publisher that emits synthetic frames +## Publisher ```python title="camera.py" import numpy as np import cortex -from cortex import Node, ArrayMessage +from cortex import Node +from cortex.messages.standard import ArrayMessage class Camera(Node): @@ -20,7 +19,6 @@ class Camera(Node): self._i = 0 async def tick(self): - # Synthetic 640x480 RGB frame frame = (np.random.rand(480, 640, 3) * 255).astype("uint8") self.pub.publish(ArrayMessage(data=frame, name=f"f{self._i}", frame_id="camera")) self._i += 1 @@ -29,13 +27,13 @@ class Camera(Node): cortex.run(Camera().run()) ``` -## Pattern: subscriber that processes frames +## Subscriber ```python title="viewer.py" -import numpy as np import cortex -from cortex import Node, ArrayMessage +from cortex import Node from cortex.messages.base import MessageHeader +from cortex.messages.standard import ArrayMessage async def on_frame(msg: ArrayMessage, header: MessageHeader): @@ -54,40 +52,39 @@ class Viewer(Node): cortex.run(Viewer().run()) ``` -## Aliasing rule of thumb +## Aliasing rule ```mermaid flowchart LR A[recv multipart
copy=False] --> B[np.frombuffer view] B --> C{Do you...} - C -->|only read inside callback| OK[Use as-is: fastest] - C -->|mutate| CP[arr = arr.copy] + C -->|only read inside callback| OK[Use as-is] + C -->|mutate| CP[arr.copy] C -->|keep past callback| CP - C -->|pass to another thread| CP - CP --> Safe[safe, owned copy] + C -->|hand off to another thread| CP ``` -## `ImageMessage` specifics +The view is valid for the lifetime of the ZMQ frame, which ends when the callback returns. Copy if you need ownership. + +## `ImageMessage` -[`ImageMessage`][cortex.messages.standard.ImageMessage] carries an `encoding` -string plus optional `width` / `height` (auto-filled from the array shape): +[`ImageMessage`][cortex.messages.standard.ImageMessage] adds an `encoding` string plus optional `width`/`height` (auto-filled from shape): ```python from cortex.messages.standard import ImageMessage -msg = ImageMessage(data=frame, encoding="rgb8") # width/height filled on __post_init__ +msg = ImageMessage(data=frame, encoding="rgb8") # width/height set in __post_init__ pub.publish(msg) ``` -Encodings are free-form strings — Cortex does no validation or conversion. -Downstream code decides what `rgb8` / `bgr8` / `mono8` mean. +Encodings are free-form. Cortex doesn't validate or convert them. -## Zero-copy footprint +## Cost -A 1080p RGB frame is ~6 MB. On the benchmark suite: +1080p RGB frame ≈ 6 MB. On the benchmark suite: -- Allocation on encode: **zero** (array is passed by view). -- Allocation on decode: **zero** (array is a view into the ZMQ frame). +- Encode allocation: zero. +- Decode allocation: zero (array is a view into the ZMQ frame). - Throughput: ~1400 fps on a modern workstation. ## See also diff --git a/docs/tutorials/pytorch-tensors.md b/docs/tutorials/pytorch-tensors.md index 0e06503..b29b5b0 100644 --- a/docs/tutorials/pytorch-tensors.md +++ b/docs/tutorials/pytorch-tensors.md @@ -1,16 +1,14 @@ # PyTorch tensors -[`TensorMessage`][cortex.messages.standard.TensorMessage] lets you pipe -tensors between processes with the same zero-copy multipart transport used -for NumPy arrays. Device and `requires_grad` metadata are preserved; the -bytes travel via the CPU side of the tensor. +[`TensorMessage`][cortex.messages.standard.TensorMessage] pipes tensors between processes using the same zero-copy multipart transport as NumPy. Device and `requires_grad` are preserved; bytes travel via the CPU side. ## Publish ```python title="inference_producer.py" import torch import cortex -from cortex import Node, TensorMessage +from cortex import Node +from cortex.messages.standard import TensorMessage class Inference(Node): @@ -20,7 +18,6 @@ class Inference(Node): self.create_timer(1 / 30, self.tick) async def tick(self): - # Fake feature tensor; could be output of a real model feats = torch.randn(4, 256, 7, 7, device="cuda" if torch.cuda.is_available() else "cpu") self.pub.publish(TensorMessage(data=feats, name="layer4_feats")) @@ -32,8 +29,9 @@ cortex.run(Inference().run()) ```python title="downstream_consumer.py" import cortex -from cortex import Node, TensorMessage +from cortex import Node from cortex.messages.base import MessageHeader +from cortex.messages.standard import TensorMessage async def on_features(msg: TensorMessage, header: MessageHeader): @@ -50,7 +48,7 @@ class Consumer(Node): cortex.run(Consumer().run()) ``` -## What gets preserved +## What's preserved ```mermaid flowchart LR @@ -70,13 +68,12 @@ flowchart LR | `shape` | ✓ | | `device` | ✓ string; restored on decode if available | | `requires_grad` | ✓ | -| `grad` (the actual gradient) | ✗ not sent | +| `grad` (the gradient itself) | ✗ not sent | | autograd graph | ✗ not sent (`detach()` is implicit) | ## Multi-tensor payloads -When you need several tensors together — e.g. a model's inputs and outputs -— use [`MultiTensorMessage`][cortex.messages.standard.MultiTensorMessage]: +[`MultiTensorMessage`][cortex.messages.standard.MultiTensorMessage] carries several tensors at once. Each gets its own OOB frame; no bytes are copied into a container. ```python from cortex.messages.standard import MultiTensorMessage @@ -89,19 +86,13 @@ msg = MultiTensorMessage(tensors={ pub.publish(msg) ``` -Each tensor gets its own OOB frame; no bytes are copied into a container. - ## Caveats !!! warning "CPU detour is mandatory" - Even for two processes on the same GPU, tensors are DMA'd to CPU on send - and back to GPU on receive. That is a copy on each side. Cortex does not - currently support CUDA IPC — for tight in-process handoffs, prefer a - torch.multiprocessing queue or shared CUDA memory. + Even for two processes on the same GPU, tensors are DMA'd to CPU on send and back to GPU on receive — one copy per side. Cortex does not support CUDA IPC. For tight in-process handoffs, use `torch.multiprocessing` or shared CUDA memory directly. !!! note "Install with the `torch` extra" - `TensorMessage` raises on construction if PyTorch is not installed. Use - `pip install -e ".[torch]"`. + `TensorMessage` raises on construction if PyTorch is not installed. `pip install -e ".[torch]"`. ## See also diff --git a/pyproject.toml b/pyproject.toml index 2cb9345..af65781 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,9 +43,6 @@ dev = [ docs = [ "zensical", "mkdocstrings[python]>=0.26", - "mkdocs-gen-files>=0.5", - "mkdocs-literate-nav>=0.6", - "mkdocs-section-index>=0.3", "ruff>=0.8.0", ] all = [ From ad8d2909527d8b1653dabef5967d6d2a789e7a46 Mon Sep 17 00:00:00 2001 From: Richeek Das Date: Wed, 13 May 2026 02:08:13 -0400 Subject: [PATCH 2/3] lint --- docs/gen_ref_pages.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/docs/gen_ref_pages.py b/docs/gen_ref_pages.py index c1e8007..cb47130 100644 --- a/docs/gen_ref_pages.py +++ b/docs/gen_ref_pages.py @@ -17,6 +17,7 @@ --check Exit non-zero if the generated tree differs from what is checked in. """ + from __future__ import annotations import argparse @@ -45,8 +46,9 @@ def collect_modules() -> list[tuple[Path, str]]: if parts[-1] == "__init__": parts = parts[:-1] - doc_path = doc_path.with_name("index.md") if doc_path.parts else Path( - "index.md") + doc_path = ( + doc_path.with_name("index.md") if doc_path.parts else Path("index.md") + ) elif parts[-1].startswith("_"): continue @@ -113,10 +115,11 @@ def write_pages(pages: dict[Path, str]) -> None: def check_pages(pages: dict[Path, str]) -> int: drift: list[str] = [] expected_paths = {Path(p) for p in pages} - actual_paths = { - p.relative_to(REFERENCE_DIR) - for p in REFERENCE_DIR.rglob("*.md") - } if REFERENCE_DIR.exists() else set() + actual_paths = ( + {p.relative_to(REFERENCE_DIR) for p in REFERENCE_DIR.rglob("*.md")} + if REFERENCE_DIR.exists() + else set() + ) for missing in sorted(expected_paths - actual_paths): drift.append(f"missing: docs/reference/{missing.as_posix()}") @@ -128,7 +131,9 @@ def check_pages(pages: dict[Path, str]) -> int: drift.append(f"out of date: docs/reference/{rel.as_posix()}") if drift: - print("error: docs/reference/ is out of sync with src/cortex/:", file=sys.stderr) + print( + "error: docs/reference/ is out of sync with src/cortex/:", file=sys.stderr + ) for line in drift: print(f" - {line}", file=sys.stderr) print( From c5d122f76989c59e72f54100485c39d818a63df0 Mon Sep 17 00:00:00 2001 From: Richeek Das Date: Wed, 13 May 2026 02:10:56 -0400 Subject: [PATCH 3/3] stupid bug --- docs/reference/zzz_extra.md | 1 - 1 file changed, 1 deletion(-) delete mode 100644 docs/reference/zzz_extra.md diff --git a/docs/reference/zzz_extra.md b/docs/reference/zzz_extra.md deleted file mode 100644 index 0f22871..0000000 --- a/docs/reference/zzz_extra.md +++ /dev/null @@ -1 +0,0 @@ -extra