diff --git a/.DS_Store b/.DS_Store index 173056d..af572a6 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.github/workflows/build_package.yml b/.github/workflows/build_package.yml new file mode 100644 index 0000000..536a757 --- /dev/null +++ b/.github/workflows/build_package.yml @@ -0,0 +1,58 @@ +name: Build Package + +on: + push: + pull_request: + +jobs: + build_docs: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.10' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install -e ".[docs]" + + - name: Build API + run: | + python tools/build/create_api.py + + - name: Build Docs + run: | + cd tools/docs + make clean + python create_templates.py + python create_documents.py + make dirhtml + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: tools/docs/_build/dirhtml + + deploy_docs: + needs: build_docs + runs-on: ubuntu-latest + + permissions: + pages: write + id-token: write + + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2e9003f..b479bed 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,10 @@ venv/ # OS files .DS_Store -Thumbs.db \ No newline at end of file +Thumbs.db + +# pytest +.pytest_cache/ + +# dev tests +*.ipynb diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..c5bf032 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,7 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.12.0 # use the latest Ruff version you prefer + hooks: + - id: ruff + args: [--fix] + - id: ruff-format \ No newline at end of file diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..1b055a9 --- /dev/null +++ b/README.rst @@ -0,0 +1,41 @@ +im2sim +====== + +``im2sim`` is a library designed to simplify the development of ML-accelerated +digital twins based on medical images. + +This includes two main components: + +1. Deep Learning (DL) frameworks based on + `PyTorch `_ and + `PyTorch Geometric `_ + +2. Mesh processing frameworks based on + `VTK `_ and + `PyVista `_ + +Features +-------- + +* Image and Mesh Data Processing +* DL models for medical imaging applications +* DL models for digital twin applications +* Hybrid DL models for simulation outputs directly from images +* Building blocks for custom DL models +* Visualisation utilities + +Installation +------------ + +1. Install the CUDA dependencies: + +.. code-block:: bash + + pip install torch-scatter torch-cluster \ + -f https://data.pydata.org/whl/torch-2.3.1+cu121.html + +2. Install the repository: + +.. code-block:: bash + + pip install git+https://github.com/mrphys/im2sim.git \ No newline at end of file diff --git a/im2sim/__about__.py b/im2sim/__about__.py new file mode 100644 index 0000000..9028be0 --- /dev/null +++ b/im2sim/__about__.py @@ -0,0 +1,38 @@ +# Copyright 2026 University College London. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""About IM2SIM""" + +__all__ = [ + "__title__", + "__summary__", + "__uri__", + "__version__", + "__author__", + "__email__", + "__license__", + "__copyright__", +] + +__title__ = "im2sim" +__summary__ = "A library to accelerate the development of deep learning models for medical image analysis and simulation." +__uri__ = "https://github.com/mrphys/im2sim" + +__version__ = "0.1.0" + +__author__ = "Anirudh Raman, Vivek Muthurangu, Javier Montalt-Tordera" +__email__ = "v.muthurangu@ucl.ac.uk" + +__license__ = "Apache 2.0" +__copyright__ = "2026 University College London" diff --git a/im2sim/__init__.py b/im2sim/__init__.py index 0a89292..0d81f8a 100644 --- a/im2sim/__init__.py +++ b/im2sim/__init__.py @@ -1 +1,28 @@ -from . import callbacks, data, layers, losses, metrics, models, utils +# This file was automatically generated by tools/build/create_api.py. +# Do not edit. +"""IM2SIM.""" +import os as _os +import sys as _sys + +from im2sim.__about__ import * + +# Import submodules. +from im2sim._api import data +from im2sim._api import layers +from im2sim._api import configs +from im2sim._api import ops +from im2sim._api import losses +from im2sim._api import models +from im2sim._api import plot + +# Make sure directory containing top level submodules is in +# the __path__ so that "from tensorflow_mri.foo import bar" works. +# We're using callbacks, but there's nothing special about that. +_API_MODULE = _sys.modules[__name__].layers +_im2sim_api_dir = _os.path.dirname(_os.path.dirname(_API_MODULE.__file__)) +_current_module = _sys.modules[__name__] + +if not hasattr(_current_module, '__path__'): + __path__ = [_im2sim_api_dir] +elif _im2sim_api_dir not in __path__: + __path__.append(_im2sim_api_dir) diff --git a/im2sim/_api/configs/__init__.py b/im2sim/_api/configs/__init__.py new file mode 100644 index 0000000..827cfd5 --- /dev/null +++ b/im2sim/_api/configs/__init__.py @@ -0,0 +1,6 @@ +# This file was automatically generated by tools/build/create_api.py. +# Do not edit. +"""Configuration classes for model and training settings.""" + +from im2sim.src.layers.image_conv_blocks import ImageConvBlockConfig as ImageConvBlockConfig +from im2sim.src.layers.halfunet import HalfUNetConfig as HalfUNetConfig diff --git a/im2sim/_api/data/__init__.py b/im2sim/_api/data/__init__.py new file mode 100644 index 0000000..81821ce --- /dev/null +++ b/im2sim/_api/data/__init__.py @@ -0,0 +1,5 @@ +# This file was automatically generated by tools/build/create_api.py. +# Do not edit. +"""Data loading and preprocessing utilities.""" + + diff --git a/im2sim/_api/layers/__init__.py b/im2sim/_api/layers/__init__.py new file mode 100644 index 0000000..a55376e --- /dev/null +++ b/im2sim/_api/layers/__init__.py @@ -0,0 +1,15 @@ +# This file was automatically generated by tools/build/create_api.py. +# Do not edit. +"""Custom layers for building deep learning models.""" + +from im2sim.src.layers.graph_blocks import DefaultGraphNorm as DefaultGraphNorm +from im2sim.src.layers.graph_blocks import GraphConvBlock as GraphConvBlock +from im2sim.src.layers.graph_blocks import GraphConvResBlock as GraphConvResBlock +from im2sim.src.layers.graph_blocks import GraphResDecoderBlock as GraphResDecoderBlock +from im2sim.src.layers.custom_image_layers import DepthwiseConv as DepthwiseConv +from im2sim.src.layers.custom_image_layers import DepthwiseSeparableConv as DepthwiseSeparableConv +from im2sim.src.layers.custom_image_layers import GhostConv as GhostConv +from im2sim.src.layers.custom_image_layers import EfficientChannelAttn as EfficientChannelAttn +from im2sim.src.layers.custom_image_layers import SqueezeExcite as SqueezeExcite +from im2sim.src.layers.custom_image_layers import ConditionedSqueezeExcite as ConditionedSqueezeExcite +from im2sim.src.layers.image_conv_blocks import ImageConvBlock as ImageConvBlock diff --git a/im2sim/_api/losses/__init__.py b/im2sim/_api/losses/__init__.py new file mode 100644 index 0000000..6228ba5 --- /dev/null +++ b/im2sim/_api/losses/__init__.py @@ -0,0 +1,5 @@ +# This file was automatically generated by tools/build/create_api.py. +# Do not edit. +"""Custom loss functions for training deep learning models.""" + + diff --git a/im2sim/_api/models/__init__.py b/im2sim/_api/models/__init__.py new file mode 100644 index 0000000..b762097 --- /dev/null +++ b/im2sim/_api/models/__init__.py @@ -0,0 +1,5 @@ +# This file was automatically generated by tools/build/create_api.py. +# Do not edit. +"""Predefined deep learning models for various tasks.""" + +from im2sim.src.layers.halfunet import HalfUNet as HalfUNet diff --git a/im2sim/_api/ops/__init__.py b/im2sim/_api/ops/__init__.py new file mode 100644 index 0000000..b87d432 --- /dev/null +++ b/im2sim/_api/ops/__init__.py @@ -0,0 +1,5 @@ +# This file was automatically generated by tools/build/create_api.py. +# Do not edit. +"""Custom operations for deep learning models.""" + +from im2sim.src.data.ops import normtorange as normtorange diff --git a/im2sim/_api/plot/__init__.py b/im2sim/_api/plot/__init__.py new file mode 100644 index 0000000..377748d --- /dev/null +++ b/im2sim/_api/plot/__init__.py @@ -0,0 +1,5 @@ +# This file was automatically generated by tools/build/create_api.py. +# Do not edit. +"""Utilities for visualizing data and model outputs.""" + + diff --git a/im2sim/callbacks/contents.md b/im2sim/callbacks/contents.md deleted file mode 100644 index 9257ff4..0000000 --- a/im2sim/callbacks/contents.md +++ /dev/null @@ -1,2 +0,0 @@ -1. plotting callbacks -2. metric calculation callbacks \ No newline at end of file diff --git a/im2sim/data/__init__.py b/im2sim/data/__init__.py deleted file mode 100644 index e466aee..0000000 --- a/im2sim/data/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .core import * -from . import mesh_utils,transforms,ops \ No newline at end of file diff --git a/im2sim/data/mesh_utils.py b/im2sim/data/mesh_utils.py deleted file mode 100644 index 43f29d3..0000000 --- a/im2sim/data/mesh_utils.py +++ /dev/null @@ -1,77 +0,0 @@ -import numpy as np -import torch -from itertools import combinations -from torch_geometric.utils import to_undirected -import torch_geometric.nn as gnn - - -# def get_node_ids(mesh): -# cell_ids = np.zeros((len(mesh.points),)) -# for id in np.unique(mesh['CellEntityIds']): -# cells = mesh.extract_cells(np.where(mesh['CellEntityIds'] == id)[0])['vtkOriginalPointIds'] -# nodes = np.unique(cells) -# cell_ids[nodes] = id -# return cell_ids - -def add_structure_masks(data, mesh, structure_list): - ids = np.unique(mesh['CellEntityIds']) - missing_ids = set(range(len(structure_list))) - set(ids.tolist()) - for id in missing_ids: - setattr(data, f'{structure_list[id]}_mask', torch.zeros((len(mesh.points))).to(torch.bool)) - for id, name in zip(ids, structure_list): - cell_ids = torch.zeros((len(mesh.points))) - cells = mesh.extract_cells(np.where(mesh['CellEntityIds'] == id)[0])['vtkOriginalPointIds'] - nodes = np.unique(cells) - cell_ids[nodes] = 1 - setattr(data, f'{name}_mask', cell_ids.to(torch.bool)) - - -def get_edges_tet(mesh): - tet_cells = mesh.extract_cells(np.where(mesh['CellEntityIds'] == 0)[0]) - tet_cells = tet_cells.cells.reshape(-1, 5)[:, 1:] - edges = np.reshape(np.array([list(combinations(cell,2)) for cell in tet_cells]), [-1,2]) - edges = torch.from_numpy(np.unique(edges, axis=0).T) - edges = to_undirected(edges) - return edges - -def get_node_features(mesh, feature_names): - features = torch.from_numpy(np.array([mesh.point_data[name] for name in feature_names]).T) - return features - -# def set_structure_masks(data, mesh, structure_list): -# node_ids = get_node_ids(mesh) -# for id, structure in enumerate(structure_list): -# setattr(data, f"is_{structure}", torch.from_numpy(node_ids==id)) -# return data - -def get_tet_cells(mesh): - tet_cells = mesh.extract_cells(np.where(mesh['CellEntityIds'] == 0)[0]) - tet_cells = tet_cells.cells.reshape(-1, 5)[:, 1:] - tet_cells = torch.from_numpy(tet_cells).permute(1,0) - return tet_cells - -def make_padded_batch(x, batch): - jagged_x = [x[batch==i] for i in torch.unique(batch)] - padded_x= torch.nn.utils.rnn.pad_sequence(jagged_x, batch_first=True) - lengths = torch.tensor([len(s) for s in jagged_x]) - mask = torch.arange(padded_x.size(1))[None, :] < lengths[:, None] - return padded_x, mask - - -def _compute_edge_lengths(points, edges): - coords = points[edges] - distances = (coords[0] - coords[1])**2 - return distances - -def cluster_pool(mesh): - distances = _compute_edge_lengths(mesh.x, mesh.edge_index).sum(-1) - weights = 1/(distances + 1e-8) - clusters = gnn.graclus(mesh.edge_index,weights, mesh.x.shape[0]) - pooled_mesh = gnn.avg_pool(clusters, mesh) - return pooled_mesh - -def extract_features(mesh, fnames): - out = [] - for name in fnames: - out.append(torch.from_numpy(mesh.point_data[name])) - return torch.stack(out, dim=-1) diff --git a/im2sim/data/transforms.py b/im2sim/data/transforms.py deleted file mode 100644 index 3da5782..0000000 --- a/im2sim/data/transforms.py +++ /dev/null @@ -1,52 +0,0 @@ -from .ops import * -from .core import * - - - -def transform_from_fn(fn, keys,attr=None,channels=None,per_channel=False,channel_dim=-1,name=None): - - class FnOp(Operation): - - def forward(self, x): - return fn(x) - - return Transform(op=FnOp(), keys=keys,attr=attr,channels=channels,per_channel=per_channel,channel_dim=channel_dim,name=name) - - -# ------------------------------------------------------------------------------------ -# SIMPLE TRANSFORM FACTORIES -# ------------------------------------------------------------------------------------ - - -def Norm(keys,attr=None,channels=None,per_channel=False,channel_dim=-1,name=None): - return Transform(op=NormOp(), keys=keys,attr=attr,channels=channels,per_channel=per_channel,channel_dim=channel_dim,name=name) - -def RangeNorm(llim, hlim, keys,attr=None,channels=None,per_channel=False,channel_dim=-1,name=None): - return Transform(op=RangeNormOp(a=llim, b=hlim), keys=keys,attr=attr,channels=channels,per_channel=per_channel,channel_dim=channel_dim,name=name) - -def ZScore(keys,attr=None,channels=None,per_channel=False,channel_dim=-1,name=None): - return Transform(op=ZScoreOp(), keys=keys,attr=attr,channels=channels,per_channel=per_channel,channel_dim=channel_dim,name=name) - -# ------------------------------------------------------------------------------------ -# INVERTIBLE TRANSFORM FACTORIES -# ------------------------------------------------------------------------------------ - -def PowerScaling(exp,preserve_sign, keys,attr=None,channels=None,per_channel=False,channel_dim=-1,name=None): - return Transform(op=PowerScaleOp(exp=exp,preserve_sign=preserve_sign), keys=keys,attr=attr,channels=channels,per_channel=per_channel,channel_dim=channel_dim,name=name) - -# ------------------------------------------------------------------------------------ -# FITTABLE TRANSFORM FACTORIES -# ------------------------------------------------------------------------------------ - - -def FitNorm(keys,attr=None,channels=None,per_channel=False,channel_dim=-1,name=None): - return Transform(op=FitNormOp(), keys=keys,attr=attr,channels=channels,per_channel=per_channel,channel_dim=channel_dim,name=name) - -def FitRangeNorm(llim, hlim, keys,attr=None,channels=None,per_channel=False,channel_dim=-1,name=None): - return Transform(op=FitRangeNormOp(a=llim, b=hlim), keys=keys,attr=attr,channels=channels,per_channel=per_channel,channel_dim=channel_dim,name=name) - -def FitZScore(keys,attr=None,channels=None,per_channel=False,channel_dim=-1,name=None): - return Transform(op=FitZScoreOp(), keys=keys,attr=attr,channels=channels,per_channel=per_channel,channel_dim=channel_dim,name=name) - - - diff --git a/im2sim/layers/__init__.py b/im2sim/layers/__init__.py deleted file mode 100644 index 542a28e..0000000 --- a/im2sim/layers/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .graph_convs import * -from .image_convs import * -from .projections import * -from .layer_util import get_image_layer, get_activation, get_default_kwargs, get_graph_layer - diff --git a/im2sim/layers/graph_convs.py b/im2sim/layers/graph_convs.py deleted file mode 100644 index 10c3904..0000000 --- a/im2sim/layers/graph_convs.py +++ /dev/null @@ -1,311 +0,0 @@ -import logging - -import torch -from torch import nn -import torch_geometric.nn as gnn - -from .layer_util import get_graph_layer, get_activation -from .projections import TrilinearProjection -from ..data.mesh_utils import cluster_pool - - - - -logger = logging.getLogger(__name__) - -class GraphConvBlock(nn.Module): - """ - A convolutional block for graph data - - Args: - in_channels (int): The number of channels in the input to the layer. - filters (int, optional): The number of filters in each convolutional layer (default: 32) - depth (int, optional): The number of successive convolutional layers (default: 2) - conv_type (str, optional): The type of graph convolution to apply (default: "ChebConv", options: "GraphConv", "GCNConv", "GATConv") - conv_kwargs(dict, optional): Dictionary of keyword arguments for the chosen conv_type - activation (str, optional): The activation function applied after each convolution (default: "relu", options: "leakyrelu","gelu","sigmoid","linear") - norm_type (str, optional): The normalization method to apply between convolutions (default:"InstanceNorm", options: "BatchNorm", "LayerNorm") - - Returns: - A `torch.nn.Module` object. - - """ - def __init__(self, - in_channels, - filters, - depth=1, - conv_type='ChebConv', - conv_kwargs={'K':3}, - activation='relu', - norm_type='InstanceNorm'): - super().__init__() - - conv = get_graph_layer(conv_type) - self.convs = nn.ModuleList([ - conv(in_channels if i==0 else filters, filters, **conv_kwargs) - for i in range(depth) - ]) - - self.norms = nn.ModuleList([ - get_graph_layer(norm_type)(filters) if norm_type else nn.Identity() - for _ in range(depth) - ]) - - self.act = get_activation(activation)(inplace=True) if activation.lower() == 'relu' else get_activation(activation)() - - def forward(self, x, edge_index): - for conv, norm in zip(self.convs, self.norms): - logger.debug("Graph features shape:%s", x.shape) - x = norm(self.act(conv(x,edge_index))) - return x - -class GraphConvResBlock(nn.Module): - """ - A convolutional block for graph data - - Args: - in_channels (int): The number of channels in the input to the layer. - filters (int, optional): The number of filters in each convolutional layer (default: 32) - depth (int, optional): The number of successive convolutional layers (default: 2) - conv_type (str, optional): The type of graph convolution to apply (default: "ChebConv", options: "GraphConv", "GCNConv", "GATConv") - conv_kwargs(dict, optional): Dictionary of keyword arguments for the chosen conv_type - activation (str, optional): The activation function applied after each convolution (default: "relu", options: "leakyrelu","gelu","sigmoid","linear") - norm_type (str, optional): The normalization method to apply between convolutions (default:"InstanceNorm", options: "BatchNorm", "LayerNorm") - - Returns: - A `torch.nn.Module` object. - - """ - def __init__(self, - in_channels, - filters, - depth=3, - conv_type='ChebConv', - conv_kwargs={'K':3}, - activation='relu', - norm_type='InstanceNorm'): - super().__init__() - - self.convs=nn.ModuleList( - GraphConvBlock(in_channels=in_channels if i==0 else filters, - filters=filters, - conv_type=conv_type, - conv_kwargs=conv_kwargs, - activation=activation, - norm_type=norm_type) - for i in range(depth) - ) - - - def forward(self, x, edge_index): - x1 = self.convs[0](x,edge_index) - x = self.convs[1](x1,edge_index) - for conv in self.convs[2:]: - x = conv(x,edge_index) - return x+x1 - - -class GraphResDecoderBlock(nn.Module): - """ - A graph convolutional decoder block with the same structure as MeshDeformNet and Image2Flow - - Args: - encoder_channels (List[int]): The number of channels projected from the encoder to each decoder level (len=n_decoder_levels) - out_channels (int): The number of output channels including node coordinates and features - filters (List(List(int)), optional): The number of convolutional filters for each level (default:[[384,288], [144,96], [64,32]]) - res_block_depth (int, optional): The number of successive convolutions in each residual block (default: 3) - n_process_blocks (int, optional): The number of residual blocks prior to projection(default: 1) - n_deform_blocks (int, optional): The number of residual blocks after projection(default: 3) - template_edge_index (torch.Tensor, optional): If template tensor is the fixed it can be passed (default: None) - conv_type (str, optional): The type of graph convolution to apply (default: "ChebConv", options: "GraphConv", "GCNConv", "GATConv") - conv_kwargs(dict, optional): Dictionary of keyword arguments for the chosen conv_type - activation (str, optional): The activation function applied after each convolution (default: "relu", options: "leakyrelu","gelu","sigmoid","linear") - out_activation (str, optional): The activation function applied after each convolution (default: "linear", options: "leakyrelu","gelu","sigmoid","relu","softmax") - norm_type (str, optional): The normalization method to apply between convolutions (default:"InstanceNorm", options: "BatchNorm", "LayerNorm") - - Returns: - A `torch.nn.Module` object. - - """ - def __init__(self, - projection_channels, - graph_channels, - out_channels, - filters, - res_depth = 3, - n_process_blocks = 1, - n_deform_blocks = 3, - template_edge_index=None, - conv_type="ChebConv", - conv_kwargs={'K':3}, - activation="relu", - out_activation="linear", - norm_type="InstanceNorm"): - super().__init__() - - conv_config = dict(depth=res_depth, - conv_type=conv_type, - conv_kwargs=conv_kwargs, - activation=activation, - norm_type=norm_type) - - self.process_conv = nn.ModuleList([ - GraphConvResBlock(in_channels=graph_channels if i==0 else filters[0], - filters=filters[0], - **conv_config) - for i in range(n_process_blocks) - ]) - - - self.deform_conv = nn.ModuleList([ - GraphConvResBlock(in_channels=filters[0] + projection_channels if i==0 else filters[1], - filters=filters[1], - **conv_config) - for i in range(n_deform_blocks) - ]) - - self.out_conv = GraphConvBlock(in_channels=filters[1], - filters=out_channels, - depth=1, - conv_type=conv_type, - conv_kwargs=conv_kwargs, - activation=out_activation, - norm_type=None) - - self.edge_index=template_edge_index - - def forward(self,graph_features,encoder_projection,prev_results,edge_index): - logger.debug("IN GRAPH DECODER") - if edge_index is None: - edge_index=self.edge_index - - x=graph_features.clone() - logger.debug("Process convs...") - for pconv in self.process_conv: x=pconv(x,edge_index) - x = torch.cat([x, encoder_projection], axis=-1) - logger.debug("Decoder convs") - for dconv in self.deform_conv: x=dconv(x, edge_index) - res = self.out_conv(x, edge_index) + prev_results - - return x,res - - -# class RecursiveTopKPooling(nn.Module): - -# def __init__(self, -# n_channels, -# n_levels = 5, -# compression_ratio = 0.5): -# super().__init__() - -# self.pools = nn.ModuleList([ -# gnn.TopKPooling(in_channels=n_channels, ratio=compression_ratio) -# for _ in range(n_levels-1) -# ]) - -# def forward(self, x, edge_index, edge_attr=None, batch=None): -# x_list, edge_index_list, edge_attr_list, batch_list, perm_list, score_list = [x], [edge_index], [edge_attr], [batch], [], [] -# for pool in self.pools: -# x, edge_index, edge_attr, batch, perm, score = pool(x, edge_index, edge_attr, batch) -# x_list.append(x) -# edge_index_list.append(edge_index) -# edge_attr_list.append(edge_attr) -# batch_list.append(batch) -# perm_list.append(perm) -# score_list.append(score) -# perm_list.append(torch.ones(x.shape[0]).to(torch.bool)) -# score_list.append(torch.ones(x.shape[0])) -# return x_list, edge_index_list, edge_attr_list, batch_list, perm_list, score_list - - - -class RecursiveClusterPooling(nn.Module): - - def __init__(self, n_levels = 5): - super().__init__() - self.n_levels = 5 - - def forward(self, graph): - multigraph = [graph.clone()] - for _ in range(self.n_levels-1): - graph = cluster_pool(graph) - multigraph.append(graph.clone()) - return multigraph - - - - -class GraphUNetDecoderBlock(nn.Module): - - def __init__(self, - #in_channels, - out_channels, - filters, - domain_size, - res_depth = 3, - n_align_blocks = 1, - n_deform_blocks = 3, - conv_type="ChebConv", - conv_kwargs={'K':3}, - activation="relu", - out_activation="linear", - norm_type="InstanceNorm", - batched_ops = True): - super().__init__() - - conv_config = dict(depth=res_depth, - conv_type=conv_type, - conv_kwargs=conv_kwargs, - activation=activation, - norm_type=norm_type) - - - if n_align_blocks > 0: - self.align=True - self.align_conv = gnn.Sequential('x, edge_index, batch',[ - (GraphConvResBlock(in_channels=out_channels*2 if i==0 else filters, - filters=filters, - **conv_config), 'x, edge_index -> x') - for i in range(n_align_blocks) - ]) - else: - self.align=False - - - self.deform_conv = gnn.Sequential('x, edge_index, batch',[ - (GraphConvResBlock(in_channels=out_channels+filters if i==0 else filters, - filters=filters, - **conv_config), 'x, edge_index -> x') - for i in range(n_deform_blocks) - ]) - - self.convert_conv = GraphConvBlock(in_channels=filters, - filters=out_channels, - depth=1, - conv_type=conv_type, - conv_kwargs=conv_kwargs, - activation=out_activation, - norm_type=None) - - self.projection_args = {"domain_size":domain_size, "batch_ops":batched_ops} - - # INFO: removed graph features for now may want to add back - def forward(self,image_features,prev_deformation,template_x,edge_index,batch): - - # Move all zero points after unpooling - if self.align: - x = torch.cat([prev_deformation, template_x], axis=-1) - x = self.align_conv(x, edge_index) - x = self.convert_conv(x, edge_index) - prev_deformation = prev_deformation+x - - # apply current deformation to template - x = template_x + prev_deformation - proj = TrilinearProjection(**self.projection_args)(image_features, x[:,:3], batch) - x = torch.cat([x, proj], axis=-1) - - # get new deformations based on current position and projections - x = self.deform_conv(x, edge_index) - x = self.convert_conv(x, edge_index) - return x+prev_deformation - \ No newline at end of file diff --git a/im2sim/layers/image_convs.py b/im2sim/layers/image_convs.py deleted file mode 100644 index 7074ab6..0000000 --- a/im2sim/layers/image_convs.py +++ /dev/null @@ -1,380 +0,0 @@ -import logging -import torch -from torch import nn -from .layer_util import get_image_layer, get_activation - -logger = logging.getLogger(__name__) - -class ImageConvBlock(nn.Module): - """ - A convolutional block for image data - - Args: - in_channels (int): The number of channels in the input to the layer. - filters (int, optional): The number of filters in each convolutional layer (default: 32) - kernel_size (int, optional): The kernel(filter) size for the convolutional layers (default: 3) - depth (int, optional): The number of successive convolutional layers (default: 2) - rank (int, optional): The number of spatial dimensions in the data i.e., 2D, 3D (default:2), - activation (str, optional): The activation function applied after each convolution (default: "relu", options: "leakyrelu","gelu","sigmoid","linear") - norm_type (str, optional): The normalization method to apply between convolutions (default:None, options: "BatchNorm", "InstanceNorm", "LayerNorm") - dropout_rate (float, optional): The spatial dropout rate to be applied to the final convolution output (default:None) - - Returns: - A `torch.nn.Module` object. - - """ - def __init__(self, - in_channels, - filters=32, - kernel_size=3, - depth=1, - rank=3, - activation='relu', - norm_type=None, - dropout_rate=None): - super().__init__() - - conv = get_image_layer('Conv', rank) - self.convs = nn.ModuleList([ - conv(in_channels if i==0 else filters, filters, kernel_size, padding=kernel_size//2) - for i in range(depth) - ]) - - self.norms = nn.ModuleList([ - get_image_layer(norm_type, rank)(filters) if norm_type else nn.Identity() - for _ in range(depth) - ]) - self.drop = nn.Dropout1d(p=dropout_rate) if dropout_rate else nn.Identity() - - self.act = get_activation(activation)(inplace=True) if activation.lower() == 'relu' else get_activation(activation)() - - - def forward(self, x): - """ - Args: - x (torch.Tensor): Input feature maps in image space [in_channels, ...] where the number of dims in ... corresponds to rank - - Returns: - torch.Tensor: Output feature maps [out_channels, ...] - """ - - for conv, norm in zip(self.convs, self.norms): - logger.debug("Image feature shape:%s", tuple(x.shape)) - x = self.act(norm(conv(x))) - return self.drop(x) - -class ImageConvResBlock(nn.Module): - """ - A convolutional residual block for image data - - Args: - in_channels (int): The number of channels in the input to the layer. - filters (int, optional): The number of filters in each convolutional layer (default: 32) - kernel_size (int, optional): The kernel(filter) size for the convolutional layers (default: 3) - depth (int, optional): The number of successive convolutional layers (default: 3) - rank (int, optional): The number of spatial dimensions in the data i.e., 2D, 3D (default:2), - activation (str, optional): The activation function applied after each convolution (default: "relu", options: "leakyrelu","gelu","sigmoid","linear") - norm_type (str, optional): The normalization method to apply between convolutions (default:None, options: "BatchNorm", "InstanceNorm", "LayerNorm") - dropout_rate (float, optional): The spatial dropout rate to be applied to the convolution prior to residual connection (default:None) - - Returns: - A `torch.nn.Module` object. - - """ - def __init__(self, - in_channels, - filters=32, - kernel_size=3, - depth=3, - rank=3, - activation='relu', - norm_type=None, - dropout_rate=None): - super().__init__() - - conv_params = dict(filters=filters, - kernel_size=kernel_size, - rank=rank, - activation=activation, - norm_type=norm_type) - self.initial_conv = ImageConvBlock(in_channels=in_channels, - **conv_params, - depth=1, - dropout_rate=None) - self.main_conv = ImageConvBlock(in_channels=filters, - **conv_params, - depth=depth-2, - dropout_rate=None) - self.out_conv = ImageConvBlock(in_channels=filters, - **conv_params, - depth=1, - dropout_rate=dropout_rate) - - def forward(self, x): - """ - Args: - x (torch.Tensor): Input feature maps in image space [in_channels, ...] where the number of dims in ... corresponds to rank - - Returns: - torch.Tensor: Output feature maps [out_channels, ...] - """ - x1 = self.initial_conv(x) - x = self.main_conv(x1) - x = self.out_conv(x) + x1 - return x - -class ImageResEncoder(nn.Module): - """ - A CNN encoder for images. Structured like the encoder of a ResUNet. - - Args: - in_channels (int): The number of channels in the input image. - filters (List[int], optional): The number of convolutional filters in each encoder level (default: [16,32,64,128,256]) - kernel_size (int, optional): The kernel(filter) size for the convolutional layers (default: 3) - res_depth (int, optional): The number of successive convolutional layers in each residual block (default: 3) - res_blocks_per_level (int, optional): The number of successive residual blocks per encoder level (default: 2) - rank (int, optional): The number of spatial dimensions in the data i.e., 2D, 3D (default:3), - activation (str, optional): The activation function applied after each convolution (default: "relu", options: "leakyrelu","gelu","sigmoid","linear") - norm_type (str, optional): The normalization method to apply between convolutions (default:None, options: "BatchNorm", "InstanceNorm", "LayerNorm") - dropout_rate (float, optional): The spatial dropout rate to be applied to each residual block prior to residual connection (default:None) - - Returns: - A `torch.nn.Module` object. - """ - - def __init__(self, - in_channels, - filters=[16,32,64,128,256], - kernel_size=3, - res_depth=3, - res_blocks_per_level=2, - rank=3, - norm_type=None, - pool_type='MaxPool', - pool_size=2, - activation='relu', - dropout_rate=None): - super().__init__() - - n_levels = len(filters) - in_channels = [in_channels, *filters] - self.conv_blocks = nn.ModuleList([ - nn.ModuleList([ - ImageConvResBlock(in_channels=in_channels[i] if j==0 else in_channels[i+1], - filters=filters[i], - kernel_size=kernel_size, - depth=res_depth, - rank=rank, - activation=activation, - norm_type=norm_type, - dropout_rate=dropout_rate) - for j in range(res_blocks_per_level) - ]) - for i in range(n_levels) - ]) - pool = get_image_layer(pool_type, rank) - self.maxpools = nn.ModuleList([ - pool(pool_size) if i>0 else nn.Identity() - for i in range(n_levels) - ]) - - def forward(self,x): - """ - Args: - x (torch.Tensor): Input image [in_channels, ...] - - Returns: - List[torch.Tensor]: Output feature maps from each level ordered from top to bottom [Tensor([filters[0], ...], ..., Tensor([filters[N], ...]) - """ - logger.debug("IN ENCODER") - outputs = [] - for pool, convs in zip(self.maxpools, self.conv_blocks): - x = pool(x) - for conv in convs: - x = conv(x) - logger.debug("Conv output shape:%s", x.shape) - outputs.append(x) - return outputs - - -class ImageEncoder(nn.Module): - """ - A CNN encoder for images. Structured like the encoder of a UNet. - - Args: - in_channels (int): The number of channels in the input image. - filters (List[int], optional): The number of convolutional filters in each encoder level (default: [16,32,64,128,256]) - kernel_size (int, optional): The kernel(filter) size for the convolutional layers (default: 3) - conv_blocks_per_level (int, optional): The number of successive convolutional blocks per encoder level (default: 1) - rank (int, optional): The number of spatial dimensions in the data i.e., 2D, 3D (default:3), - activation (str, optional): The activation function applied after each convolution (default: "relu", options: "leakyrelu","gelu","sigmoid","linear") - norm_type (str, optional): The normalization method to apply between convolutions (default:None, options: "BatchNorm", "InstanceNorm", "LayerNorm") - dropout_rate (float, optional): The spatial dropout rate to be applied to each residual block prior to residual connection (default:None) - - Returns: - A `torch.nn.Module` object. - """ - - def __init__(self, - in_channels, - filters=[16,32,64,128,256], - kernel_size=3, - conv_blocks_per_level=1, - rank=3, - norm_type=None, - pool_type='MaxPool', - pool_size=2, - activation='relu', - dropout_rate=None): - super().__init__() - - n_levels = len(filters) - self.conv_blocks = nn.ModuleList([ - ImageConvBlock(in_channels=in_channels if i==0 else filters[i-1], - filters=filters[i], - kernel_size=kernel_size, - depth=conv_blocks_per_level, - rank=rank, - activation=activation, - norm_type=norm_type, - dropout_rate=dropout_rate) - for i in range(n_levels) - ]) - pool = get_image_layer(pool_type, rank) - self.maxpools = nn.ModuleList([ - pool(pool_size) if i>0 else nn.Identity() - for i in range(n_levels) - ]) - - def forward(self,x): - """ - Args: - x (torch.Tensor): Input image [in_channels, ...] - - Returns: - List[torch.Tensor]: Output feature maps from each level ordered from top to bottom [Tensor([filters[0], ...], ..., Tensor([filters[N], ...]) - """ - outputs = [] - for pool, conv in zip(self.maxpools, self.conv_blocks): - x = conv(pool(x)) - outputs.append(x) - return outputs - - - - - -class ImageDecoder(nn.Module): - """ - CNN decoder for images. Mirrors ImageEncoder like a UNet decoder. - - Args: - filters (List[int]): Encoder filter sizes in top→bottom order. - kernel_size (int): Convolution kernel size. - conv_blocks_per_level (int): Number of conv blocks per level. - rank (int): Spatial rank (2 or 3). - upsample_type (str): "ConvTranspose" or "Upsample". - activation (str): Activation name. - norm_type (str): Normalization type. - dropout_rate (float): Dropout rate. - skip (bool): Use skip connections. - """ - - def __init__(self, - filters=[16,32,64,128,256], - kernel_size=3, - conv_blocks_per_level=1, - rank=3, - upsample_type="Upsample", - activation="relu", - norm_type=None, - dropout_rate=None, - skip=True): - super().__init__() - - self.skip = skip - n_levels = len(filters) - - rev_filters = filters[::-1] - - if upsample_type.lower() == 'upsample': - # if rank == 4: - # self.ups = nn.ModuleList([ - # Upsample4d(scale_factor=(1, 2, 2, 2)) - # for _ in range(n_levels - 1) - # ]) - - # else: - self.ups = nn.ModuleList([ - nn.Upsample(scale_factor=2, mode='trilinear' if rank==3 else 'bilinear', align_corners=True) - for _ in range(n_levels - 1) - ]) - else: - up_layer = get_image_layer(upsample_type, rank) - self.ups = nn.ModuleList([ - up_layer(rev_filters[i], rev_filters[i+1], kernel_size=2, stride=2) - for i in range(n_levels - 1) - ]) - - - self.conv_blocks = nn.ModuleList([ - ImageConvBlock( - in_channels=( - rev_filters[i] + rev_filters[i+1] - if skip else rev_filters[i] - ), - filters=rev_filters[i+1], - kernel_size=kernel_size, - depth=conv_blocks_per_level, - rank=rank, - activation=activation, - norm_type=norm_type, - dropout_rate=dropout_rate - ) - for i in range(n_levels - 1) - ]) - - - def _match_size(self, x, skip): - if x.shape[2:] != skip.shape[2:]: - # center crop skip to x - diff = [s - t for s, t in zip(skip.shape[2:], x.shape[2:])] - slices = [slice(d//2, d//2 + t) for d, t in zip(diff, x.shape[2:])] - skip = skip[(..., *slices)] - return skip - - - def forward(self, encoder_outputs): - """ - Args: - encoder_outputs: List of tensors from encoder (top→bottom). - - Returns: - Decoded tensor at highest resolution. - """ - - # Reverse the encoder outputs so we traverse from bottleneck to top - rev_enc = encoder_outputs[::-1] - - # Start from bottleneck - x = rev_enc[0] - - # Traverse decoder levels - for i, (up, conv) in enumerate(zip(self.ups, self.conv_blocks)): - - x = up(x) - - if self.skip: - skip_feat = rev_enc[i + 1] # next encoder feature - skip_feat = self._match_size(x, skip_feat) - x = torch.cat([x, skip_feat], dim=1) - - x = conv(x) - - return x - - - - - - \ No newline at end of file diff --git a/im2sim/layers/layer_util.py b/im2sim/layers/layer_util.py deleted file mode 100644 index e2d3713..0000000 --- a/im2sim/layers/layer_util.py +++ /dev/null @@ -1,603 +0,0 @@ -from torch import nn -import torch.nn.functional as F -import torch_geometric.nn as gnn -import math - - -def get_image_layer(name, rank): - """Get an N-D layer object. - - Args: - name: A `str`. The name of the requested layer. - rank: An `int`. The rank of the requested layer. - - Returns: - A `torch.nn.Module` object. - - Raises: - ValueError: If the requested layer is unknown. - """ - try: - return _IMAGE_LAYERS[(name, rank)] - except KeyError as err: - raise ValueError( - f"Could not find a layer with name '{name}' and rank {rank}.") from err - - -def get_graph_layer(name): - """Get an graph layer object. - - Args: - name: A `str`. The name of the requested layer. - - Returns: - A `torch.nn.Module` object. - - Raises: - ValueError: If the requested layer is unknown. - """ - try: - return _GRAPH_LAYERS[name] - except KeyError as err: - raise ValueError( - f"Could not find an activation with name '{name}'") from err - -def get_default_kwargs(name): - """Get an graph layer object. - - Args: - name: A `str`. The name of the requested layer. - - Returns: - A `torch.nn.Module` object. - - Raises: - ValueError: If the requested layer is unknown. - """ - try: - return _LAYER_KWARGS[name] - except KeyError as err: - raise ValueError( - f"Could not find an activation with name '{name}'") from err - - -def get_activation(name): - """Get an activation object. - - Args: - name: A `str`. The name of the requested layer. - - Returns: - A `torch.nn.Module` object. - - Raises: - ValueError: If the requested activation is unknown. - """ - try: - return _ACTIVATIONS[name] - except KeyError as err: - raise ValueError( - f"Could not find an activation with name '{name}'") from err - - -_IMAGE_LAYERS = { - ('AveragePooling', 1): nn.AvgPool1d, - ('AveragePooling', 2): nn.AvgPool2d, - ('AveragePooling', 3): nn.AvgPool3d, - ('Conv', 1): nn.Conv1d, - ('Conv', 2): nn.Conv2d, - ('Conv', 3): nn.Conv3d, - ('ConvTranspose', 1): nn.ConvTranspose1d, - ('ConvTranspose', 2): nn.ConvTranspose2d, - ('ConvTranspose', 3): nn.ConvTranspose3d, - ('MaxPool', 1): nn.MaxPool1d, - ('MaxPool', 2): nn.MaxPool2d, - ('MaxPool', 3): nn.MaxPool3d, - ('Dropout', 1): nn.Dropout1d, - ('Dropout', 2): nn.Dropout2d, - ('Dropout', 3): nn.Dropout3d, - ('ZeroPadding', 1): nn.ZeroPad1d, - ('ZeroPadding', 2): nn.ZeroPad2d, - ('ZeroPadding', 3): nn.ZeroPad3d, - ('BatchNorm', 1): nn.BatchNorm1d, - ('BatchNorm', 2): nn.BatchNorm2d, - ('BatchNorm', 3): nn.BatchNorm3d, - ('InstanceNorm', 1): nn.InstanceNorm1d, - ('InstanceNorm', 2): nn.InstanceNorm2d, - ('InstanceNorm', 3): nn.InstanceNorm3d -} - -_GRAPH_LAYERS = { - 'ChebConv': gnn.ChebConv, - 'GraphConv': gnn.GraphConv, - 'GCNConv': gnn.GCNConv, - 'GATConv': gnn.GATConv, - 'InstanceNorm': gnn.InstanceNorm, - 'BatchNorm': gnn.BatchNorm, - 'GraphNorm': gnn.GraphNorm, -} - -_LAYER_KWARGS = { - 'ChebConv': {'K':3}, - 'GraphConv': {}, - 'GCNConv': {}, - 'GATConv': {} -} -_ACTIVATIONS = { - "relu": nn.ReLU, - "leaky_relu": nn.LeakyReLU, - "gelu": nn.GELU, - "sigmoid": nn.Sigmoid, - "linear": nn.Identity, - "softmax": nn.Softmax -} - - -def init_weights(m): - if isinstance(m, gnn.ChebConv): - for lin in m.lins: - nn.init.kaiming_normal_(lin.weight, nonlinearity='leaky_relu') - lin.weight.data *= 0.1 - if lin.bias is not None: - nn.init.zeros_(lin.bias) - -def standardize_spatial_factors(factors, rank): - """ - Convert a sequence of spatial factors into a standardized list of tuples. - """ - standardized = [] - - for f in factors: - if isinstance(f, int): - standardized.append(tuple([f] * rank)) - elif isinstance(f, (tuple, list)): - standardized.append(tuple(f)) - else: - raise TypeError( - f"Each factor must be an int, tuple, or list, got {type(f).__name__}" - ) - - return standardized - -def _expand_to_4d(param, name="parameter"): - """ - Convert an int or 4-tuple/list into a 4-tuple: (T, D, H, W) - """ - if isinstance(param, int): - return (param, param, param, param) - if isinstance(param, (tuple, list)): - if len(param) != 4: - raise ValueError(f"{name} must have 4 elements (T, D, H, W), got {param}") - return tuple(param) - raise TypeError(f"{name} must be an int or a tuple/list of length 4") - - -def _same_padding_4d(kernel_size): - """ - Compute 'same-like' padding for odd kernel sizes. - """ - k = _expand_to_4d(kernel_size, "kernel_size") - return tuple(kk // 2 for kk in k) - - -def _get_spatial_op(rank, op2d, op3d, name="spatial op"): - if rank == 2: - return op2d - if rank == 3: - return op3d - raise ValueError(f"{name} only supports rank=2 or rank=3, got {rank}") - - -def _default_spatial_mode(rank): - return "bilinear" if rank == 2 else "trilinear" - -def _same_padding_time(kernel_size, rank): - """ - Return same-style padding for (T + spatial) kernel sizes. - - rank=2 -> expects int or (T, H, W) - rank=3 -> expects int or (T, D, H, W) - """ - if isinstance(kernel_size, int): - k = (kernel_size,) * (rank + 1) - elif isinstance(kernel_size, (tuple, list)): - if len(kernel_size) != rank + 1: - raise ValueError( - f"kernel_size must have length {rank + 1} for rank={rank}, got {kernel_size}" - ) - k = tuple(kernel_size) - else: - raise TypeError("kernel_size must be an int or tuple/list") - - return tuple(kk // 2 for kk in k) - - -class TimeDistributed(nn.Module): - """ - Apply a module independently to each time step. - - Expected input: - 2D+1: (N, C, T, H, W) - 3D+1: (N, C, T, D, H, W) - - Wrapped module should accept: - 2D+1: (N, C, H, W) - 3D+1: (N, C, D, H, W) - """ - def __init__(self, module): - super().__init__() - self.module = module - - def forward(self, x): - if x.ndim not in (5, 6): - raise ValueError( - f"TimeDistributed expects 5D or 6D input [N, C, T, ...], got shape {tuple(x.shape)}" - ) - - n, c, t = x.shape[:3] - spatial = x.shape[3:] - - # (N, C, T, *S) -> (N, T, C, *S) - permute_order = [0, 2, 1] + list(range(3, x.ndim)) - x = x.permute(*permute_order).contiguous() - - # (N, T, C, *S) -> (N*T, C, *S) - x = x.reshape(n * t, c, *spatial) - - y = self.module(x) # (N*T, C_out, *S_out) - - c_out = y.shape[1] - spatial_out = y.shape[2:] - - # (N*T, C_out, *S_out) -> (N, T, C_out, *S_out) - y = y.reshape(n, t, c_out, *spatial_out) - - # (N, T, C_out, *S_out) -> (N, C_out, T, *S_out) - permute_back = [0, 2, 1] + list(range(3, y.ndim)) - y = y.permute(*permute_back).contiguous() - - return y - - -class SpaceDistributed(nn.Module): - """ - Apply a module independently at each spatial location over time. - - Expected input: - 2D+1: (N, C, T, H, W) - 3D+1: (N, C, T, D, H, W) - - Wrapped module should accept: - (N_flat, C, T) - """ - def __init__(self, module): - super().__init__() - self.module = module - - def forward(self, x): - if x.ndim not in (5, 6): - raise ValueError( - f"SpaceDistributed expects 5D or 6D input [N, C, T, ...], got shape {tuple(x.shape)}" - ) - - n, c, t = x.shape[:3] - spatial = x.shape[3:] - spatial_rank = len(spatial) - spatial_prod = math.prod(spatial) - - # (N, C, T, *S) -> (N, *S, C, T) - permute_order = [0] + list(range(3, x.ndim)) + [1, 2] - x = x.permute(*permute_order).contiguous() - - # (N, *S, C, T) -> (N*prod(S), C, T) - x = x.reshape(n * spatial_prod, c, t) - - y = self.module(x) # (N*prod(S), C_out, T_out) - - c_out = y.shape[1] - t_out = y.shape[2] - - # (N*prod(S), C_out, T_out) -> (N, *S, C_out, T_out) - y = y.reshape(n, *spatial, c_out, t_out) - - # (N, *S, C_out, T_out) -> (N, C_out, T_out, *S) - permute_back = [0, spatial_rank + 1, spatial_rank + 2] + list(range(1, spatial_rank + 1)) - y = y.permute(*permute_back).contiguous() - - return y - - -class ConvTime(nn.Module): - def __init__( - self, - in_channels, - out_channels, - kernel_size, - stride=1, - padding=0, - dilation=1, - groups=1, - bias=True, - padding_mode="zeros", - device=None, - dtype=None, - rank=3, - ): - super().__init__() - self.rank = rank - - self.time_kernel, self.space_kernel = self._split_param(kernel_size, "kernel_size") - self.time_stride, self.space_stride = self._split_param(stride, "stride") - self.time_padding, self.space_padding = self._split_param(padding, "padding") - self.time_dilation, self.space_dilation = self._split_param(dilation, "dilation") - - spatial_conv = _get_spatial_op(rank, nn.Conv2d, nn.Conv3d, "ConvTime spatial conv") - - self.conv_spatial = TimeDistributed( - spatial_conv( - in_channels, - out_channels, - kernel_size=self.space_kernel, - stride=self.space_stride, - padding=self.space_padding, - dilation=self.space_dilation, - groups=groups, - bias=bias, - padding_mode=padding_mode, - device=device, - dtype=dtype, - ) - ) - - self.conv_time = SpaceDistributed( - nn.Conv1d( - out_channels, - out_channels, - kernel_size=self.time_kernel, - stride=self.time_stride, - padding=self.time_padding, - dilation=self.time_dilation, - groups=1, - bias=bias, - device=device, - dtype=dtype, - ) - ) - - def _split_param(self, param, name="parameter"): - if isinstance(param, (tuple, list)): - if len(param) != self.rank + 1: - raise ValueError(f"{name} must have {self.rank + 1} elements (T + spatial dims)") - return param[0], tuple(param[1:]) - return param, (param,) * self.rank - - def forward(self, x): - x = self.conv_spatial(x) - x = self.conv_time(x) - return x - - -class UpsampleTime(nn.Module): - """ - Separable (rank)+1D upsampling: - - spatial upsampling per time step - - temporal upsampling per spatial location - - rank=2: - input (B, C, T, H, W) - output (B, C, T_out, H_out, W_out) - - rank=3: - input (B, C, T, D, H, W) - output (B, C, T_out, D_out, H_out, W_out) - """ - def __init__(self, scale_factor=None, size=None, mode=None, align_corners=False, rank=3): - super().__init__() - self.rank = rank - - if (scale_factor is None) == (size is None): - raise ValueError("Provide exactly one of scale_factor or size") - - if mode is None: - mode = _default_spatial_mode(rank) - elif rank == 2 and mode == "trilinear": - mode = "bilinear" - - self.use_scale_factor = scale_factor is not None - - if self.use_scale_factor: - self.time_value, self.space_value = self._split_param(scale_factor, "scale_factor") - else: - self.time_value, self.space_value = self._split_param(size, "size") - - if self.use_scale_factor: - self.up_spatial = TimeDistributed( - nn.Upsample( - scale_factor=self.space_value, - mode=mode, - align_corners=align_corners if "linear" in mode else None, - ) - ) - self.up_time = SpaceDistributed( - nn.Upsample( - scale_factor=self.time_value, - mode="linear", - align_corners=align_corners, - ) - ) - else: - self.up_spatial = TimeDistributed( - nn.Upsample( - size=self.space_value, - mode=mode, - align_corners=align_corners if "linear" in mode else None, - ) - ) - self.up_time = SpaceDistributed( - nn.Upsample( - size=self.time_value, - mode="linear", - align_corners=align_corners, - ) - ) - - def _split_param(self, param, name): - if isinstance(param, (tuple, list)): - if len(param) != self.rank + 1: - raise ValueError(f"{name} must have {self.rank + 1} elements (T + spatial dims)") - return param[0], tuple(param[1:]) - return param, (param,) * self.rank - - def forward(self, x): - x = self.up_spatial(x) - x = self.up_time(x) - return x - - -class ConvTransTime(nn.Module): - """ - Separable (rank)+1D transposed convolution: - 1) spatial ConvTransposeNd applied independently per time step - 2) temporal ConvTranspose1d applied independently per spatial location - """ - def __init__( - self, - in_channels, - out_channels, - kernel_size, - stride=1, - padding=0, - output_padding=0, - bias=True, - dilation=1, - rank=3, - ): - super().__init__() - self.rank = rank - - self.time_kernel, self.space_kernel = self._split_param(kernel_size, "kernel_size") - self.time_stride, self.space_stride = self._split_param(stride, "stride") - self.time_padding, self.space_padding = self._split_param(padding, "padding") - self.time_outpad, self.space_outpad = self._split_param(output_padding, "output_padding") - self.time_dilation, self.space_dilation = self._split_param(dilation, "dilation") - - spatial_conv_trans = _get_spatial_op( - rank, nn.ConvTranspose2d, nn.ConvTranspose3d, "ConvTransTime spatial conv" - ) - - self.up_spatial = TimeDistributed( - spatial_conv_trans( - in_channels=in_channels, - out_channels=out_channels, - kernel_size=self.space_kernel, - stride=self.space_stride, - padding=self.space_padding, - output_padding=self.space_outpad, - dilation=self.space_dilation, - bias=bias, - ) - ) - - self.up_time = SpaceDistributed( - nn.ConvTranspose1d( - in_channels=out_channels, - out_channels=out_channels, - kernel_size=self.time_kernel, - stride=self.time_stride, - padding=self.time_padding, - output_padding=self.time_outpad, - dilation=self.time_dilation, - bias=bias, - ) - ) - - def _split_param(self, param, name="parameter"): - if isinstance(param, (tuple, list)): - if len(param) != self.rank + 1: - raise ValueError(f"{name} must have {self.rank + 1} elements (T + spatial dims)") - return param[0], tuple(param[1:]) - return param, (param,) * self.rank - - def forward(self, x): - x = self.up_spatial(x) - x = self.up_time(x) - return x - - -class MaxPoolTime(nn.Module): - def __init__(self, kernel_size, stride=None, padding=0, dilation=1, ceil_mode=False, rank=3): - super().__init__() - self.rank = rank - - stride = kernel_size if stride is None else stride - - self.time_kernel, self.space_kernel = self._split_param(kernel_size, "kernel_size") - self.time_stride, self.space_stride = self._split_param(stride, "stride") - self.time_padding, self.space_padding = self._split_param(padding, "padding") - self.time_dilation, self.space_dilation = self._split_param(dilation, "dilation") - - spatial_pool = _get_spatial_op(rank, nn.MaxPool2d, nn.MaxPool3d, "MaxPoolTime spatial pool") - - self.pool_spatial = TimeDistributed( - spatial_pool( - kernel_size=self.space_kernel, - stride=self.space_stride, - padding=self.space_padding, - dilation=self.space_dilation, - ceil_mode=ceil_mode, - ) - ) - - self.pool_time = SpaceDistributed( - nn.MaxPool1d( - kernel_size=self.time_kernel, - stride=self.time_stride, - padding=self.time_padding, - dilation=self.time_dilation, - ceil_mode=ceil_mode, - ) - ) - - def _split_param(self, param, name="parameter"): - if isinstance(param, (tuple, list)): - if len(param) != self.rank + 1: - raise ValueError(f"{name} must have {self.rank + 1} elements (T + spatial dims)") - return param[0], tuple(param[1:]) - return param, (param,) * self.rank - - def forward(self, x): - x = self.pool_spatial(x) - x = self.pool_time(x) - return x - - -class BatchNormTime(nn.Module): - def __init__(self, num_features, eps=1e-5, momentum=0.1, affine=True, track_running_stats=True, rank=3): - super().__init__() - - spatial_bn = _get_spatial_op(rank, nn.BatchNorm2d, nn.BatchNorm3d, "BatchNormTime spatial norm") - - self.bn_spatial = TimeDistributed( - spatial_bn( - num_features, - eps=eps, - momentum=momentum, - affine=affine, - track_running_stats=track_running_stats, - ) - ) - - self.bn_time = SpaceDistributed( - nn.BatchNorm1d( - num_features, - eps=eps, - momentum=momentum, - affine=affine, - track_running_stats=track_running_stats, - ) - ) - - def forward(self, x): - x = self.bn_spatial(x) - x = self.bn_time(x) - return x \ No newline at end of file diff --git a/im2sim/layers/projections.py b/im2sim/layers/projections.py deleted file mode 100644 index 315cb29..0000000 --- a/im2sim/layers/projections.py +++ /dev/null @@ -1,61 +0,0 @@ -import logging - -import torch -import torch.nn.functional as F -from torch import nn -from ..data.mesh_utils import make_padded_batch - -logger = logging.getLogger(__name__) - -class TrilinearProjection(nn.Module): - - def __init__(self, domain_size, batch_ops=True): - super().__init__() - self.domain_size = domain_size - self.batch_ops = batch_ops - - def forward(self, encoder_outputs, graph_coords, batch): - logger.debug("IN PROJECTION") - if logger.isEnabledFor(logging.DEBUG): - logger.debug("Domain size: %s", tuple(self.domain_size)) - logger.debug("Encoder outputs - shape:%s, max:%.2f, min:%.2f", tuple(encoder_outputs.shape), encoder_outputs.max(), encoder_outputs.min()) - logger.debug("Graph coords - shape:%s, max:%.2f, min:%.2f", tuple(graph_coords.shape), graph_coords.max(), graph_coords.min()) - logger.debug("Batch - shape:%s, vals:%s", tuple(batch.shape), tuple(torch.unique(batch))) - if self.batch_ops: - # make a padded batched tensor and get the padding mask - # do the projection - # extract the graph features using the padding mask - if logger.isEnabledFor(logging.DEBUG): - logger.debug("batch: %s", tuple(torch.unique(batch))) - padded_coords, padding_mask = make_padded_batch(graph_coords, batch) - logger.debug("padded_shape: %s, mask_shape:%s", - tuple(padded_coords.shape), tuple(padding_mask.shape)) - grid = torch.stack([(2*padded_coords[...,j]/(d-1)) - 1 - for j,d in enumerate(self.domain_size)], axis=-1) # normalise coords [-1,1] and divide by scale - grid = grid.unsqueeze(-2).unsqueeze(-2) # [B,N,3]->[B,N,1,1,3]] - if logger.isEnabledFor(logging.DEBUG): - logger.debug("grid_shape: %s, grid_vals: %.2f-%.2f, encoder_outputs_shape:%s",tuple(grid.shape), grid.max(), grid.min(), tuple(encoder_outputs.shape)) - grid = grid.type_as(encoder_outputs) - projections = F.grid_sample(encoder_outputs,grid, align_corners=True).squeeze(-1).squeeze(-1).permute(0,2,1) # [B,C,N,1,1] -> [B,N,C] - logger.debug("projection_shape:%s",tuple(projections.shape)) - projections = projections[padding_mask] - logger.debug("masked_projection_shape:%s",tuple(projections.shape)) - - else: - # loop through the batches and concatenate the projections - projections = [] - for i in torch.unique(batch).to(torch.int16): - coords = graph_coords[batch==i] - logger.debug('coords shape: %s', tuple(coords.shape)) - grid = torch.stack([(2*coords[:,j]/(d-1)) - 1 - for j,d in enumerate(self.domain_size)], axis=-1) # normalise coords [-1,1] and divide by scale - grid = grid.unsqueeze(0).unsqueeze(-2).unsqueeze(-2) # [N,3]->[1,N,1,1,3] - if logger.isEnabledFor(logging.DEBUG): - # logger.debug("i: %s, %s",type(i), i.dtype) - logger.debug("grid_shape: %s, grid_vals: %.2f-%.2f, encoder_outputs_shape:%s",tuple(grid.shape), grid.max(), grid.min(), tuple(encoder_outputs[i].shape)) - grid = grid.type_as(encoder_outputs) - projections.append(F.grid_sample(encoder_outputs[i].unsqueeze(0),grid, align_corners=True).squeeze().permute(1,0)) # [1,C,N,1,1] -> [1,C,N] -> [N,C] - logger.debug("projection %d shape:%s",i,tuple(projections[-1].shape)) - projections = torch.cat(projections, dim=0) - logger.debug("final projection shape:%s",tuple(projections.shape)) - return projections diff --git a/im2sim/losses/__init__.py b/im2sim/losses/__init__.py deleted file mode 100644 index ce55bd3..0000000 --- a/im2sim/losses/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import pointcloud, mesh, feature \ No newline at end of file diff --git a/im2sim/losses/feature.py b/im2sim/losses/feature.py deleted file mode 100644 index 9e20b63..0000000 --- a/im2sim/losses/feature.py +++ /dev/null @@ -1,11 +0,0 @@ - -import logging - -import torch - -logger = logging.getLogger(__name__) - -def mse(gr1, gr2): - return torch.mean((gr1.x[...,3:] - gr2.x[...,3:])**2) - - diff --git a/im2sim/losses/mesh.py b/im2sim/losses/mesh.py deleted file mode 100644 index a6a2c6b..0000000 --- a/im2sim/losses/mesh.py +++ /dev/null @@ -1,25 +0,0 @@ -from itertools import combinations -import logging -from ..data.mesh_utils import _compute_edge_lengths - -import torch - -logger = logging.getLogger(__name__) - -def edge_length_deviation_loss(gr1, gr2): - return _edge_length_deviation(gr2.x[:,:3], gr2.edge_index) - - -def _edge_length_deviation(points, edges): - lengths = _compute_edge_lengths(points, edges) - std_dev = lengths.sum(-1).std() - return std_dev - - - -def _aspect_ratio(tet_vertices): - vert_ids = list(combinations(range(4),2)) - edge_coords = tet_vertices[...,vert_ids,:] - distances = torch.linalg.norm(edge_coords[...,0,:]-edge_coords[...,1,:], dim=-1) - aspect_ratio = distances.max(-1).values/distances.mean(-1) - return aspect_ratio.mean() diff --git a/im2sim/losses/utils.py b/im2sim/losses/utils.py deleted file mode 100644 index cec0710..0000000 --- a/im2sim/losses/utils.py +++ /dev/null @@ -1,17 +0,0 @@ -from torch import nn -import inspect - -class GraphLoss(nn.Module): - - def __init__(self, loss_fn, kwargs): - self.loss_fn = loss_fn - self.params = inspect.signature(loss_fn).parameters - self.kwargs = kwargs - - - def forward(self,true_graph, pred_graph): - gr_dict = {'true':true_graph, 'pred':pred_graph} - call_args = {key: getattr(gr_dict[key.split('_')[0]], key.split[1]) # key is in format _ - for key in self.params} - loss = self.loss_fn(**call_args, **self.kwargs) - return loss \ No newline at end of file diff --git a/im2sim/metrics/__init__.py b/im2sim/metrics/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/im2sim/metrics/contents.md b/im2sim/metrics/contents.md deleted file mode 100644 index 262efab..0000000 --- a/im2sim/metrics/contents.md +++ /dev/null @@ -1,3 +0,0 @@ -1. mask metrics -2. mesh metrics -3. feature metrics diff --git a/im2sim/models/UNet.py b/im2sim/models/UNet.py deleted file mode 100644 index ceef47f..0000000 --- a/im2sim/models/UNet.py +++ /dev/null @@ -1,469 +0,0 @@ -import torch -from torch import nn -import torch.nn.functional as F -from ..layers.layer_util import get_image_layer, get_activation, standardize_spatial_factors - -class ImageConvBlock(nn.Module): - """ - A convolutional block for image data - - Args: - in_channels (int): The number of channels in the input to the layer. - filters (int, optional): The number of filters in each convolutional layer (default: 32) - kernel_size (int, optional): The kernel(filter) size for the convolutional layers (default: 3) - depth (int, optional): The number of successive convolutional layers (default: 2) - rank (int, optional): The number of spatial dimensions in the data i.e., 2D, 3D (default:2), - activation (str, optional): The activation function applied after each convolution (default: "relu", options: "leakyrelu","gelu","sigmoid","linear") - norm_type (str, optional): The normalization method to apply between convolutions (default:None, options: "BatchNorm", "InstanceNorm", "LayerNorm") - dropout_rate (float, optional): The spatial dropout rate to be applied to the final convolution output (default:None) - - Returns: - A `torch.nn.Module` object. - - """ - def __init__(self, - in_channels, - filters=32, - kernel_size=3, - depth=1, - rank=3, - activation='relu', - norm_type=None, - dropout_rate=None): - super().__init__() - - conv = get_image_layer('Conv', rank) - drop = get_image_layer('Dropout', rank) - self.convs = nn.ModuleList([ - conv(in_channels if i==0 else filters, filters, kernel_size, padding=kernel_size//2) - for i in range(depth) - ]) - - self.norms = nn.ModuleList([ - get_image_layer(norm_type, rank)(filters) if norm_type else nn.Identity() - for _ in range(depth) - ]) - self.drop = drop(p=dropout_rate) if dropout_rate else nn.Identity() - - self.act = get_activation(activation)(inplace=True) if activation.lower() == 'relu' else get_activation(activation)() - - - def forward(self, x): - """ - Args: - x (torch.Tensor): Input feature maps in image space [in_channels, ...] where the number of dims in ... corresponds to rank - - Returns: - torch.Tensor: Output feature maps [out_channels, ...] - """ - - for conv, norm in zip(self.convs, self.norms): - x = norm(self.act(conv(x))) - return self.drop(x) - - -class ImageEncoder(nn.Module): - """ - A CNN encoder for images. Structured like the encoder of a UNet. - - Args: - in_channels (int): The number of channels in the input image. - filters (List[int], optional): The number of convolutional filters in each encoder level (default: [16,32,64,128,256]) - kernel_size (int, optional): The kernel(filter) size for the convolutional layers (default: 3) - conv_blocks_per_level (int, optional): The number of successive convolutional blocks per encoder level (default: 1) - rank (int, optional): The number of spatial dimensions in the data i.e., 2D, 3D (default:3), - activation (str, optional): The activation function applied after each convolution (default: "relu", options: "leakyrelu","gelu","sigmoid","linear") - norm_type (str, optional): The normalization method to apply between convolutions (default:None, options: "BatchNorm", "InstanceNorm", "LayerNorm") - dropout_rate (float, optional): The spatial dropout rate to be applied to each residual block prior to residual connection (default:None) - - Returns: - A `torch.nn.Module` object. - """ - - def __init__(self, - in_channels, - filters=[16,32,64,128,256], - pool_sizes = None, - kernel_size=3, - conv_blocks_per_level=1, - rank=3, - norm_type=None, - pool_type='MaxPool', - activation='relu', - dropout_rate=None): - super().__init__() - - n_levels = len(filters) - self.conv_blocks = nn.ModuleList([ - ImageConvBlock(in_channels=in_channels if i==0 else filters[i-1], - filters=filters[i], - kernel_size=kernel_size, - depth=conv_blocks_per_level, - rank=rank, - activation=activation, - norm_type=norm_type, - dropout_rate=dropout_rate) - for i in range(n_levels) - ]) - - if pool_sizes is None: - pool_sizes = 2 - - pool_sizes_standard = standardize_spatial_factors(pool_sizes, rank) - - pool = get_image_layer(pool_type, rank) - self.maxpools = nn.ModuleList([ - pool(pool_sizes_standard[i-1]) if i>0 else nn.Identity() - for i in range(n_levels) - ]) - - def forward(self,x): - """ - Args: - x (torch.Tensor): Input image [in_channels, ...] - - Returns: - List[torch.Tensor]: Output feature maps from each level ordered from top to bottom [Tensor([filters[0], ...], ..., Tensor([filters[N], ...]) - """ - outputs = [] - for pool, conv in zip(self.maxpools, self.conv_blocks): - x = conv(pool(x)) - outputs.append(x) - return outputs - - -class ImageDecoder(nn.Module): - """ - CNN decoder for images. Mirrors ImageEncoder like a UNet decoder. - - Args: - filters (List[int]): Encoder filter sizes in top→bottom order. - kernel_size (int): Convolution kernel size. - conv_blocks_per_level (int): Number of conv blocks per level. - rank (int): Spatial rank (2 or 3). - upsample_type (str): "ConvTranspose" or "Upsample". - activation (str): Activation name. - norm_type (str): Normalization type. - dropout_rate (float): Dropout rate. - skip (bool): Use skip connections. - """ - - def __init__(self, - filters=[16,32,64,128,256], - kernel_size=3, - pool_sizes=None, - upsample_sizes = None, - conv_blocks_per_level=1, - rank=3, - upsample_type="ConvTranspose", - activation="relu", - norm_type=None, - dropout_rate=None, - skip=True): - super().__init__() - - self.skip = skip - self.rank = rank - - if pool_sizes is None: - pool_sizes = 2 - - pool_sizes_standard = standardize_spatial_factors(pool_sizes, rank) - - n_levels = len(filters) - - rev_filters = filters[::-1] - rev_pool_sizes = pool_sizes_standard[::-1] - - if upsample_sizes is None: - upsample_sizes = rev_pool_sizes - else: - upsample_sizes = standardize_spatial_factors(upsample_sizes, rank) - - - if upsample_type.lower() == 'upsample': - self.ups = nn.ModuleList([ - nn.Upsample(scale_factor=upsample_sizes[i], mode='trilinear' if rank==3 else 'bilinear', align_corners=True) - for i in range(n_levels - 1) - ]) - else: - up_layer = get_image_layer(upsample_type, rank) - self.ups = nn.ModuleList([ - up_layer(rev_filters[i], rev_filters[i+1], kernel_size=upsample_sizes[i], stride=upsample_sizes[i]) - for i in range(n_levels - 1) - ]) - - - self.conv_blocks = nn.ModuleList([ - ImageConvBlock( - in_channels=rev_filters[i+1] * (2 if skip else 1), - filters=rev_filters[i+1], - kernel_size=kernel_size, - depth=conv_blocks_per_level, - rank=rank, - activation=activation, - norm_type=norm_type, - dropout_rate=dropout_rate - ) - for i in range(n_levels - 1) - ]) - - - def _match_size(self, x, skip): - """ - Match skip spatial size to x spatial size using: - - center crop if skip is larger - - interpolation if skip is smaller - - Args: - x: decoder tensor, shape [B, C, ...] - skip: encoder skip tensor, shape [B, C, ...] - - Returns: - skip resized to have spatial shape x.shape[2:] - """ - target_size = x.shape[2:] - skip_size = skip.shape[2:] - - if skip_size == target_size: - return skip - - # First crop any dimensions where skip is too large - slices = [slice(None), slice(None)] - needs_crop = False - - for s, t in zip(skip_size, target_size): - if s > t: - start = (s - t) // 2 - end = start + t - slices.append(slice(start, end)) - needs_crop = True - else: - slices.append(slice(None)) - - if needs_crop: - skip = skip[tuple(slices)] - - # Then upsample if any dimensions are still too small - if skip.shape[2:] != target_size: - mode = "trilinear" if self.rank == 3 else "bilinear" - skip = F.interpolate(skip, size=target_size, mode=mode, align_corners=True) - - return skip - - - def forward(self, encoder_outputs): - """ - Args: - encoder_outputs: List of tensors from encoder (top→bottom). - - Returns: - Decoded tensor at highest resolution. - """ - - # Reverse the encoder outputs so we traverse from bottleneck to top - rev_enc = encoder_outputs[::-1] - - # Start from bottleneck - x = rev_enc[0] - - # Traverse decoder levels - for i, (up, conv) in enumerate(zip(self.ups, self.conv_blocks)): - x = up(x) - - if self.skip: - skip_feat = rev_enc[i + 1] # next encoder feature - skip_feat = self._match_size(x, skip_feat) - x = torch.cat([x, skip_feat], dim=1) - - x = conv(x) - - return x - -class UNet(nn.Module): - """ - Flexible UNet for 2D or 3D images. - - Args: - in_channels (int): Input channels. - out_channels (int): Output channels. - filters (List[int]): Encoder filter sizes. - kernel_size (int): Conv kernel size. - pool_sizes (Tuple or list): pool sizes per level , - upsample_sizes (Tuple or list): upsample sizes per level, - conv_blocks_per_level (int): Depth per level. - rank (int): Spatial rank. - activation (str): Activation function. - norm_type (str): Normalization type. - dropout_rate (float): Dropout. - final_activation (str): Output activation. - """ - - def __init__(self, - in_channels, - out_channels, - filters=[16,32,64,128,256], - pool_sizes = None, - upsample_sizes = None, - kernel_size=3, - conv_blocks_per_level=1, - rank=3, - activation="relu", - norm_type=None, - dropout_rate=None, - final_activation="linear"): - - pool_sizes_temp = [] - if pool_sizes is None: - for i in range(len(filters)-1): - pool_sizes_temp.append(2) - pool_sizes = pool_sizes_temp - - if not isinstance(pool_sizes, (list, tuple)): - raise TypeError("pool_sizes must be a list or tuple") - - for i, p in enumerate(pool_sizes): - if isinstance(p, int) and not isinstance(p,bool): - if p < 1: - raise ValueError(f"pool_sizes must be positive") - elif isinstance(p, (tuple,list)): - if len(p) != rank: - raise ValueError(f"pool_sizes tuple must be same length as rank") - for j , ps in enumerate(p): - if not isinstance(ps, int) or isinstance(ps,bool): - raise TypeError(f"pool_sizes must be an int") - if ps < 1: - raise ValueError(f"pool_sizes must be positive") - else: - raise TypeError("each entry in pool_sizes must be either an int or a tuple or list") - - if len(filters) != (len(pool_sizes)+1): - raise ValueError(f"pool_sizes do not match number of filters. For {len(filters)}, please input {len(filters)-1} number of pools.") - - - if upsample_sizes is not None: - if not isinstance(upsample_sizes, (list, tuple)): - raise TypeError("upsample_sizes must be a list or tuple") - - for i, p in enumerate(upsample_sizes): - if isinstance(p, int) and not isinstance(p,bool): - if p < 1: - raise ValueError(f"upsample_sizes must be positive") - elif isinstance(p, (tuple,list)): - if len(p) != rank: - raise ValueError(f"upsample_sizes tuple must be same length as rank") - for j , ps in enumerate(p): - if not isinstance(ps, int) or isinstance(ps,bool): - raise TypeError(f"upsample_sizes must be an int") - if ps < 1: - raise ValueError(f"upsample_sizes must be positive") - else: - raise TypeError("each entry in upsample_sizes must be either an int or a tuple or list") - - if len(filters) != (len(upsample_sizes)+1): - raise ValueError(f"upsample_sizes do not match number of filters. For {len(filters)}, please input {len(filters)-1} number of upsamples.") - - - super().__init__() - - self.encoder = ImageEncoder( - in_channels=in_channels, - filters=filters, - pool_sizes=pool_sizes, - kernel_size=kernel_size, - conv_blocks_per_level=conv_blocks_per_level, - rank=rank, - activation=activation, - norm_type=norm_type, - dropout_rate=dropout_rate - ) - - self.decoder = ImageDecoder( - filters=filters, - pool_sizes=pool_sizes, - upsample_sizes = upsample_sizes, - kernel_size=kernel_size, - conv_blocks_per_level=conv_blocks_per_level, - rank=rank, - activation=activation, - norm_type=norm_type, - dropout_rate=dropout_rate - ) - - conv = get_image_layer("Conv", rank) - - self.final_conv = conv(filters[0], out_channels, kernel_size=1) - - self.final_act = ( - get_activation(final_activation)() - if final_activation.lower() != "linear" - else nn.Identity() - ) - - def forward(self, x): - enc_feats = self.encoder(x) - x = self.decoder(enc_feats) - x = self.final_conv(x) - x = self.final_act(x) - return x - -class StandardUNet(UNet): - """ - Standaed UNet for 2D or 3D images. - - Args: - in_channels (int): Input channels. - out_channels (int): Output channels. - filters (List[int]): Encoder filter sizes. - kernel_size (int): Conv kernel size. - pool_sizes (Tuple or list): pool sizes per dim , - conv_blocks_per_level (int): Depth per level. - rank (int): Spatial rank. - activation (str): Activation function. - norm_type (str): Normalization type. - dropout_rate (float): Dropout. - final_activation (str): Output activation. - """ - - def __init__(self, - in_channels, - out_channels, - filters=[16, 32, 64, 128, 256], - pool_size = 2, - kernel_size=3, - conv_blocks_per_level=1, - rank=3, - activation="relu", - norm_type=None, - dropout_rate=None, - final_activation="linear"): - - pool_size_standard = [] - if isinstance(pool_size, int): - pool_size_single = [] - for j in range(rank): - pool_size_single.append(pool_size) - pool_size_single = tuple(pool_size_single) - for i in range(len(filters)-1): - pool_size_standard.append(pool_size_single) - elif isinstance(pool_size , (tuple,list)): - if len(pool_size) == rank: - for i in range(len(filters)-1): - pool_size_standard.append(pool_size) - else: - raise ValueError(f"pool_size must be an int or have same length as rank") - - super().__init__( - in_channels=in_channels, - out_channels=out_channels, - filters=filters, - pool_sizes=pool_size_standard, - upsample_sizes=None, - kernel_size=kernel_size, - conv_blocks_per_level=conv_blocks_per_level, - rank=rank, - activation=activation, - norm_type=norm_type, - dropout_rate=dropout_rate, - final_activation=final_activation, - ) \ No newline at end of file diff --git a/im2sim/models/__init__.py b/im2sim/models/__init__.py deleted file mode 100644 index c2e035f..0000000 --- a/im2sim/models/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .image_to_graph import SimpleI2G, I2GUNet -from .utils import get_model_config -from .UNet import UNet, StandardUNet \ No newline at end of file diff --git a/im2sim/models/image_to_graph.py b/im2sim/models/image_to_graph.py deleted file mode 100644 index 38de625..0000000 --- a/im2sim/models/image_to_graph.py +++ /dev/null @@ -1,332 +0,0 @@ -import logging - -import torch -from torch import nn -import torch_geometric.nn as gnn -# from torch_geometric.nn import TopKPooling -from torch_geometric.data import Data - -from ..layers import * - -logger = logging.getLogger(__name__) - - - -class SimpleI2G(nn.Module): - - def __init__(self, - in_channels, - out_channels, - cnn_filters=[16,32,64,128,256], - cnn_kernel_size=3, - cnn_res_depth=3, - cnn_res_blocks_per_level=2, - cnn_rank=3, - cnn_norm_type=None, - cnn_pool_type='MaxPool', - cnn_pool_size=2, - cnn_activation='relu', - projection_ids = [[3,4],[1,2],[0,1]], - gnn_filters = [[384,288], [144,96], [64,32]], - gnn_res_depth = 3, - gnn_n_process_blocks = 1, - gnn_n_deform_blocks = 3, - template_edge_index=None, - gnn_conv_type="ChebConv", - gnn_conv_kwargs={'K':3}, - gnn_activation="relu", - out_activation="linear", - gnn_norm_type="InstanceNorm", - batched_ops=True): - super().__init__() - - logger.debug("Defining SimpleI2G layers...") - self.batched_ops = batched_ops - - self.encoder = ImageResEncoder(in_channels=in_channels, - filters=cnn_filters, - kernel_size=cnn_kernel_size, - res_depth=cnn_res_depth, - res_blocks_per_level=cnn_res_blocks_per_level, - rank=cnn_rank, - norm_type=cnn_norm_type, - pool_type=cnn_pool_type, - pool_size=cnn_pool_size, - activation=cnn_activation) - - # self.projection_layers = nn.ModuleList([TrilinearProjection() for _ in range(len(cnn_filters))]) - self.projection_ids = projection_ids - - projection_channels = _get_projection_channels(cnn_filters, self.projection_ids) - self.decoder_blocks = nn.ModuleList([ - GraphResDecoderBlock(projection_channels=projection_channels[i], - graph_channels=out_channels if i==0 else gnn_filters[i-1][1], - out_channels=out_channels, - filters=gnn_filters[i], - res_depth=gnn_res_depth, - n_process_blocks = gnn_n_process_blocks, - n_deform_blocks = gnn_n_deform_blocks, - template_edge_index=template_edge_index, - conv_type=gnn_conv_type, - conv_kwargs=gnn_conv_kwargs, - activation=gnn_activation, - out_activation=out_activation, - norm_type=gnn_norm_type) - for i in range(len(gnn_filters)) - - ]) - logger.debug("Done") - - def forward(self, x, template): - logger.debug("In model forward pass...") - encoder_outputs = self.encoder(x) - outputs = [] - graph_features = template.x.clone() - curr_mesh = template.x.clone() - for dec, ids in zip(self.decoder_blocks, self.projection_ids): - proj_inp = torch.cat([TrilinearProjection(domain_size=x.shape[-3:], batch_ops=self.batched_ops)(encoder_outputs[id], curr_mesh[:,:3], template.batch) - for id in ids], dim=-1) - graph_features, curr_mesh = dec(graph_features,proj_inp,curr_mesh,template.edge_index) - out_graph = template.clone() - out_graph.x = curr_mesh - outputs.append(out_graph) - return outputs - - -# class I2GUNet(nn.Module): - -# def __init__(self, -# in_channels, -# out_channels, -# filters=[16,32,64,128,256], -# cnn_kernel_size=3, -# cnn_res_depth=3, -# cnn_res_blocks_per_level=2, -# cnn_rank=3, -# cnn_norm_type=None, -# cnn_pool_type='MaxPool', -# cnn_pool_size=2, -# cnn_activation='relu', -# gnn_res_depth = 3, -# gnn_n_process_blocks = 1, -# gnn_n_deform_blocks = 3, -# template_edge_index=None, -# gnn_conv_type="ChebConv", -# gnn_conv_kwargs={'K':3}, -# gnn_activation="relu", -# out_activation="linear", -# gnn_norm_type="InstanceNorm", -# batched_ops=True): -# super().__init__() - -# logger.debug("Defining I2G layers...") -# self.batched_ops = batched_ops -# self.n_levels = len(filters) - -# self.encoder = ImageResEncoder(in_channels=in_channels, -# filters=filters, -# kernel_size=cnn_kernel_size, -# res_depth=cnn_res_depth, -# res_blocks_per_level=cnn_res_blocks_per_level, -# rank=cnn_rank, -# norm_type=cnn_norm_type, -# pool_type=cnn_pool_type, -# pool_size=cnn_pool_size, -# activation=cnn_activation) - -# gnn_config = dict(depth=gnn_res_depth, -# conv_type=gnn_conv_type, -# conv_kwargs=gnn_conv_kwargs, -# activation=gnn_activation, -# norm_type=gnn_norm_type) - -# self.align_conv = nn.ModuleList([ -# gnn.Sequential('x, edge_index, batch',[ -# (GraphConvResBlock(in_channels=out_channels*2 if i==0 else filt, -# filters=filt, -# **gnn_config),'x, edge_index -> x') -# for i in range(gnn_n_process_blocks) -# ]) -# for filt in reversed(filters) -# ]) - -# self.deform_conv = nn.ModuleList([ -# gnn.Sequential('x, edge_index, batch',[ -# (GraphConvResBlock(in_channels=filt*2 + out_channels if i==0 else filt, -# filters=filt, -# **gnn_config), 'x, edge_index -> x') -# for i in range(gnn_n_deform_blocks) -# ]) -# for filt in reversed(filters) -# ]) - -# self.convert_convs = nn.ModuleList([ -# GraphConvBlock(in_channels=filt, -# filters=out_channels, -# depth=1, -# conv_type=gnn_conv_type, -# conv_kwargs=gnn_conv_kwargs, -# activation=out_activation, -# norm_type=None) -# for filt in reversed(filters) -# ]) - -# self.pools = nn.ModuleList([gnn.TopKPooling(in_channels=3) for _ in range(len(filters)-1)]) - - -# logger.debug("Done") - -# def forward(self, img, template): -# logger.debug("In model forward pass...") -# # Get encoder outputs -# encoder_outputs = self.encoder(img) -# encoder_outputs.reverse() -# # Get pooled coordinates of the template - -# x, edge_index, batch = template.x, template.edge_index, template.batch -# feats, edges, batches,perms = [x],[edge_index],[batch],[] -# # layer = TopKPooling(in_channels=3) -# # logger.debug("device: x:%s, edge:%s, batch:%s, layer:%s", x.device, edge_index.device, batch.device, next(layer.parameters()).device) - -# for pool in self.pools: -# logger.debug("Pooling the template...") - -# x, edge_index, _, batch, perm, _ = pool(x, edge_index, batch=batch) -# feats.append(x) -# edges.append(edge_index) -# batches.append(batch) -# logger.debug("shape - x:%s", tuple(x.shape)) -# perms.append(perm) -# feats.reverse() # [10k, 5k, 2.5k, 1.25k, 600].reverse() -# edges.reverse() -# batches.reverse() -# perms.append(torch.ones(feats[0].shape[0]).to(torch.bool)) # add a selector that does nothing for the bottom level -# perms.reverse() # [5k, 2.5k,1.24k,600].reverse() - -# curr_mesh = feats[0] -# outputs=[] -# for enc, feat, edge, batch, perm, aconv, cconv, dconv in zip(encoder_outputs, feats, edges, batches, perms, self.align_conv, self.convert_convs, self.deform_conv): -# logger.debug("In graph decoder...") -# temp = torch.zeros_like(feat) -# temp[perm] = curr_mesh -# curr_mesh = torch.cat([temp, feat], axis=-1) # [N,6] -# logger.debug("curr_mesh:%s, edge:%s", tuple(curr_mesh.shape), tuple(edge.shape)) -# graph_features = aconv(curr_mesh, edge) # [N,6] -> [N,F] -# logger.debug("curr_mesh after align conv:%s", tuple(graph_features.shape)) -# proc_mesh = temp + cconv(graph_features,edge) # [N,F] -> [N,3] -# logger.debug("curr_mesh after convert conv:%s", tuple(proc_mesh.shape)) -# proj = TrilinearProjection(domain_size=img.shape[-3:], batch_ops=self.batched_ops)(enc, proc_mesh[:,:3], batch) -# logger.debug("projection shape:%s", tuple(proj.shape)) -# curr_mesh = torch.cat([proc_mesh, graph_features, proj], axis=-1) # [N, 3+F+C] -# logger.debug("deform conv input shape:%s", tuple(curr_mesh.shape)) -# curr_mesh = dconv(curr_mesh, edge) # [N,F] -# logger.debug("deform conv output shape:%s", tuple(curr_mesh.shape)) -# curr_mesh = proc_mesh + cconv(curr_mesh, edge) # [N,3] -# logger.debug("convert conv output shape:%s", tuple(curr_mesh.shape)) -# out_graph = Data() -# out_graph.x = curr_mesh -# out_graph.edge_index = edge -# out_graph.batch = batch -# outputs.append(out_graph) -# outputs[-1] = template.clone() -# outputs[-1].x = curr_mesh -# return outputs - -class I2GUNet(nn.Module): - - def __init__(self, - in_channels, - out_channels, - domain_size, - filters=[16,32,64,128,256], - cnn_kernel_size=3, - cnn_res_depth=3, - cnn_res_blocks_per_level=2, - cnn_rank=3, - cnn_norm_type='InstanceNorm', - cnn_pool_type='MaxPool', - cnn_pool_size=2, - cnn_activation='leaky_relu', - gnn_res_depth = 3, - gnn_n_align_blocks = 1, - gnn_n_deform_blocks = 3, - gnn_conv_type="ChebConv", - gnn_conv_kwargs={'K':3}, - gnn_activation="leaky_relu", - gnn_norm_type="InstanceNorm", - batched_ops=True): - super().__init__() - - logger.debug("Defining I2G layers...") - self.batched_ops = batched_ops - self.n_levels = len(filters) - self.n_channels = out_channels - self.encoder = ImageResEncoder(in_channels=in_channels, - filters=filters, - kernel_size=cnn_kernel_size, - res_depth=cnn_res_depth, - res_blocks_per_level=cnn_res_blocks_per_level, - rank=cnn_rank, - norm_type=cnn_norm_type, - pool_type=cnn_pool_type, - pool_size=cnn_pool_size, - activation=cnn_activation) - - - self.gpool = RecursiveClusterPooling(n_levels=self.n_levels) - - self.decoder = nn.ModuleList([ - GraphUNetDecoderBlock(#in_channels = out_channels if i==1 else filters[-i+1], - out_channels = out_channels, - filters = filters[-i], - domain_size = domain_size, - res_depth = gnn_res_depth, - n_align_blocks = 0 if i==1 else gnn_n_align_blocks, - n_deform_blocks = gnn_n_deform_blocks, - conv_type=gnn_conv_type, - conv_kwargs=gnn_conv_kwargs, - activation=gnn_activation, - norm_type=gnn_norm_type, - batched_ops=batched_ops) - for i in range(1,self.n_levels+1) - ]) - - - logger.debug("Done") - - - def forward(self, img, template): - encoder_features = self.encoder(img) - encoder_features.reverse() - - multi_template = self.gpool(template) - multi_template.reverse() - - outputs = [] - deformation = torch.zeros_like(multi_template[0].x) - # features = multi_template[0].x.clone() - for i, (dec_layer, enc) in enumerate(zip(self.decoder, encoder_features)): - t = multi_template[i] - deformation = dec_layer(enc,deformation,t.x, t.edge_index, t.batch) - out_graph = Data(x=t.x+deformation, edge_index=t.edge_index, batch=t.batch) - outputs.append(out_graph) - if i < self.n_levels-1: - deformation = gnn.unpool.knn_interpolate(deformation, t.x, multi_template[i+1].x) - - - - # outputs[-1] = template.clone() - # outputs[-1].x = template.x+deformation+prev_deformation - return outputs - - - - -def _get_projection_channels(filters, ids): - channels = [] - for id_list in ids: - sum = 0 - for id in id_list: - sum += filters[id] - channels.append(sum) - return channels diff --git a/im2sim/models/utils.py b/im2sim/models/utils.py deleted file mode 100644 index 4203b8a..0000000 --- a/im2sim/models/utils.py +++ /dev/null @@ -1,43 +0,0 @@ -def get_model_config(name): - """Get the default config for a specified type of model. - - Args: - name: A `str`. The name of the model type. - - Returns: - A config dictionary - - Raises: - ValueError: If the requested model config doesn't exist. - """ - try: - return _CONFIGS[(name)] - except KeyError as err: - raise ValueError( - f"Could not find config for model with name '{name}'") from err - - -_CONFIGS = { - "Image2Flow":dict(cnn_filters=[16,48,96,192,384], - cnn_kernel_size=3, - cnn_res_depth=3, - cnn_res_blocks_per_level=2, - cnn_rank=3, - cnn_norm_type="InstanceNorm", - cnn_pool_type='MaxPool', - cnn_pool_size=2, - cnn_activation='leaky_relu', - projection_ids = [[3,4],[1,2],[0,1]], - gnn_filters = [[384,288], [144,96], [64,32]], - gnn_res_depth = 3, - gnn_n_process_blocks = 1, - gnn_n_deform_blocks = 3, - template_edge_index=None, - gnn_conv_type="ChebConv", - gnn_conv_kwargs={'K':3}, - gnn_activation="leaky_relu", - out_activation="linear", - gnn_norm_type="InstanceNorm", - batched_ops=True) - -} diff --git a/im2sim/plot/__init__.py b/im2sim/plot/__init__.py deleted file mode 100644 index 51620e3..0000000 --- a/im2sim/plot/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .pointcloud import PointCloudPlot \ No newline at end of file diff --git a/im2sim/plot/pointcloud.py b/im2sim/plot/pointcloud.py deleted file mode 100644 index 698cd2b..0000000 --- a/im2sim/plot/pointcloud.py +++ /dev/null @@ -1,167 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -import matplotlib.animation as animation - - -class PointCloudPlot(): - - def __init__(self, - nrows, - ncols, - point_sets, - color_sets=None, - figsize=None, - cmap='Blues_r', - elev=20, - azim=90): - - self.cmap = cmap - self.elev = elev - self.azim = azim - - if color_sets is None: - is_colored = False - color_sets = [ - 0.1 * np.ones(points.shape[0]) - for points in point_sets - ] - else: - is_colored = True - - if figsize is None: - figsize = (ncols * 3, nrows * 3) - - self.fig, axes = plt.subplots( - nrows, ncols, - figsize=figsize, - subplot_kw={'projection': '3d'} - ) - - self.axes = np.array(axes).reshape(-1) - self.scatters = [] - - maxs = np.max(point_sets[-1],axis=0) - mins = np.min(point_sets[-1],axis=0) - - for i, (ax, points, colors) in enumerate( - zip(self.axes, point_sets, color_sets)): - print(points.shape, colors.shape) - sc = ax.scatter(points[:, 0], - points[:, 1], - points[:, 2], - c=colors, - cmap=cmap, - vmin=colors.min(), - vmax=colors.max()) - - ax.view_init(elev=elev, - azim=azim, - vertical_axis='y') - - # if i == 0: - # lims = ax.get_w_lims() - # else: - ax.set_xlim(mins[0], maxs[0]) - ax.set_ylim(mins[1], maxs[1]) - ax.set_zlim(mins[2], maxs[2]) - - if is_colored: - plt.colorbar(sc, ax=ax, shrink=0.5) - - self.scatters.append(sc) - - self.point_sets = point_sets - self.color_sets = color_sets - - # --------------------------------------------------------- - # DRAW ONE FRAME (works for static OR animation) - # --------------------------------------------------------- - - def draw_frame(self, point_sets=None, color_sets=None): - - if point_sets is None: - point_sets = self.point_sets - - if color_sets is None: - color_sets = self.color_sets - - new_scatters = [] - - for ax, sc, pts, colors in zip( - self.axes, - self.scatters, - point_sets, - color_sets): - - # If number of points changed → recreate scatter - if sc is None or len(pts) != len(sc.get_offsets()): - - if sc is not None: - sc.remove() - - sc = ax.scatter( - pts[:, 0], - pts[:, 1], - pts[:, 2], - c=colors, - cmap=self.cmap, - vmin=0, - vmax=1 - ) - - else: - sc._offsets3d = (pts[:, 0], pts[:, 1], pts[:, 2]) - sc.set_array(colors) - - new_scatters.append(sc) - - self.scatters = new_scatters - - return self.scatters - - # --------------------------------------------------------- - # SAVE SINGLE IMAGE - # --------------------------------------------------------- - - def save_image(self, filename, dpi=200): - plt.tight_layout() - self.fig.savefig(filename, dpi=dpi) - plt.close(self.fig) - - # --------------------------------------------------------- - # ANIMATE - # --------------------------------------------------------- - - def animate(self, - point_sequence_sets, - color_sequence_sets=None, - filename="animation.gif", - fps=15): - - n_frames = len(point_sequence_sets) - - def update(frame): - - if color_sequence_sets is None: - colors = None - else: - colors = color_sequence_sets[frame] - - return self.draw_frame( - point_sets=point_sequence_sets[frame], - color_sets=colors - ) - - ani = animation.FuncAnimation( - self.fig, - update, - frames=n_frames, - blit=False - ) - - if filename.endswith(".gif"): - ani.save(filename, writer="pillow", fps=fps) - else: - ani.save(filename, writer="ffmpeg", fps=fps) - - plt.close(self.fig) \ No newline at end of file diff --git a/im2sim/src/__init__.py b/im2sim/src/__init__.py new file mode 100644 index 0000000..7794496 --- /dev/null +++ b/im2sim/src/__init__.py @@ -0,0 +1,8 @@ +# Import submodules. +from im2sim.src import data +from im2sim.src import layers +# from im2sim.src import configs +from im2sim.src import losses +from im2sim.src import models +from im2sim.src import plot + diff --git a/im2sim/src/data/__init__.py b/im2sim/src/data/__init__.py new file mode 100644 index 0000000..101d11e --- /dev/null +++ b/im2sim/src/data/__init__.py @@ -0,0 +1,25 @@ +from im2sim.src.data import mesh_utils, ops, transforms +from im2sim.src.data.core import ( + DataLoader, + Dataset, + FittableOperation, + InvertibleOperation, + Operation, + Pipeline, + Transform, + register_op, +) + +__all__ = [ + "mesh_utils", + "ops", + "transforms", + "Operation", + "InvertibleOperation", + "FittableOperation", + "register_op", + "Transform", + "DataLoader", + "Dataset", + "Pipeline", +] diff --git a/im2sim/data/contents.md b/im2sim/src/data/contents.md similarity index 100% rename from im2sim/data/contents.md rename to im2sim/src/data/contents.md diff --git a/im2sim/data/core.py b/im2sim/src/data/core.py similarity index 75% rename from im2sim/data/core.py rename to im2sim/src/data/core.py index e802dd1..1766998 100644 --- a/im2sim/data/core.py +++ b/im2sim/src/data/core.py @@ -1,114 +1,22 @@ -from abc import ABC, abstractmethod import copy -import torch -from torch_geometric.data import Data, Batch import logging +from abc import ABC, abstractmethod +from collections.abc import Callable +from typing import Any -logger = logging.getLogger(__name__) - - - - -def collate(batch): - ''' - Collates a batch of data in the form of dict{str: torch.Tensor/PyG.Data} - Tensors are batched by stacking in the 0 dim and PyG Data are batched by PyG (https://pytorch-geometric.readthedocs.io/en/2.5.2/advanced/batching.html) - ''' - out_dict = {} - for key,val in batch[0].items(): - if isinstance(val, torch.Tensor): - out_dict[key] = torch.utils.data.default_collate([b[key] for b in batch]) - elif isinstance(val, Data): - out_dict[key] = Batch.from_data_list([b[key] for b in batch]) - else: - raise TypeError(f"{key} is type {type(val)}. Generator outputs must be either torch.Tensor or torch_geometric.data.Data object") - return out_dict - - -def DataLoader(dataset, **kwargs): - """ - Create a DataLoader for an im2sim dataset. - - Args: - dataset (im2sim.data.Dataset): - Dataset to wrap in a DataLoader. - - **kwargs: - Additional keyword arguments passed to ``torch.utils.data.DataLoader``. - Common options include: - - - batch_size (int, optional): - Number of samples per batch (default: 1). - - shuffle (bool, optional): - Whether to reshuffle the data at every epoch (default: False). - - num_workers (int, optional): - Number of subprocesses used for data loading. ``0`` means data - is loaded in the main process (default: 0). - - pin_memory (bool, optional): - If True, tensors are copied into CUDA pinned memory before returning. - - See https://docs.pytorch.org/docs/stable/data.html for the full list - of supported arguments. - - Returns: - im2sim.data.DataLoader: - Configured DataLoader instance. - """ - return torch.utils.data.DataLoader(dataset, collate_fn=collate, **kwargs) - -class Dataset(torch.utils.data.Dataset): - """ - Template for building im2sim datasets. - - To build a custom dataset, create a new Dataset with a custom load function, - case files, and transforms. - - Args: - load_fn (Callable[[str], dict[str, Tensor | PyGData]]): - Function that loads all files needed for a specific case and returns - a dictionary containing the data for that case. - - cases (list[str]): - List of case names. These names are passed to `load_fn` to load data. - - transforms (list[Transform] | Pipeline): - Transforms or pipeline applied to each sample. - - Example: - >>> import torch - >>> - >>> cases = ['case1', 'case2', 'case3', 'case4'] - >>> - >>> def load(case): - ... img = torch.load(f'images/{case}.pt') - ... graph = torch.load(f'graphs/{case}.pt') - ... template = torch.load(f'template/{case}.pt') - ... return {'image': img, 'template': template, 'out_graph': graph} - >>> - >>> dataset = im2sim.data.Dataset(load_fn=load, cases=cases) - """ - def __init__(self, load_fn, cases, transforms=[]): - self.load_fn = load_fn - self.cases = cases - self.transforms = transforms - - def __len__(self): - return len(self.cases) +import torch +from torch_geometric.data import Batch, Data - def __getitem__(self, idx): - case = self.cases[idx] - sample = self.load_fn(case) +from im2sim.src.utils import api_util - for transform in self.transforms: - sample = transform(sample) - return sample - +logger = logging.getLogger(__name__) class Operation(ABC): """ - Abstract base class for making operations. + Abstract base class for making operations. To make a new simple operation, subclass this and overwrite the forward method. """ + def __init_subclass__(cls): original_init = cls.__init__ @@ -147,26 +55,20 @@ def load_state_dict(self, state): for k, v in state.items(): setattr(self, k, v) - def _is_serializable(self, v): """ helper function to check if attr is serializable """ - return ( - isinstance(v, (int, float, str, bool)) - or isinstance(v, torch.Tensor) - or v is None - ) - + return isinstance(v, (int, float, str, bool, torch.Tensor)) or v is None + def to(self, device): """ - method to move attrs to device for torch training + method to move attrs to device for torch training """ for k, v in self.__dict__.items(): if isinstance(v, torch.Tensor): setattr(self, k, v.to(device)) return self - class InvertibleOperation(Operation): @@ -195,7 +97,6 @@ def complete_fit(self): pass - class Transform: """ A wrapper for operations that allows the selective application of the operation by dict key, object attribute and channel @@ -206,11 +107,11 @@ class Transform: keys (list[str]): List of keys in the data dict for the op to operate over - - attr (str, optional): - If data[key] is an object, attr is the attribute of that object to perfrom the op over. - If the op needs the full object, set attr to 'all' - If data[key] is not an object attr=None + + attr (str, optional): + If data[key] is an object, attr is the attribute of that object to perfrom the op over. + If the op needs the full object, set attr to 'all' + If data[key] is not an object attr=None (default:None) """ @@ -218,14 +119,17 @@ def __init__( self, op, keys, - multikey = False, + multikey=False, attr=None, channels=None, per_channel=False, channel_dim=-1, - name=None + name=None, ): - + + if keys is None or keys == []: + raise ValueError("keys must be specified") + self.keys = keys if isinstance(keys, list) else [keys] self.attr = attr @@ -239,7 +143,7 @@ def __init__( self.channel_dim = channel_dim self.per_channel = per_channel - self.op = op + self.op = op self.name = name if name is not None else f"{op.__class__.__name__}_{'_'.join(self.keys)}" @@ -254,10 +158,7 @@ def __init__( # ----------------------------- def _get_target(self, data, key): - if self.attr is None: - return data[key] - - elif self.attr == "all": + if self.attr is None or self.attr == "all": return data[key] elif hasattr(data[key], self.attr): @@ -282,15 +183,10 @@ def _ensure_per_channel_ops(self, x): x_moved = torch.moveaxis(x, self.channel_dim, 0) - channels = ( - range(x_moved.shape[0]) - if self.channels is None - else self.channels - ) + channels = range(x_moved.shape[0]) if self.channels is None else self.channels self.op = [copy.deepcopy(self.op) for _ in channels] - def _apply_channel_op(self, x, fn, no_return=False): if self.channels is None and not self.per_channel: @@ -302,16 +198,10 @@ def _apply_channel_op(self, x, fn, no_return=False): if self.per_channel: self._ensure_per_channel_ops(x) - channels = ( - range(x_moved.shape[0]) - if self.channels is None - else self.channels - ) + channels = range(x_moved.shape[0]) if self.channels is None else self.channels if self.per_channel: - for i, c in enumerate(channels): - op = self.op[i] fn_op = getattr(op, fn) @@ -321,7 +211,6 @@ def _apply_channel_op(self, x, fn, no_return=False): x_moved[c] = fn_op(x_moved[c]) else: - idx = self.channels if self.channels is not None else slice(None) fn_op = getattr(self.op, fn) @@ -355,12 +244,11 @@ def _apply_op(self, data, fn, no_return=False): if not isinstance(outputs, (list, tuple)): outputs = [outputs] - - for k, out in zip(self.keys, outputs): + for k, out in zip(self.keys, outputs, strict=True): self._set_target(data, k, out) - elif len(self.keys)>1: - # ---- single key op over mutliple keys ---- + elif len(self.keys) > 1: + # ---- single key op over mutliple keys ---- inputs = [self._get_target(data, k) for k in self.keys] if no_return: @@ -368,12 +256,12 @@ def _apply_op(self, data, fn, no_return=False): self._apply_channel_op(i, fn, no_return) return - for i,k in zip(inputs, self.keys): + for i, k in zip(inputs, self.keys, strict=True): out = self._apply_channel_op(i, fn, no_return) self._set_target(data, k, out) else: - # --- single key --- + # --- single key --- key = self.keys[0] x = self._get_target(data, key) @@ -382,21 +270,23 @@ def _apply_op(self, data, fn, no_return=False): if no_return: return None - + self._set_target(data, key, x) return data - + # ----------------------------- # Public API # ----------------------------- def forward(self, data): + data = copy.deepcopy(data) if self.is_fittable and not self.fitted: raise RuntimeError(f"Fittable transform {self.name} has not been fit") return self._apply_op(data, "forward") def inverse(self, data): + data = copy.deepcopy(data) if not self.is_invertible: raise RuntimeError(f"{self.name} is not invertible") if self.is_fittable and not self.fitted: @@ -412,13 +302,14 @@ def fit(self, dataloader): print(self.op) if not self.is_fittable: return - + for batch in dataloader: self._apply_op(batch, "fit_step", no_return=True) # finalize fit if self.per_channel: - for op in self.op: op.complete_fit() + for op in self.op: + op.complete_fit() else: self.op.complete_fit() @@ -431,7 +322,9 @@ def fit(self, dataloader): def config(self): return { "type": self.__class__.__name__, - "op": self.op.__class__.__name__ if not self.per_channel else self.op[0].__class__.__name__, + "op": self.op.__class__.__name__ + if not self.per_channel + else self.op[0].__class__.__name__, "op_args": self.op.call_args if not self.per_channel else self.op[0].call_args, "name": self.name, "keys": self.keys, @@ -452,7 +345,7 @@ def state_dict(self): def load_state_dict(self, state): if self.per_channel: - for op, s in zip(self.op, state): + for op, s in zip(self.op, state, strict=True): op.load_state_dict(s) else: self.op.load_state_dict(state) @@ -461,13 +354,13 @@ def load_state_dict(self, state): def to(self, device): if self.per_channel: - for op in self.op: op.to(device) + for op in self.op: + op.to(device) else: self.op.to(device) class Pipeline: - def __init__(self, transforms): self.transforms = transforms @@ -475,39 +368,46 @@ def __init__(self, transforms): # Forward # ----------------------------- def __call__(self, data): - for t in self.transforms: - if logger.isEnabledFor(logging.DEBUG): - logging.debug(f' before {t.name}') - for k,v in data.items(): - logging.debug(k) - - if isinstance(v, Data): - for c in range(v.x.shape[-1]): - logging.debug(f'channel {c}- max:{v.x[...,c].max()}, min:{v.x[...,c].min()}') - else: - logging.debug(v.shape) + transforms = self._get_applicable_transforms(data.keys()) + for t in transforms: + logger.debug(f"before {t.name}") + _log_data(data) + data = t.forward(data) + + logger.debug(f"after {t.name}") + _log_data(data) return data # ----------------------------- # Inverse (reverse order) # ----------------------------- def inverse(self, data): - for t in reversed(self.transforms): + transforms = self._get_applicable_transforms(data.keys()) + for t in reversed(transforms): if t.is_invertible: - if logger.isEnabledFor(logging.DEBUG): - logging.debug(f' before {t.name}') - for k,v in data.items(): - logging.debug(k) - - if isinstance(v, Data): - for c in range(v.x.shape[-1]): - logging.debug(f'channel {c}- max:{v.x[...,c].max()}, min:{v.x[...,c].min()}') - else: - logging.debug(v.shape) + logger.debug(f"before {t.name} inverse") + _log_data(data) + data = t.inverse(data) + + logger.debug(f"after {t.name} inverse") + _log_data(data) + return data + def _get_applicable_transforms(self, keys: str) -> list[Transform]: + transforms = [] + for t in self.transforms: + skip = False + for key in t.keys: + if key not in keys: + skip = True + break + if not skip: + transforms.append(t) + return transforms + # ----------------------------- # Fit (only fittable transforms) # ----------------------------- @@ -516,26 +416,19 @@ def fit(self, dataset): temp_dataset = copy.deepcopy(dataset) for i in range(n): if self.transforms[i].is_fittable: - temp_dataset.transforms = [t.forward for t in self.transforms[:i]] - dataloader = DataLoader(temp_dataset,batch_size=1) + temp_dataset.transforms = [Pipeline(self.transforms[:i])] + dataloader = DataLoader(temp_dataset, batch_size=1) self.transforms[i].fit(dataloader) return self - - # ----------------------------- # Serialization # ----------------------------- def config(self): - return { - "transforms": [t.config() for t in self.transforms] - } + return {"transforms": [t.config() for t in self.transforms]} def state_dict(self): - return { - t.name: t.state_dict() - for t in self.transforms - } + return {t.name: t.state_dict() for t in self.transforms} # ----------------------------- # Loading @@ -546,12 +439,17 @@ def from_config(cls, config): transforms = [] for tconf in config["transforms"]: - op_cls = TRANSFORM_REGISTRY[tconf["op"]] op_args = tconf["op_args"] - op = op_cls(*op_args["args"], **op_args["kwargs"]) if not tconf["per_channel"]\ - else [op_cls(*op_args["args"], **op_args["kwargs"]) for _ in range(len(tconf["channels"]))] + op = ( + op_cls(*op_args["args"], **op_args["kwargs"]) + if not tconf["per_channel"] + else [ + op_cls(*op_args["args"], **op_args["kwargs"]) + for _ in range(len(tconf["channels"])) + ] + ) t = Transform( op=op, @@ -560,7 +458,7 @@ def from_config(cls, config): channels=tconf["channels"], per_channel=tconf["per_channel"], channel_dim=tconf["channel_dim"], - name=tconf["name"] + name=tconf["name"], ) transforms.append(t) @@ -580,10 +478,7 @@ def to(self, device): def save_pipeline(pipeline, path): - obj = { - "config": pipeline.config(), - "state": pipeline.state_dict() - } + obj = {"config": pipeline.config(), "state": pipeline.state_dict()} torch.save(obj, path) @@ -597,6 +492,19 @@ def load_pipeline(path): return pipeline + +def _log_data(data: dict[str, Any]) -> None: + if logger.isEnabledFor(logging.DEBUG): + for k, v in data.items(): + logger.debug(k) + + if isinstance(v, Data): + for c in range(v.x.shape[-1]): + logger.debug(f"channel {c}- max:{v.x[..., c].max()}, min:{v.x[..., c].min()}") + else: + logger.debug(v.shape) + + # ------------------------------------------------------------------------------------ # TRANSFORM REGISTRY DEFINITION # ------------------------------------------------------------------------------------ @@ -604,6 +512,127 @@ def load_pipeline(path): TRANSFORM_REGISTRY = {} + def register_op(cls): TRANSFORM_REGISTRY[cls.__name__] = cls - return cls \ No newline at end of file + return cls + + +# ------------------------------------------------------------------------------------ +# DATA LOADING +# ------------------------------------------------------------------------------------ + + +LoadFn = Callable[[str], dict[str, torch.Tensor | Data]] + + +def collate(batch): + """ + Collates a batch of data in the form of dict{str: torch.Tensor/PyG.Data} + Tensors are batched by stacking in the 0 dim and PyG Data are batched by PyG (https://pytorch-geometric.readthedocs.io/en/2.5.2/advanced/batching.html) + """ + out_dict = {} + for key, val in batch[0].items(): + if isinstance(val, torch.Tensor): + out_dict[key] = torch.utils.data.default_collate([b[key] for b in batch]) + elif isinstance(val, Data): + out_dict[key] = Batch.from_data_list([b[key] for b in batch]) + else: + raise TypeError( + f"{key} is type {type(val)}. Generator outputs must be either torch.Tensor or torch_geometric.data.Data object" + ) + return out_dict + + +def DataLoader(dataset, **kwargs): + """ + Create a DataLoader for an im2sim dataset. + + Args: + dataset (im2sim.data.Dataset): + Dataset to wrap in a DataLoader. + + **kwargs: + Additional keyword arguments passed to ``torch.utils.data.DataLoader``. + Common options include: + + - batch_size (int, optional): + Number of samples per batch (default: 1). + - shuffle (bool, optional): + Whether to reshuffle the data at every epoch (default: False). + - num_workers (int, optional): + Number of subprocesses used for data loading. ``0`` means data + is loaded in the main process (default: 0). + - pin_memory (bool, optional): + If True, tensors are copied into CUDA pinned memory before returning. + + See https://docs.pytorch.org/docs/stable/data.html for the full list + of supported arguments. + + Returns: + im2sim.data.DataLoader: + Configured DataLoader instance. + """ + return torch.utils.data.DataLoader(dataset, collate_fn=collate, **kwargs) + + +class Dataset(torch.utils.data.Dataset): + """ + Template for building im2sim datasets. + + To build a custom dataset, create a new Dataset with a custom load function, + case files, and transforms. + + Args: + load_fn (Callable[[str], dict[str, Tensor | PyGData]]): + Function that loads all files needed for a specific case and returns + a dictionary containing the data for that case. + + cases (list[str]): + List of case names. These names are passed to `load_fn` to load data. + + transforms (list[Transform] | Pipeline): + Transforms or pipeline applied to each sample. + + Example: + >>> import torch + >>> + >>> cases = ['case1', 'case2', 'case3', 'case4'] + >>> + >>> def load(case): + ... img = torch.load(f'images/{case}.pt') + ... graph = torch.load(f'graphs/{case}.pt') + ... template = torch.load(f'template/{case}.pt') + ... return {'image': img, 'template': template, 'out_graph': graph} + >>> + >>> dataset = im2sim.data.Dataset(load_fn=load, cases=cases) + """ + + def __init__( + self, + load_fn: LoadFn, + cases: list[str], + transforms: list[Transform] | Pipeline | None = None, + ): + self.load_fn = load_fn + self.cases = cases + self.add_transforms(transforms) + + def add_transforms(self, transforms: list[Transform] | Pipeline | None = None): + if transforms is None: + self.transforms = [] + elif isinstance(transforms, Pipeline): + self.transforms = [transforms] + elif isinstance(transforms, list): + self.transforms = transforms + + def __len__(self) -> int: + return len(self.cases) + + def __getitem__(self, idx: int) -> dict[str, torch.Tensor | Data]: + case = self.cases[idx] + sample = self.load_fn(case) + + for transform in self.transforms: + sample = transform(sample) + return sample diff --git a/im2sim/src/data/mesh_utils.py b/im2sim/src/data/mesh_utils.py new file mode 100644 index 0000000..ee8e4b2 --- /dev/null +++ b/im2sim/src/data/mesh_utils.py @@ -0,0 +1,324 @@ +import numpy as np +import torch +import torch_geometric.nn as gnn +from pyvista.core.pointset import PointGrid +from torch_geometric.data import Data +from torch_geometric.utils import to_undirected + + +class InputMeshError(ValueError): + pass + + +def get_structure_ids(mesh: PointGrid, structure_dict: dict[int, str]) -> dict[str, torch.Tensor]: + """ + Extracts node ids for different substructures in a pyvista PointGrid object. + + Args: + mesh (pyvista.core.pointset.PointGrid): A pyvista mesh object. + structure_dict (Dict[int, str]): A dictionary that maps pyvista 'CellEntityIds' to structure names. + + Returns: + ids (Dict[str, torch.Tensor]): A dictionary with items in the format 'structurename_index': torch.Tensor(N), where N is the number of nodes in the structure. + """ + cells = get_structure_edges(mesh, structure_dict) + ids = {f"{k.split('_edge_index')[0]}_index": torch.unique(v) for k, v in cells.items()} + return ids + + +def get_structure_edges(mesh: PointGrid, structure_dict: dict[int, str]) -> dict[str, torch.Tensor]: + """ + Extracts edges for different substructures in a pyvista PointGrid object. + + Args: + mesh (pyvista.core.pointset.PointGrid): A pyvista mesh object. + structure_dict (Dict[int, str]): A dictionary that maps pyvista 'CellEntityIds' to structure names. + + Returns: + edges (Dict[str, torch.Tensor]): A dictionary with items in the format 'structurename_index': torch.Tensor(2, N), + where N is the number of edges in the structure. + """ + + if _has_missing_ids(mesh, structure_dict): + raise InputMeshError("Mesh has missing ids") + + edges = {} + + for i, key in structure_dict.items(): + edges[f"{key}_edge_index"] = get_edges(mesh, i) + + return edges + + +def get_edges(mesh: PointGrid, structure_id: int) -> torch.Tensor: + """ + Extracts edges for a single structure in a pyvista PointGrid object. + + Args: + mesh (pyvista.core.pointset.PointGrid): A pyvista mesh object. + structure_id (int): An integer corresponding to the structure id + + Returns: + edges (torch.Tensor): Tensor of shape (2, N) where N is the number of edges in the structure. + """ + submesh = mesh.extract_cells(np.where(mesh["CellEntityIds"] == structure_id)) + edges = submesh.extract_all_edges().lines.reshape(-1, 3)[:, 1:].T + edges = torch.from_numpy(submesh["vtkOriginalPointIds"][edges]).long() + edges = to_undirected(edges) + return edges + + +def get_structure_cells(mesh: PointGrid, structure_dict: dict[int, str]) -> dict[str, torch.Tensor]: + """ + Extracts cells for different substructures in a pyvista PointGrid object. + + Args: + mesh (pyvista.core.pointset.PointGrid): A pyvista mesh object. + structure_dict (Dict[int, str]): A dictionary that maps pyvista 'CellEntityIds' to structure names. + + Returns: + cells (Dict[str, torch.Tensor]): A dictionary with items in the format 'structurename_index': torch.Tensor(m, N), where m is 3 for triangles and 4 for tetrahedrons + and N is the number of cells in the structure. + """ + if _has_missing_ids(mesh, structure_dict): + raise InputMeshError("Mesh has missing ids") + + out_dict = {k: None for k in structure_dict.values()} + + for id, k in structure_dict.items(): + submesh = mesh.extract_cells(np.where(mesh["CellEntityIds"] == id)[0]) + subcells = submesh.cells.reshape(-1, submesh.cells[0] + 1)[:, 1:] + cells = submesh["vtkOriginalPointIds"][subcells] + out_dict[k] = torch.from_numpy(cells).permute(1, 0).to(torch.long) + + out_dict = {f"{k}_cell_index": v for k, v in out_dict.items()} + + return out_dict + + +def _has_missing_ids(mesh: PointGrid, structure_dict: dict[int, str]) -> bool: + ids = np.unique(mesh["CellEntityIds"]) + + missing_ids = set(structure_dict.keys()) - set(ids.tolist()) + + return len(missing_ids) != 0 + + +def set_attrs(data: Data, attrs: dict[str, torch.Tensor]) -> None: + """ + A helper function to set multiple attributes of a PyG Data object with keys and values from a dictionary. + + Args: + data (torch_geometric.data.Data): Data object to be modified + attrs (Dict[str, torch.Tensor]): a dictionary of attribute names and values to be set in the Data object + + Returns: + None + """ + for k, v in attrs.items(): + setattr(data, k, v) + + +def get_edges_tet(mesh: PointGrid) -> torch.Tensor: + """ + A function to get the edge index for training from a tetrahedral pyvista mesh + + Args: + mesh (pyvista.core.pointset.PointGrid): A pyvista mesh object. + + Returns: + edges (torch.Tensor): A tensor of shape [2,M] where M is the number of edges and the values are the node ids + """ + edges = get_structure_edges(mesh, {0: "vol"})["vol_edge_index"] + return edges + + +def get_edges_surf(mesh: PointGrid) -> torch.Tensor: + """ + A function to get the edge index for training from a pyvista surface mesh + + Args: + mesh (pyvista.core.pointset.PointGrid): A pyvista mesh object. + + Returns: + edges (torch.Tensor): A tensor of shape [2,M] where M is the number of edges and the values are the node ids + """ + edges = mesh.extract_all_edges().lines.reshape(-1, 3)[:, 1:] + edges = torch.Tensor(edges).T.long() + edges = to_undirected(edges) + return edges + + +def get_node_features(mesh: PointGrid, feature_names: list[str]) -> torch.Tensor: + """ + Extracts the node features from a pyvista mesh object based on the feature names provided. + + Args: + mesh (pyvista.core.pointset.PointGrid): A pyvista mesh object. + feature_names (List[str]): A list of feature names in the mesh pointdata. + + Returns: + features (torch.Tensor): A tensor of shape [N,C] where N is the number of nodes and C is len(feature_names). + """ + features = torch.from_numpy(np.array([mesh.point_data[name] for name in feature_names]).T) + return features + + +def make_padded_batch(x: torch.Tensor, batch: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: + """ + A helper function to pad a batch of data to the same size. + + Takes a flat concatenated batch of variable-length graphs/pointclouds and + pads them to a uniform length so they can be stacked into a dense tensor. + + Args: + x (torch.Tensor): A tensor of features of shape (N, C) where N is the + total number of nodes across all instances in the batch, and C is + the number of features per node. + batch (torch.Tensor): A tensor of shape (N,) containing integer indices + in the range [0, B-1] where B is the batch size. Each value + indicates which instance in the batch the corresponding node + belongs to. + + Returns: + padded_x (torch.Tensor): A dense tensor of shape (B, L, C) where B is + the batch size and L is the length of the longest instance. Shorter + instances are zero-padded to length L. + mask (torch.BoolTensor): A boolean tensor of shape (B, L) where + mask[i, j] is True if position j in instance i is a real node, + and False if it is padding. Suitable for use as an attention mask + or for zeroing out padded positions in a loss function. + + Example: + >>> # 5 nodes total, 3 instances: instance 0 has 3 nodes, instances 1 and 2 have 1 node each + >>> x = torch.randn(5, 8) + >>> batch = torch.tensor([0, 0, 0, 1, 2]) + >>> padded_x, mask = make_padded_batch(x, batch) + >>> padded_x.shape # (3, 3, 8) + >>> mask.shape # (3, 3) + >>> mask + tensor([[ True, True, True], + [ True, False, False], + [ True, False, False]]) + """ + jagged_x = [x[batch == i] for i in torch.unique(batch)] + padded_x = torch.nn.utils.rnn.pad_sequence(jagged_x, batch_first=True) + lengths = torch.tensor([len(s) for s in jagged_x]) + mask = torch.arange(padded_x.size(1))[None, :] < lengths[:, None] + return padded_x, mask + + +def compute_edge_lengths(points: torch.Tensor, edges: torch.Tensor) -> torch.Tensor: + """ + Computes the squared Euclidean distance for each edge in a mesh. + + Args: + points (torch.Tensor): Node coordinate tensor of shape (N, D) where N + is the number of nodes and D is the spatial dimensionality + (e.g. 3 for 3D meshes). + edges (torch.Tensor): Edge index tensor of shape (2, E) where E is the + number of edges. Each column represents an edge as a pair of node + indices [src, dst]. + + Returns: + distances (torch.Tensor): A tensor of shape (E, D) containing the + per-dimension squared differences between the endpoints of each + edge. Sum over the last dimension to get scalar squared edge + lengths. + """ + coords = points[edges] + distances = torch.linalg.norm(coords[0] - coords[1], dim=-1) + return distances + + +def cluster_pool(mesh: Data) -> Data: + """ + Performs Graclus clustering-based pooling on a mesh graph, coarsening it + by merging nodes into clusters weighted by inverse edge length. + + Shorter edges produce higher weights, encouraging spatially close nodes to + be merged together. This preserves the overall geometry of the mesh while + reducing its resolution. + + Args: + mesh (torch_geometric.data.Data): A PyTorch Geometric Data object with + the following required attributes: + - x (torch.Tensor): Node feature matrix of shape (N, C). + - edge_index (torch.Tensor): Edge index tensor of shape (2, E). + + Returns: + pooled_mesh (torch_geometric.data.Data): A coarsened PyTorch Geometric + Data object with fewer nodes, where each node represents the + average of the nodes in its cluster. Has the same structure as the + input mesh with updated x and edge_index. + + Notes: + - Edge weights are computed as 1 / (squared_length + 1e-8), where the + epsilon prevents division by zero for degenerate zero-length edges. + - Pooling is performed using torch_geometric.nn.avg_pool, so node + features in each cluster are averaged. + """ + distances = compute_edge_lengths(mesh.x, mesh.edge_index) + weights = 1 / (distances + 1e-8) + clusters = gnn.graclus(mesh.edge_index, weights, mesh.x.shape[0]) + pooled_mesh = gnn.avg_pool(clusters, mesh) + return pooled_mesh + + +def rasterize(points: torch.Tensor, im_shape: list[int], vox_sizes: list[float]) -> torch.Tensor: + """ + Computes the squared Euclidean distance between voxel centroids in a grid to a pointcloud + + Args: + points (torch.Tensor): Node coordinate tensor of shape (N, D) where N + is the number of nodes and D is the spatial dimensionality + (e.g. 3 for 3D meshes). + im_shape (torch.Tensor): A list of dim sizes for the image/mask corresponding + to the point cloud. + vox_sizes (torch.Tensor): A list of voxel sizes for each dimension + + Returns: + distances (torch.Tensor): A tensor of shape specified by im_shape where each voxel + is the distance of the voxel centroid to the pointcloud. + """ + im_coords = [ + torch.arange(size / 2, n, size) for n, size in zip(im_shape, vox_sizes, strict=True) + ] + grids = torch.meshgrid(*im_coords, indexing="ij") # three [128,128,128] tensors + coord_tensor = torch.stack(grids, dim=-1).reshape(-1, 3) + + nns = gnn.pool.knn(x=points, y=coord_tensor, k=1) + dists = torch.linalg.norm(coord_tensor - points[nns[1]], dim=-1) + return dists.reshape(im_shape) + + +def hard_threshold(y: torch.Tensor, threshold: float = 1.0) -> torch.Tensor: + """ + Thresholds a Tensor y according to a specified float threshold. Every value less than the threshold + is assigned 1.0 and values greater are assigned 0.0 + + Args: + y (torch.Tensor): tensor containing the raw values + threshold (float): threshold value + + Returns: + y_thresh (torch.Tensor): thresholded input tensor + + """ + return (y < threshold).float() + + +def soft_threshold(y, threshold=1.5, sharpness=10.0): + """ + Thresholds a Tensor y according to a specified float threshold. Every value less than the threshold + is assigned 1.0 and values greater are assigned 0.0 + + Args: + y (torch.Tensor): tensor containing the raw values + threshold (float): threshold value + + Returns: + y_thresh (torch.Tensor): thresholded input tensor + + """ + return torch.sigmoid(sharpness * (threshold - y)) diff --git a/im2sim/data/ops.py b/im2sim/src/data/ops.py similarity index 67% rename from im2sim/data/ops.py rename to im2sim/src/data/ops.py index 5084737..b58a285 100644 --- a/im2sim/data/ops.py +++ b/im2sim/src/data/ops.py @@ -1,86 +1,114 @@ -from .core import * +import torch +from im2sim.src.data.core import FittableOperation, InvertibleOperation, Operation, register_op +from im2sim.src.utils import api_util + +# TODO: fix the PowerScaleOp # ------------------------------------------------------------------------------------ # OP FUNCTION LIBRARY # ------------------------------------------------------------------------------------ eps = 1e-8 +@api_util.export("ops.normtorange") def normtorange(x, max=None, min=None, a=0, b=1): - if min==None: + """ + Normalizes the input tensor `x` to a specified range [a, b]. + + Args: + x (torch.Tensor) : Input tensor to be normalized. + max (float, optional): Maximum value for normalization. If None, uses the maximum of `x`. + min (float, optional): Minimum value for normalization. If None, uses the minimum of `x`. + a (float, optional): Lower bound of the target range. Default is 0. + b (float, optional): Upper bound of the target range. Default is 1. + """ + if min is None: min = x.min() - if max==None: + if max is None: max = x.max() - return a + ((x-min)*(b-a))/(max-min+eps) + return a + ((x - min) * (b - a)) / (max - min + eps) + def inv_normtorange(x, max=None, min=None, a=0, b=1): - if min==None: + if min is None: min = x.min() - if max==None: + if max is None: max = x.max() - return min + ((x-a)*(max-min))/(b-a) + return min + ((x - a) * (max - min)) / (b - a) def normalise(x, max=None, min=None): return normtorange(x, max, min) + def inv_normalise(x, max, min): return inv_normtorange(x, max, min) -def standardise(x, mean=None, std=None): - if mean==None: - mean=x.mean() - if std==None: - std=x.std() - return (x-mean)/(std+eps) - -def inv_standardise(x, mean=None, std=None): - return x*std + mean +def standardise(x, mean=None, std=None): + if mean is None: + mean = x.mean() + if std is None: + std = x.std() + return (x - mean) / (std + eps) +def inv_standardise(x, mean=None, std=None): + return x * std + mean # ------------------------------------------------------------------------------------ # SIMPLE OPERATIONS LIBRARY # ------------------------------------------------------------------------------------ + @register_op class NormOp(Operation): - def forward(self, x): return normalise(x) + @register_op class RangeNormOp(Operation): - def __init__(self, a, b): self.a = a self.b = b + def forward(self, x): return normtorange(x, a=self.a, b=self.b) - + + @register_op class ZScoreOp(Operation): - def forward(self, x): return standardise(x) - + + # ------------------------------------------------------------------------------------ # INVERTIBLE OPERATIONS LIBRARY # ------------------------------------------------------------------------------------ + @register_op class PowerScaleOp(InvertibleOperation): - - def __init__(self, exp, preserve_sign=True): + def __init__(self, exp, preserve_sign=True, eps=1e-8): + if exp == 0: + raise ValueError("exp must not be 0") self.exp = exp + self.preserve_sign = preserve_sign + self.eps = eps def forward(self, x): - return torch.sign(x) * torch.pow(torch.abs(x), self.exp) + if self.preserve_sign: + return torch.sign(x) * torch.pow(torch.abs(x) + self.eps, self.exp) + else: + return torch.pow(x, self.exp) def inverse(self, x): - return torch.sign(x) * torch.pow(torch.abs(x), 1/self.exp) + if self.preserve_sign: + return torch.sign(x) * torch.pow(torch.abs(x) + self.eps, 1 / self.exp) + else: + return torch.pow(x, 1 / self.exp) # ------------------------------------------------------------------------------------ @@ -90,17 +118,16 @@ def inverse(self, x): @register_op class FitNormOp(FittableOperation): - def __init__(self): self.max = torch.Tensor([-torch.inf]) self.min = torch.Tensor([torch.inf]) def forward(self, x): return normalise(x, self.max, self.min) - + def inverse(self, x): return inv_normalise(x, self.max, self.min) - + def fit_step(self, x): self.max = torch.maximum(self.max, x.max()) self.min = torch.minimum(self.min, x.min()) @@ -108,21 +135,21 @@ def fit_step(self, x): def complete_fit(self): pass + @register_op class FitRangeNormOp(FittableOperation): - def __init__(self, a, b): self.a = a self.b = b self.max = torch.Tensor([-torch.inf]) self.min = torch.Tensor([torch.inf]) - + def forward(self, x): return normtorange(x, self.max, self.min, self.a, self.b) - + def inverse(self, x): return inv_normtorange(x, self.max, self.min, self.a, self.b) - + def fit_step(self, x): self.max = torch.maximum(self.max, x.max()) self.min = torch.minimum(self.min, x.min()) @@ -130,28 +157,25 @@ def fit_step(self, x): def complete_fit(self): pass + @register_op class FitZScoreOp(FittableOperation): - def __init__(self): self.sum = 0 self.sq_sum = 0 self.numel = 0 - + def forward(self, data): return standardise(data, self.mean, self.std) def inverse(self, data): return inv_standardise(data, self.mean, self.std) - + def fit_step(self, data): self.sum += data.sum() - self.sq_sum += (data ** 2).sum() + self.sq_sum += (data**2).sum() self.numel += data.numel() - def complete_fit(self): self.mean = self.sum / self.numel - self.std = torch.sqrt(self.sq_sum / self.numel - self.mean ** 2) - - \ No newline at end of file + self.std = torch.sqrt(self.sq_sum / self.numel - self.mean**2) diff --git a/im2sim/data/pca.py b/im2sim/src/data/pca.py similarity index 75% rename from im2sim/data/pca.py rename to im2sim/src/data/pca.py index 94378a4..4cede0e 100644 --- a/im2sim/data/pca.py +++ b/im2sim/src/data/pca.py @@ -1,7 +1,6 @@ - class PCA: """ - Principal Component Analysis + Principal Component Analysis This class can be used for PCA-related operations such as computing PCs and saving PC projection matrices @@ -21,31 +20,31 @@ class PCA: """ def __init__(self, data, axis=-1): - ''' + """ takes the data and axis, computes the PCs matrix and stores in object attributes - ''' + """ raise NotImplementedError def save(self): - '''saves the PC data''' + """saves the PC data""" raise NotImplementedError def load(self, fname): - '''loads saved PC data''' + """loads saved PC data""" raise NotImplementedError def forward_transform(data): - '''forward transform''' + """forward transform""" raise NotImplementedError - + def inverse_transform(data): - '''inverse transform''' + """inverse transform""" raise NotImplementedError def forward_transform_tf(data): - '''forward transform''' + """forward transform""" raise NotImplementedError - + def inverse_transform_tf(data): - '''inverse transform''' - raise NotImplementedError \ No newline at end of file + """inverse transform""" + raise NotImplementedError diff --git a/im2sim/data/scaling.py b/im2sim/src/data/scaling.py similarity index 98% rename from im2sim/data/scaling.py rename to im2sim/src/data/scaling.py index 4cf9ebf..1a0d9a4 100644 --- a/im2sim/data/scaling.py +++ b/im2sim/src/data/scaling.py @@ -1,5 +1,4 @@ -# TODO: Investigate PowerScaling to replace the StandardScaler and DataProc - +# TODO: Investigate PowerScaling to replace the StandardScaler and DataProc class StandardScaler: @@ -23,6 +22,7 @@ class StandardScaler: standard deviation of node feature(s) """ + class Normaliser: """ min-max normaliser for node features @@ -44,7 +44,7 @@ class Normaliser: max value of node feature(s) """ - + class DataProcessor: """ Node Data Processor for Im2Sim models @@ -67,5 +67,4 @@ class DataProcessor: -------- >>> obj = MyClass(param1=10, param2="test") >>> obj.method() - 42 """ - + 42""" diff --git a/im2sim/src/data/transforms.py b/im2sim/src/data/transforms.py new file mode 100644 index 0000000..1664a69 --- /dev/null +++ b/im2sim/src/data/transforms.py @@ -0,0 +1,147 @@ +from im2sim.src.data.core import Operation, Transform +from im2sim.src.data.ops import FitNormOp, FitRangeNormOp, FitZScoreOp, NormOp, PowerScaleOp, RangeNormOp, ZScoreOp + + +def transform_from_fn( + fn, keys, attr=None, channels=None, per_channel=False, channel_dim=-1, name=None +): + + class FnOp(Operation): + def forward(self, x): + return fn(x) + + return Transform( + op=FnOp(), + keys=keys, + attr=attr, + channels=channels, + per_channel=per_channel, + channel_dim=channel_dim, + name=name, + ) + + +# ------------------------------------------------------------------------------------ +# SIMPLE TRANSFORM FACTORIES +# ------------------------------------------------------------------------------------ + + +def Norm(keys, attr=None, channels=None, per_channel=False, channel_dim=-1, name=None): + return Transform( + op=NormOp(), + keys=keys, + attr=attr, + channels=channels, + per_channel=per_channel, + channel_dim=channel_dim, + name=name, + ) + + +def RangeNorm( + llim, + hlim, + keys, + attr=None, + channels=None, + per_channel=False, + channel_dim=-1, + name=None, +): + return Transform( + op=RangeNormOp(a=llim, b=hlim), + keys=keys, + attr=attr, + channels=channels, + per_channel=per_channel, + channel_dim=channel_dim, + name=name, + ) + + +def ZScore(keys, attr=None, channels=None, per_channel=False, channel_dim=-1, name=None): + return Transform( + op=ZScoreOp(), + keys=keys, + attr=attr, + channels=channels, + per_channel=per_channel, + channel_dim=channel_dim, + name=name, + ) + + +# ------------------------------------------------------------------------------------ +# INVERTIBLE TRANSFORM FACTORIES +# ------------------------------------------------------------------------------------ + + +def PowerScaling( + exp, + preserve_sign, + keys, + attr=None, + channels=None, + per_channel=False, + channel_dim=-1, + name=None, +): + return Transform( + op=PowerScaleOp(exp=exp, preserve_sign=preserve_sign), + keys=keys, + attr=attr, + channels=channels, + per_channel=per_channel, + channel_dim=channel_dim, + name=name, + ) + + +# ------------------------------------------------------------------------------------ +# FITTABLE TRANSFORM FACTORIES +# ------------------------------------------------------------------------------------ + + +def FitNorm(keys, attr=None, channels=None, per_channel=False, channel_dim=-1, name=None): + return Transform( + op=FitNormOp(), + keys=keys, + attr=attr, + channels=channels, + per_channel=per_channel, + channel_dim=channel_dim, + name=name, + ) + + +def FitRangeNorm( + llim, + hlim, + keys, + attr=None, + channels=None, + per_channel=False, + channel_dim=-1, + name=None, +): + return Transform( + op=FitRangeNormOp(a=llim, b=hlim), + keys=keys, + attr=attr, + channels=channels, + per_channel=per_channel, + channel_dim=channel_dim, + name=name, + ) + + +def FitZScore(keys, attr=None, channels=None, per_channel=False, channel_dim=-1, name=None): + return Transform( + op=FitZScoreOp(), + keys=keys, + attr=attr, + channels=channels, + per_channel=per_channel, + channel_dim=channel_dim, + name=name, + ) diff --git a/im2sim/src/layers/README.md b/im2sim/src/layers/README.md new file mode 100644 index 0000000..cd71c82 --- /dev/null +++ b/im2sim/src/layers/README.md @@ -0,0 +1,272 @@ +# Configurable Half U-Net + +This module provides a configurable PyTorch implementation of a **Half U-Net** architecture. + +The purpose of this implementation is to allow rapid experimentation with different CNN architectures without changing the model code. The network structure is controlled through configuration objects and reusable presets. + +The model is built around: + +- `HalfUNet` - the PyTorch model implementation +- `HalfUNetConfig` - defines the architecture configuration + + +The architecture supports: + +- 1D, 2D, and 3D inputs +- configurable encoder depth +- configurable feature channels +- custom convolution blocks +- custom pooling and upsampling layers +- configurable skip connection fusion +- residual blocks +- dilated bottlenecks +- depthwise separable convolutions +- Ghost convolution variants + +--- + +# Basic Usage + +A default Half U-Net can be created using a configuration object: + +```python +import torch + +from half_unet import HalfUNet, HalfUNetConfig + + +cfg = HalfUNetConfig(num_downsamples=2, hidden_channels=32) + +model = HalfUNet.build(rank=3, in_channels=1, out_channels=1, cfg=cfg) + + +x = torch.randn(1, 1, 64, 64, 64) + +y = model(x) +``` + +--- + +# Applying Presets + +Architectures can be modified using presets. Presets are composable, meaning multiple architectural changes can be applied together. + +Example: + +```python +cfg = HalfUNetConfig(num_downsamples=2) + +cfg = cfg.apply_presets(["ghost_depthwise", "residual"]) + +model = HalfUNet.build(rank=3, in_channels=1, out_channels=1, cfg=cfg) +``` + +--- + +# Available Presets + +## Residual Encoder Blocks + +Adds residual connections inside convolution blocks. + +Useful when deeper networks require improved gradient flow. + +```python +cfg = cfg.apply_presets(["residual"]) +``` + +--- + +## Dilated Bottleneck + +Replaces the deepest encoder block with dilated convolutions to increase the receptive field. + +Useful for: + +- large structures +- segmentation +- low-resolution feature processing + +```python +cfg = cfg.apply_presets(["dilated_bottleneck"]) +``` + +--- + +## Reconstruction + +Applies convolution block settings intended for image reconstruction tasks. + +Example applications: + +- MRI reconstruction +- denoising +- image restoration + +```python +cfg = cfg.apply_presets(["reconstruction"]) +``` + +--- + +## Segmentation + +Applies convolution block settings intended for segmentation tasks. + +Example applications: + +- medical image segmentation +- semantic segmentation + +```python +cfg = cfg.apply_presets(["segmentation"]) +``` + +--- + +## Depthwise Separable Convolutions + +Replaces standard convolutions with depthwise separable convolutions. + +This reduces: + +- parameter count +- memory usage +- computational cost + +```python +cfg = cfg.apply_presets(["depthwise_separable"]) +``` + +--- + +## Ghost Convolution Variants + +Uses Ghost convolution based blocks to reduce computational cost. + +### Ghost depthwise + +```python +cfg = cfg.apply_presets(["ghost_depthwise"]) +``` + +### Ghost depthwise separable + +```python +cfg = cfg.apply_presets(["ghost_depthwise_separable"]) +``` + +--- + +# Combining Presets + +Presets can be stacked to create task-specific architectures. + +Example: a segmentation model using residual blocks and a dilated bottleneck: + +```python +cfg = HalfUNetConfig(num_downsamples=3) + +cfg = cfg.apply_presets(["segmentation", "residual", "dilated_bottleneck"]) + +model = HalfUNet.build(rank=3, in_channels=1, out_channels=3, cfg=cfg) +``` + +Example: a lightweight reconstruction model: + +```python +cfg = HalfUNetConfig(num_downsamples=4) + +cfg = cfg.apply_presets(["reconstruction", "ghost_depthwise"]) + +model = HalfUNet.build(rank=3, in_channels=1, out_channels=1, cfg=cfg) +``` + +--- + +# Custom Encoder Configurations + +Each encoder level can optionally use a different convolution block configuration. + +Example: + +```python +cfg = HalfUNetConfig(num_downsamples=3, encoder_block_cfg=[block_cfg_1, block_cfg_2, block_cfg_3]) +``` + +This allows heterogeneous architectures such as: + +``` +Encoder level 1: + Standard convolution blocks + +Encoder level 2: + Depthwise separable convolution blocks + +Encoder level 3: + Dilated convolution blocks +``` + +--- + +# Pooling and Upsampling + +Pooling and upsampling operations are configurable. + +Example: + +```python +cfg = HalfUNetConfig(pool_spec=LayerConfig(name="AveragePool", kwargs={"kernel_size": 2})) +``` + +Different operations can also be provided at each level: + +```python +cfg.pool_spec = [ + LayerConfig(name="MaxPool", kwargs={"kernel_size": 2}), + LayerConfig(name="AveragePool", kwargs={"kernel_size": 2}), +] +``` + +Supported pooling layers: + +- `MaxPool` +- `AveragePool` + +Supported upsampling layers: + +- `Upsample` +- `PixelShuffle` + +--- + +# Skip Connection Fusion + +Encoder features are fused with decoder features using the configured fusion type. + +Default: + +```python +fusion_type = ResidualConnectionType.ADD +``` + +which performs: + +``` +decoder + encoder +``` + +Alternatively: + +```python +fusion_type = ResidualConnectionType.CONCAT +``` + +which performs: + +``` +[decoder, encoder] +``` + +When using concatenation, the output block automatically adjusts the number of input channels. + diff --git a/im2sim/src/layers/__init__.py b/im2sim/src/layers/__init__.py new file mode 100644 index 0000000..9c54d4a --- /dev/null +++ b/im2sim/src/layers/__init__.py @@ -0,0 +1,39 @@ +from im2sim.src.layers.graph_blocks import GraphConvBlock, GraphConvResBlock, GraphResDecoderBlock +# from image_blocks import ImageConvBlock, ImageConvResBlock, ImageDecoder, ImageEncoder +from im2sim.src.layers import custom_image_layers, image_conv_blocks, halfunet, reverse_halfunet +from im2sim.src.layers.layer_util import ( + get_activation, + get_image_layer, + register_activation, + register_graph_layer, + register_image_layer, + standardize_spatial_factors, +) +# from .meshgraphnets import MeshGraphNet, MGNDecoder, MGNEdgeBlock, MGNGnBlock, MGNNodeBlock +from im2sim.src.layers.projections import OGProjection + +__all__ = [ + "GraphConvBlock", + "GraphConvResBlock", + "GraphResDecoderBlock", + "ImageConvBlock", + "ImageConvResBlock", + "ImageEncoder", + "ImageDecoder", + "get_activation", + "get_image_layer", + "register_activation", + "register_graph_layer", + "register_image_layer", + "standardize_spatial_factors", + # "MGNEdgeBlock", + # "MGNNodeBlock", + # "MGNDecoder", + # "MGNGnBlock", + # "MeshGraphNet", + "OGProjection", + "custom_image_layers", + "image_conv_blocks", + "halfunet", + "reverse_halfunet", +] diff --git a/im2sim/layers/contents.md b/im2sim/src/layers/contents.md similarity index 100% rename from im2sim/layers/contents.md rename to im2sim/src/layers/contents.md diff --git a/im2sim/src/layers/custom_image_layers.py b/im2sim/src/layers/custom_image_layers.py new file mode 100644 index 0000000..a346193 --- /dev/null +++ b/im2sim/src/layers/custom_image_layers.py @@ -0,0 +1,375 @@ +import math + +import torch +from im2sim.src.layers.layer_util import get_activation, get_image_layer, register_with_ranks +from im2sim.src.utils import api_util + +@api_util.export("layers.DepthwiseConv") +@register_with_ranks("DepthwiseConv", ranks=(1, 2, 3)) +class DepthwiseConv(torch.nn.Module): + """ + Depthwise convolution layer that applies a separate convolutional filter to each input channel. + + This operation is useful for reducing the number of parameters and computational cost in convolutional neural networks, especially in mobile and embedded applications[1]. + + Args: + in_channels (int): Number of input channels. + out_channels (int): Number of output channels (should be equal to in_channels for depthwise convolution). + rank (int): The rank of the convolution (1 for 1D, 2 for 2D, 3 for 3D). + kernel_size (int | tuple): Size of the convolving kernel. Default is 3. + stride (int | tuple): Stride of the convolution. Default is 1. + padding (str or int | tuple): Padding added to all four sides of the input. Default is "same". + dilation (int | tuple): Spacing between kernel elements. Default is 1. + bias (bool): If True, adds a learnable bias to the output. Default is True. + + References: + .. [1] A. G. Howard et al., MobileNets: Efficient Convolutional Neural Networks for Mobile Vision Applications, + Apr. 17, 2017, arXiv: arXiv:1704.04861. doi: 10.48550/arXiv.1704.04861. + + """ + def __init__( + self, + in_channels, + out_channels, + rank, + kernel_size=3, + stride=1, + padding="same", + dilation=1, + bias=True, + ): + super().__init__() + self.conv = get_image_layer("Conv", rank)( + in_channels=in_channels, + out_channels=in_channels, + kernel_size=kernel_size, + stride=stride, + padding=padding, + dilation=dilation, + groups=in_channels, + bias=bias, + ) + + def forward(self, x): + """ + Forward pass of the depthwise convolution layer. + + Args: + x (torch.Tensor): Input tensor of shape (batch_size, in_channels, *spatial_dims). + + Returns: + torch.Tensor: Output tensor of shape (batch_size, out_channels, *spatial_dims). + """ + return self.conv(x) + +@api_util.export("layers.DepthwiseSeparableConv") +@register_with_ranks("DepthwiseSeparableConv", ranks=(1, 2, 3)) +class DepthwiseSeparableConv(torch.nn.Module): + """ + Depthwise separable convolution layer that consists of a depthwise convolution followed by a pointwise convolution. + + This operation is useful for reducing the number of parameters and computational cost in convolutional neural networks, + while retaining more representational power compared to standard depthwise convolution[1]. + + Args: + in_channels (int): Number of input channels. + out_channels (int): Number of output channels. + rank (int): The rank of the convolution (1 for 1D, 2 for 2D, 3 for 3D). + kernel_size (int | tuple): Size of the convolving kernel. Default is 3. + stride (int | tuple): Stride of the convolution. Default is 1. + padding (str or int | tuple): Padding added to all four sides of the input. Default is "same". + dilation (int | tuple): Spacing between kernel elements. Default is 1. + bias (bool): If True, adds a learnable bias to the output. Default is True. + activation (str or None): Activation function to apply after the pointwise convolution. Default is None. + + References: + .. [1] F. Chollet, Xception: Deep Learning with Depthwise Separable Convolutions, + Apr. 04, 2017, arXiv: arXiv:1610.02357. doi: 10.48550/arXiv.1610.02357. + + """ + def __init__( + self, + in_channels, + out_channels, + rank, + kernel_size=3, + stride=1, + padding="same", + dilation=1, + bias=True, + activation=None, + ): + super().__init__() + print("IN DWS:", in_channels) + self.depthwise = get_image_layer("Conv", rank)( + in_channels=in_channels, + out_channels=in_channels, + kernel_size=kernel_size, + stride=stride, + padding=padding, + dilation=dilation, + groups=in_channels, + bias=bias, + ) + self.pointwise = get_image_layer("Conv", rank)( + in_channels=in_channels, + out_channels=out_channels, + kernel_size=1, + stride=1, + padding=0, + bias=bias, + ) + self.activation = get_activation(activation) + + def forward(self, x): + """ + Forward pass of the depthwise separable convolution layer. + + Args: + x (torch.Tensor): Input tensor of shape (batch_size, in_channels, *spatial_dims). + + Returns: + torch.Tensor: Output tensor of shape (batch_size, out_channels, *spatial_dims). + """ + x = self.depthwise(x) + x = self.pointwise(x) + x = self.activation(x) + return x + +@api_util.export("layers.GhostConv") +@register_with_ranks("GhostConv", ranks=(1, 2, 3)) +class GhostConv(torch.nn.Module): + """ + Ghost convolution layer that generates more feature maps from cheap operations. + + This operation is useful for reducing the number of parameters and computational cost in convolutional neural networks. + The cheap operation can either be a depthwise convolution as per the original GhostNet paper[1] or a depthwise separable convolution as in the HalfUNet paper[2 + + Args: + in_channels (int): Number of input channels. + out_channels (int): Number of output channels. + rank (int): The rank of the convolution (1 for 1D, 2 for 2D, 3 for 3D). + kernel_size (int | tuple): Size of the convolving kernel for the primary convolution. Default is 3. + ratio (int): Ratio of the number of output channels to the number of primary convolution channels. Default is 2. + dw_kernel_size (int | tuple): Size of the convolving kernel for the cheap operation. Default is 3. + stride (int | tuple): Stride of the primary convolution. Default is 1. + padding (str or int | tuple): Padding added to all four sides of the input for the primary convolution. Default is "same". + separable (bool): If True, uses depthwise separable convolution for the cheap operation. Default is False. + bias (bool): If True, adds a learnable bias to the output. Default is True. + + References: + .. [1] K. Han, Y. Wang, Q. Tian, J. Guo, C. Xu, and C. Xu, GhostNet: More Features from Cheap Operations, + Mar. 13, 2020, arXiv: arXiv:1911.11907. doi: 10.48550/arXiv.1911.11907. + .. [2] H. Lu, Y. She, J. Tie, and S. Xu, Half-UNet: A Simplified U-Net Architecture for Medical Image Segmentation, + Front. Neuroinformatics, vol. 16, Jun. 2022, doi: 10.3389/fninf.2022.911679. + """ + def __init__( + self, + in_channels, + out_channels, + rank, + kernel_size=3, + ratio=2, + dw_kernel_size=3, + stride=1, + padding="same", + separable=False, + bias=True, + ): + super().__init__() + self.rank = rank + self.out_channels = out_channels + self.init_channels = int(out_channels / ratio) + self.new_channels = out_channels - self.init_channels + + self.primary_conv = get_image_layer("Conv", rank)( + in_channels=in_channels, + out_channels=self.init_channels, + kernel_size=kernel_size, + stride=stride, + padding=padding, + bias=bias, + ) + + cheap_conv_type = "DepthwiseSeparableConv" if separable else "DepthwiseConv" + print("IN GHOST:", self.init_channels) + self.cheap_operation = get_image_layer(cheap_conv_type, rank)( + in_channels=self.init_channels, + out_channels=self.new_channels, + kernel_size=dw_kernel_size, + stride=1, + padding="same", + bias=bias, + ) + + def forward(self, x): + """ + Forward pass of the Ghost convolution layer. + + Args: + x (torch.Tensor): Input tensor of shape (batch_size, in_channels, *spatial_dims). + + Returns: + torch.Tensor: Output tensor of shape (batch_size, out_channels, *spatial_dims). + """ + x1 = self.primary_conv(x) + x2 = self.cheap_operation(x1) + return torch.cat([x1, x2], dim=1) + +@api_util.export("layers.EfficientChannelAttn") +@register_with_ranks("EfficientChannelAttn", ranks=(1, 2, 3)) +class EfficientChannelAttn(torch.nn.Module): + """ + Efficient Channel Attention (ECA) layer that adaptively selects important channels based on global context. + + This operation is useful for improving the representational power of convolutional neural networks by focusing on the most informative channels[1]. + + Args: + channels (int): Number of input channels. + rank (int): The rank of the input tensor (1 for 1D, 2 for 2D, 3 for 3D). + + References: + .. [1] Q. Wang, B. Wu, P. Zhu, P. Li, W. Zuo, and Q. Hu, ECA-Net: Efficient Channel Attention for Deep Convolutional Neural Networks, + Mar. 04, 2020, arXiv: arXiv:1910.03151. doi: 10.48550/arXiv.1910.03151. + """ + def __init__(self, channels: int, rank: int): + super().__init__() + self.rank = rank + k_raw = math.log2(channels) / 2 + 0.5 + k = max(3, int(k_raw) if int(k_raw) % 2 == 1 else int(k_raw) + 1) + self.avg_pool = get_image_layer("AdaptiveAvgPool", rank)(1) + self.conv1d = torch.nn.Conv1d(1, 1, kernel_size=k, padding=k // 2, bias=False) + self.sigmoid = torch.nn.Sigmoid() + + def forward(self, x: torch.Tensor) -> torch.Tensor: + """ + Forward pass of the Efficient Channel Attention layer. + + Args: + x (torch.Tensor): Input tensor of shape (batch_size, channels, *spatial_dims). + + Returns: + torch.Tensor: Output tensor of shape (batch_size, channels, *spatial_dims) with channel-wise attention applied. + """ + B, C = x.shape[:2] + w = self.avg_pool(x).view(B, 1, C) # [B, 1, C] + w = self.conv1d(w) # [B, 1, C] + out_shape = [B, C] + [1] * self.rank + w = self.sigmoid(w).view(*out_shape) + return x * w + +@api_util.export("layers.SqueezeExcite") +@register_with_ranks("SqueezeExcite", ranks=(1, 2, 3)) +class SqueezeExcite(torch.nn.Module): + """ + Squeeze-and-Excitation (SE) layer that adaptively recalibrates channel-wise feature responses. + + This operation is useful for improving the representational power of convolutional neural networks by explicitly modeling interdependencies between channels[1]. + + Args: + channels (int): Number of input channels. + rank (int): The rank of the input tensor (1 for 1D, 2 for 2D, 3 for 3D). + reduction (int): Reduction ratio for the hidden layer in the SE block. Default is 8. + + References: + .. [1] J. Hu, L. Shen, and G. Sun, Squeeze-and-Excitation Networks, + Mar. 27, 2018, arXiv: arXiv:1709.01507. doi: 10.48550/arXiv.1709.01507. + """ + def __init__(self, channels: int, rank: int, reduction: int = 8): + super().__init__() + self.rank = rank + hidden = max(8, channels // reduction) + self.avg_pool = get_image_layer("AdaptiveAvgPool", rank)(1) + self.fc = torch.nn.Sequential( + torch.nn.Linear(channels, hidden), + torch.nn.ReLU(inplace=True), + torch.nn.Linear(hidden, channels), + ) + self.sigmoid = torch.nn.Sigmoid() + + def forward(self, x: torch.Tensor) -> torch.Tensor: + """ + Forward pass of the Efficient Channel Attention layer. + + Args: + x (torch.Tensor): Input tensor of shape (batch_size, channels, *spatial_dims). + + Returns: + torch.Tensor: Output tensor of shape (batch_size, channels, *spatial_dims) with channel-wise attention applied. + """ + B, C = x.shape[:2] + z = self.avg_pool(x).view(B, C) + out_shape = [B, C] + [1] * self.rank + s = self.sigmoid(self.fc(z)).view(*out_shape) + return x * s + +@api_util.export("layers.ConditionedSqueezeExcite") +@register_with_ranks("ConditionedSqueezeExcite", ranks=(1, 2, 3)) +class ConditionedSqueezeExcite(torch.nn.Module): + """ + Conditioned Squeeze-and-Excitation [1] (SE) layer that adaptively recalibrates channel-wise feature responses based on an additional conditioning input. + + This operation is useful for improving the representational power of convolutional neural networks by explicitly modeling interdependencies between channels, + while also allowing for external conditioning information to influence the recalibration process. + + Args: + channels (int): Number of input channels. + rank (int): The rank of the input tensor (1 for 1D, 2 for 2D, 3 for 3D). + n_cond (int): Number of conditioning channels. Default is 6. + reduction (int): Reduction ratio for the hidden layer in the SE block. Default is 8. + mode (str): Mode of combining the feature and conditioning information. Can be "concat" or "add". Default is "add". + + References: + .. [1] J. Hu, L. Shen, and G. Sun, Squeeze-and-Excitation Networks, + Mar. 27, 2018, arXiv: arXiv:1709.01507. doi: 10.48550/arXiv.1709.01507. + + """ + def __init__( + self, + channels: int, + rank: int, + n_cond: int = 6, + reduction: int = 8, + mode: str = "add" + ): + super().__init__() + self.rank = rank + if mode not in ("concat", "add"): + raise ValueError(f"unknown se_cond_mode {mode!r}") + self.mode = mode + hidden = max(8, channels // reduction) + self.avg_pool = get_image_layer("AdaptiveAvgPool", self.rank)(1) + if mode == "concat": + self.fc = torch.nn.Sequential( + torch.nn.Linear(channels + n_cond, hidden), + torch.nn.ReLU(inplace=True), + torch.nn.Linear(hidden, channels), + ) + else: # add + self.feat_fc = torch.nn.Linear(channels, hidden) + self.cond_fc = torch.nn.Linear(n_cond, hidden) + self.act = torch.nn.ReLU(inplace=True) + self.out_fc = torch.nn.Linear(hidden, channels) + self.sigmoid = torch.nn.Sigmoid() + + def forward(self, x: torch.Tensor, cond: torch.Tensor) -> torch.Tensor: + """ + Forward pass of the Conditioned Squeeze-and-Excitation layer. + + Args: + x (torch.Tensor): Input tensor of shape (batch_size, channels, *spatial_dims). + cond (torch.Tensor): Conditioning tensor of shape (batch_size, n_cond). + + Returns: + torch.Tensor: Output tensor of shape (batch_size, channels, *spatial_dims) with channel-wise attention applied based on the conditioning input. + """ + B, C = x.shape[:2] + z = self.avg_pool(x).view(B, C) + if self.mode == "concat": + s = self.fc(torch.cat([z, cond], dim=1)) + else: + s = self.out_fc(self.act(self.feat_fc(z) + self.cond_fc(cond))) + + out_shape = [B, C] + [1] * self.rank + s = self.sigmoid(s).view(*out_shape) + return x * s diff --git a/im2sim/src/layers/graph_blocks.py b/im2sim/src/layers/graph_blocks.py new file mode 100644 index 0000000..38d066c --- /dev/null +++ b/im2sim/src/layers/graph_blocks.py @@ -0,0 +1,341 @@ +import logging +from copy import copy + +import torch +from torch import nn + +from im2sim.src.layers.layer_util import get_activation, get_graph_layer, register_graph_layer +from im2sim.src.utils import api_util + +logger = logging.getLogger(__name__) + +@api_util.export("layers.DefaultGraphNorm") +@register_graph_layer(name="defaultnorm") +class DefaultGraphNorm(torch.nn.Module): + """ + The default normalisation for im2sim graph blocks. + Uses torch.nn.InstanceNorm2d applied to graph data, but all channels are normalised together. + + Args: + None + + """ + + def __init__(self): + super().__init__() + self.norm = torch.nn.InstanceNorm2d(1, affine=True, eps=1e-3) + + def forward(self, x, batch): + """ + Args: + x (`torch.Tensor`): The input tensor of shape (N, C) where N is the number of nodes and C is the number of channels. + batch (`torch.Tensor`): The batch tensor of shape (N,) indicating the batch index for each node. + + Returns: + `torch.Tensor`: The normalized tensor of shape (N, C). + """ + if x.dim() != 2: + raise RuntimeError(f"Expected x.dim()==2, got {x.dim()}") + shape = x.shape + + if batch is None: + batch = torch.zeros(x.shape[0], dtype=torch.long, device=x.device) + + for b in torch.unique(batch): + x[batch == b] = self.norm(x[batch == b].unsqueeze(0).unsqueeze(0)).reshape(shape) + return x + +@api_util.export("layers.GraphConvBlock") +class GraphConvBlock(nn.Module): + """ + A convolutional block for graph data + + Args: + in_channels (int): The number of channels in the input to the layer. + filters (int, optional): The number of filters in each convolutional layer (default: 32) + depth (int, optional): The number of successive convolutional layers (default: 2) + conv_type (str, optional): The type of graph convolution to apply (default: "GATConv", options: All PyG Convs) + conv_kwargs(dict, optional): Dictionary of keyword arguments for the chosen conv_type + activation (str, optional): The activation function applied after each convolution (default: "relu", options: All torch activations) + norm_type (str, optional): The normalization method to apply between convolutions (default:"defaultnorm", options: All PyG Norms) + + Returns: + A `torch.nn.Module` object. + + """ + + def __init__( + self, + in_channels, + filters, + depth=1, + conv_type="GATConv", + conv_kwargs=None, + activation="ReLU", + norm_type="defaultnorm", + norm_kwargs=None, + ): + super().__init__() + + self.convs = nn.ModuleList( + [ + get_graph_layer( + name=conv_type, + args=[in_channels if i == 0 else filters, filters], + kwargs=conv_kwargs, + ) + for i in range(depth) + ] + ) + + self.norms = nn.ModuleList( + [ + get_graph_layer(name=norm_type, kwargs=norm_kwargs) if norm_type else nn.Identity() + for _ in range(depth) + ] + ) + + self.act = get_activation(activation) + + def forward(self, in_graph): + graph = copy(in_graph) + for conv, norm in zip(self.convs, self.norms, strict=True): + graph = conv(graph) + graph = norm(graph) + graph.x = self.act(graph.x) + return graph + +@api_util.export("layers.GraphConvResBlock") +class GraphConvResBlock(nn.Module): + """ + A convolutional block for graph data + + Args: + in_channels (int): The number of channels in the input to the layer. + filters (int, optional): The number of filters in each convolutional layer (default: 32) + depth (int, optional): The number of successive convolutional layers (default: 2) + conv_type (str, optional): The type of graph convolution to apply (default: "ChebConv", options: All PyG Convs) + conv_kwargs(dict, optional): Dictionary of keyword arguments for the chosen conv_type + activation (str, optional): The activation function applied after each convolution (default: "relu", options: All torch activations) + norm_type (str, optional): The normalization method to apply between convolutions (default:"InstanceNorm", options: All PyG Norms) + + Returns: + A `torch.nn.Module` object. + + """ + + def __init__( + self, + in_channels, + filters, + depth=3, + conv_type="GATConv", + conv_kwargs=None, + activation="ReLU", + norm_type="defaultnorm", + norm_kwargs=None, + ): + super().__init__() + + self.convs = nn.ModuleList( + [ + get_graph_layer( + name=conv_type, + args=[in_channels if i == 0 else filters, filters], + kwargs=conv_kwargs, + ) + for i in range(depth) + ] + ) + + self.norms = nn.ModuleList( + [ + get_graph_layer(name=norm_type, kwargs=norm_kwargs) if norm_type else nn.Identity() + for _ in range(depth) + ] + ) + + self.act = get_activation(activation) + + def forward(self, in_graph): + graph = copy(in_graph) + for i, (conv, norm) in enumerate(zip(self.convs, self.norms, strict=True)): + graph = norm(conv(graph)) + + if i == 0: + x1 = graph.x + elif i < len(self.convs) - 1: + graph.x = self.act(graph.x) + + graph.x = self.act((graph.x + x1) / 2) + + return graph + +@api_util.export("layers.GraphResDecoderBlock") +class GraphResDecoderBlock(nn.Module): + """ + A graph convolutional decoder block with the same structure as MeshDeformNet and Image2Flow + + Args: + encoder_channels (List[int]): The number of channels projected from the encoder to each decoder level (len=n_decoder_levels) + out_channels (int): The number of output channels including node coordinates and features + filters (List(List(int)), optional): The number of convolutional filters for each level (default:[[384,288], [144,96], [64,32]]) + res_block_depth (int, optional): The number of successive convolutions in each residual block (default: 3) + n_process_blocks (int, optional): The number of residual blocks prior to projection(default: 1) + n_deform_blocks (int, optional): The number of residual blocks after projection(default: 3) + template_edge_index (torch.Tensor, optional): If template tensor is the fixed it can be passed (default: None) + conv_type (str, optional): The type of graph convolution to apply (default: "ChebConv", options: All PyG Convs) + conv_kwargs(dict, optional): Dictionary of keyword arguments for the chosen conv_type + activation (str, optional): The activation function applied after each convolution (default: "relu", options: All torch activations) + out_activation (str, optional): The activation function applied after each convolution (default: "linear", options: All torch activations) + norm_type (str, optional): The normalization method to apply between convolutions (default:"InstanceNorm", options: All PyG Norms) + + Returns: + A `torch.nn.Module` object. + + """ + + def __init__( + self, + projection_channels, + graph_channels, + out_channels, + filters, + res_depth=3, + n_deform_blocks=3, + template_edge_index=None, + conv_type="GATConv", + conv_kwargs=None, + activation="relu", + out_activation="linear", + norm_type="defaultnorm", + ): + super().__init__() + + self.process_conv = GraphConvBlock( + in_channels=graph_channels, + filters=filters[0], + depth=1, + conv_type=conv_type, + conv_kwargs=conv_kwargs, + activation=activation, + norm_type=None, + ) + + self.deform_conv = nn.ModuleList( + [ + GraphConvResBlock( + in_channels=filters[0] + projection_channels + out_channels + if i == 0 + else filters[1], + filters=filters[1], + depth=res_depth, + conv_type=conv_type, + conv_kwargs=conv_kwargs, + activation=activation, + norm_type=norm_type, + ) + for i in range(n_deform_blocks) + ] + ) + + self.out_conv = GraphConvBlock( + in_channels=filters[1], + filters=out_channels, + depth=1, + conv_type=conv_type, + conv_kwargs=conv_kwargs, + activation=out_activation, + norm_type=None, + ) + + self.edge_index = template_edge_index + + def forward(self, in_graph, prev_results, encoder_projection): + if in_graph.edge_index is None and self.edge_index is not None: + in_graph.edge_index = self.edge_index + + graph = copy(in_graph) + graph = self.process_conv(graph) + graph.x = torch.cat([graph.x, encoder_projection, prev_results], axis=-1) + + for dconv in self.deform_conv: + graph = dconv(graph) + + new_results = self.out_conv(graph).x + prev_results + return graph, new_results + + +# class GraphUNetDecoderBlock(nn.Module): + +# def __init__(self, +# #in_channels, +# out_channels, +# filters, +# domain_size, +# res_depth = 3, +# n_align_blocks = 1, +# n_deform_blocks = 3, +# conv_type="ChebConv", +# conv_kwargs={'K':3}, +# activation="relu", +# out_activation="linear", +# norm_type="InstanceNorm", +# batched_ops = True): +# super().__init__() + +# conv_config = dict(depth=res_depth, +# conv_type=conv_type, +# conv_kwargs=conv_kwargs, +# activation=activation, +# norm_type=norm_type) + + +# if n_align_blocks > 0: +# self.align=True +# self.align_conv = gnn.Sequential('x, edge_index, batch',[ +# (GraphConvResBlock(in_channels=out_channels*2 if i==0 else filters, +# filters=filters, +# **conv_config), 'x, edge_index -> x') +# for i in range(n_align_blocks) +# ]) +# else: +# self.align=False + + +# self.deform_conv = gnn.Sequential('x, edge_index, batch',[ +# (GraphConvResBlock(in_channels=out_channels+filters if i==0 else filters, +# filters=filters, +# **conv_config), 'x, edge_index -> x') +# for i in range(n_deform_blocks) +# ]) + +# self.convert_conv = GraphConvBlock(in_channels=filters, +# filters=out_channels, +# depth=1, +# conv_type=conv_type, +# conv_kwargs=conv_kwargs, +# activation=out_activation, +# norm_type=None) + +# self.projection_args = {"domain_size":domain_size, "batch_ops":batched_ops} + +# # INFO: removed graph features for now may want to add back +# def forward(self,image_features,prev_deformation,template_x,edge_index,batch): + +# # Move all zero points after unpooling +# if self.align: +# x = torch.cat([prev_deformation, template_x], axis=-1) +# x = self.align_conv(x, edge_index) +# x = self.convert_conv(x, edge_index) +# prev_deformation = prev_deformation+x + +# # apply current deformation to template +# x = template_x + prev_deformation +# proj = TrilinearProjection(**self.projection_args)(image_features, x[:,:3], batch) +# x = torch.cat([x, proj], axis=-1) + +# # get new deformations based on current position and projections +# x = self.deform_conv(x, edge_index) +# x = self.convert_conv(x, edge_index) +# return x+prev_deformation diff --git a/im2sim/src/layers/halfunet.py b/im2sim/src/layers/halfunet.py new file mode 100644 index 0000000..2372a87 --- /dev/null +++ b/im2sim/src/layers/halfunet.py @@ -0,0 +1,434 @@ +from dataclasses import dataclass, field, fields +from copy import deepcopy +import torch +from im2sim.src.layers.image_conv_blocks import ImageConvBlock, ImageConvBlockConfig +from im2sim.src.layers.layer_util import ( + ResidualConnectionType, + apply_residual_connection, + get_image_layer, +) +from im2sim.src.layers.module_config import Config, ConfigurableModule, LayerConfig, register_config +from im2sim.src.utils import api_util + + +@api_util.export('configs.HalfUNetConfig') +@register_config +@dataclass +class HalfUNetConfig(Config): + """ + Configuration class for defining the parameters of a half U-Net architecture (see im2sim.models.HalfUNet). + + Attributes can either be set directly when creating an instance of the class or modified later. + + Configuration presets can be applied to quickly set up common configurations for different use cases. + + The configuration can also be saved to and loaded from a YAML file. + + Args: + + hidden_channels (int): + Number of channels in the hidden layers. Default is 64. + + num_downsamples (int): + Number of downsampling operations in the encoder. Default is 4. + + pool_spec (LayerConfig | list[LayerConfig]): + Specification for the pooling layers. Default is a MaxPool layer with kernel size 2 for all levels. + + upsample_spec (LayerConfig | list[LayerConfig]): + Specification for the upsampling layers. Default is an Upsample layer with scale factor 2 and mode 'trilinear' for all levels. + The mode is automatically changed to 'bilinear' for 2D data and 'nearest' for 1D. + + block_cfg (ImageConvBlockConfig): + Configuration for the convolutional blocks. Default is a standard convolutional block with 2 layers, ReLU activation, and batch normalization. + + blocks_per_level (int): + Number of convolutional blocks per level in the encoder. Default is 2. + + out_activation (str | None): + Activation function for the output layer. Default is None, which means no activation is applied. + + stem_block_cfg (ImageConvBlockConfig | None): + Configuration for the stem block. If None, it defaults to a single convolutional block with the same configuration as `block_cfg`. + + encoder_block_cfg (list[ImageConvBlockConfig] | ImageConvBlockConfig | None): + Configuration for the encoder blocks. If None, it defaults to a list of `block_cfg` repeated for each downsampling level. + + out_block_cfg (ImageConvBlockConfig | None): + Configuration for the output block. If None, it defaults to a single convolutional block with the same configuration as `block_cfg` and the specified `out_activation`. + + fusion_type (ResidualConnectionType): + Type of residual connection to use in the network. It can be either 'add' (default), 'concat' or 'average'. + This determines how the encoder features are fused. + + Examples: + + To create a HalfUNet model for single class segmentation, you can do the following: + + >>> cfg = HalfUNetConfig(num_downsamples=3, hidden_channels=64) + >>> cfg = cfg.apply_presets(["single_class_segmentation", "residual", "SE", "ghost_depthwise_separable"]) + >>> model = HalfUNet.build(in_channles=20, out_channels=1, rank=3, cfg=cfg) + + For more presets, see the Preset Library below. + + """ + hidden_channels: int = 64 + num_downsamples: int = 4 + pool_spec: LayerConfig | list[LayerConfig] = field( + default_factory=lambda: LayerConfig(name="MaxPool", kwargs={"kernel_size": 2}) + ) + upsample_spec: LayerConfig | list[LayerConfig] = field( + default_factory=lambda: LayerConfig( + name="Upsample", kwargs={"scale_factor": 2, "mode": "trilinear"} + ) + ) + block_cfg: ImageConvBlockConfig = field(default_factory=lambda: ImageConvBlockConfig()) + blocks_per_level: int = 2 + out_activation: str | None = None + stem_block_cfg: ImageConvBlockConfig | None = None + encoder_block_cfg: list[ImageConvBlockConfig] | ImageConvBlockConfig | None = None + out_block_cfg: ImageConvBlockConfig | None = None + fusion_type: ResidualConnectionType = ResidualConnectionType.ADD + + def __post_init__(self): + print("HalfUNetConfig __post_init__ called") + if self.stem_block_cfg is None: + self.stem_block_cfg = self.block_cfg.apply_presets(["single_block"]) + if self.stem_block_cfg.out_activation is None: + self.stem_block_cfg.out_activation = self.stem_block_cfg.activation + + if self.encoder_block_cfg is None: + self.encoder_block_cfg = deepcopy(self.block_cfg) + if self.encoder_block_cfg.out_activation is None: + self.encoder_block_cfg.out_activation = self.encoder_block_cfg.activation + self.encoder_block_cfg = [self.encoder_block_cfg] * self.num_downsamples + + elif isinstance(self.encoder_block_cfg, ImageConvBlockConfig): + self.encoder_block_cfg = [self.encoder_block_cfg] * self.num_downsamples + + if self.out_block_cfg is None: + self.out_block_cfg = self.block_cfg.apply_presets(["single_conv"]) + self.out_block_cfg.out_activation = self.out_activation + + +@api_util.export('models.HalfUNet') +class HalfUNet(torch.nn.Module, ConfigurableModule): + """ + A Half-UNet[1] architecture for image processing tasks. + + Args: + in_channels (int): Number of input channels. + out_channels (int): Number of output channels. + rank (int): Dimensionality of the input data (1 for 1D, 2 for 2D, 3 for 3D). + hidden_channels (int): Number of channels in the hidden layers. Default is 64. + num_downsamples (int): Number of downsampling operations in the encoder. Default is 4. + pool_spec (LayerConfig | list[LayerConfig]): Specification for the pooling layers. Default is a MaxPool layer with kernel size 2 for all levels. + upsample_spec (LayerConfig | list[LayerConfig]): Specification for the upsampling layers. Default is an Upsample layer with scale factor 2 and mode 'trilinear' for all levels. + block_cfg (ImageConvBlockConfig): Configuration for the convolutional blocks. Default is a standard convolutional block with 2 layers, ReLU activation, and batch normalization. + blocks_per_level (int): Number of convolutional blocks per level in the encoder. Default is 2. + out_activation (str | None): Activation function for the output layer. Default is None, which means no activation is applied. + stem_block_cfg (ImageConvBlockConfig | None): Configuration for the stem block. If None, it defaults to a single convolutional block with the same configuration as `block_cfg`. + encoder_block_cfg (list[ImageConvBlockConfig] | ImageConvBlockConfig | None): Configuration for the encoder blocks. If None, it defaults to a list of `block_cfg` repeated for each downsampling level. + out_block_cfg (ImageConvBlockConfig | None): Configuration for the output block. If None, it defaults to a single convolutional block with the same configuration as `block_cfg` and the specified `out_activation`. + fusion_type (ResidualConnectionType): Type of residual connection to use in the network. It can be either 'add' (default), 'concat' or 'average'. This determines how the encoder features are fused. + + The best way to build a HalfUNet is to use the `im2sim.configs.HalfUNetConfig` class to define the configuration and then call the `build` method. + + Example: + + To create a HalfUNet model for single class segmentation, you can do the following: + + >>> cfg = HalfUNetConfig(num_downsamples=3, hidden_channels=64) + >>> cfg = cfg.apply_presets(["single_class_segmentation", "residual", "SE", "ghost_depthwise_separable"]) + >>> model = HalfUNet.build(in_channles=20, out_channels=1, rank=3, cfg=cfg) + + References: + .. [1] H. Lu, Y. She, J. Tie, and S. Xu, Half-UNet: A Simplified U-Net Architecture for Medical Image Segmentation, + Front. Neuroinformatics, vol. 16, Jun. 2022, doi: 10.3389/fninf.2022.911679. + """ + def __init__( + self, + in_channels: int, + out_channels: int, + rank: int, + hidden_channels: int = 64, + num_downsamples: int = 4, + pool_spec: LayerConfig | list[LayerConfig] | None = None, + upsample_spec: LayerConfig | list[LayerConfig] | None = None, + block_cfg: ImageConvBlockConfig | None = None, + blocks_per_level: int = 2, + out_activation: str | None = None, + stem_block_cfg: ImageConvBlockConfig | None = None, + encoder_block_cfg: list[ImageConvBlockConfig] | ImageConvBlockConfig | None = None, + out_block_cfg: ImageConvBlockConfig | None = None, + fusion_type: ResidualConnectionType = ResidualConnectionType.ADD, + ): + """ """ + super().__init__() + + self.in_channels = in_channels + self.out_channels = out_channels + self.hidden_channels = hidden_channels + self.num_downsamples = num_downsamples + + if pool_spec is None: + pool_spec = LayerConfig(name="MaxPool", kwargs={"kernel_size": 2}) + if upsample_spec is None: + mode = "nearest" if rank == 1 else "bilinear" if rank == 2 else "trilinear" + upsample_spec = LayerConfig(name="Upsample", kwargs={"scale_factor": 2, "mode": mode}) + if block_cfg is None: + block_cfg = ImageConvBlockConfig() + + if isinstance(pool_spec, LayerConfig): + pool_spec = [pool_spec] * num_downsamples + if isinstance(upsample_spec, LayerConfig): + upsample_spec = [upsample_spec] * num_downsamples + + for p, u in zip(pool_spec, upsample_spec, strict=True): + assert p.name.lower() in ["maxpool", "averagepool"], f"pool type {p.name} not supported" + assert u.name.lower() in ["upsample", "pixelshuffle"], ( + f"upsample type {u.name} not supported" + ) + + self.pools = torch.nn.ModuleList( + [get_image_layer(pool.name, rank)(**pool.kwargs) for pool in pool_spec] + ) + self.ups = torch.nn.ModuleList( + [get_image_layer(upsample.name, rank)(**upsample.kwargs) for upsample in upsample_spec] + ) + + if stem_block_cfg is None: + stem_block_cfg = block_cfg.apply_presets(["single_block"]) + + self.stem = ImageConvBlock.build(rank, in_channels, hidden_channels, stem_block_cfg) + print(type(self.stem)) + + if encoder_block_cfg is None: + encoder_block_cfg = [block_cfg] * num_downsamples + elif isinstance(encoder_block_cfg, ImageConvBlockConfig): + encoder_block_cfg = [encoder_block_cfg] * num_downsamples + + self.encoder_blocks = torch.nn.ModuleList( + [ + torch.nn.Sequential( + *[ImageConvBlock.build(rank, hidden_channels, hidden_channels, cfg)] + * blocks_per_level + ) + for cfg in encoder_block_cfg + ] + ) + + if out_block_cfg is None: + out_block_cfg = block_cfg.apply_presets(["single_conv"]) + out_block_cfg.out_activation = out_activation + + self.fusion_type = fusion_type + fusion_channels = ( + hidden_channels * (num_downsamples + 1) + if self.fusion_type is ResidualConnectionType.CONCAT + else hidden_channels + ) + self.out_block = ImageConvBlock.build( + rank, fusion_channels, out_channels, out_block_cfg + ) + + def forward(self, x): + """ + Forward pass of the HalfUNet model. + """ + x = self.stem(x) + + residuals = [] + for pool, block in zip(self.pools, self.encoder_blocks, strict=True): + residuals.append(x) + x = pool(x) + x = block(x) + + fused = x + for res, up in zip(reversed(residuals), self.ups, strict=True): + fused = up(fused) + fused = apply_residual_connection(fused, res, connection_type=self.fusion_type) + + out = self.out_block(fused) + return out + + + + +@HalfUNetConfig.register_preset("residual") +def half_unet_residual_type(cfg: HalfUNetConfig): + """ + Apply a residual connection to all encoder blocks in the HalfUNet configuration. + + The residual connection type is set to "add" for all encoder blocks, + which means that the output of each encoder block will be added to its input before being passed to the next layer. + This can help with gradient flow and improve training stability. + """ + cfg.encoder_block_cfg = [ + c.apply_presets(["0_residual"]) for c in cfg.encoder_block_cfg + ] + return cfg + + +@HalfUNetConfig.register_preset("dilated_bottleneck") +def unet_residual_type(cfg: HalfUNetConfig): + """ + Apply dilated convolutions to the bottleneck (lowest resolution) block in the HalfUNet configuration. + + This change modifies the last encoder block to use dilated convolutions, which can help increase the receptive field without increasing the number of parameters. + """ + cfg.encoder_block_cfg[-1] = cfg.encoder_block_cfg[-1].apply_presets(["dilated_convs"] + ) + return cfg + + +def _apply_preset_to_all_blocks(cfg: HalfUNetConfig, preset_name: str): + """ + Apply a given preset to all blocks in the HalfUNet configuration. + """ + cfg.stem_block_cfg = cfg.stem_block_cfg.apply_presets([preset_name]) + cfg.encoder_block_cfg = [c.apply_presets([preset_name]) for c in cfg.encoder_block_cfg] + cfg.out_block_cfg = cfg.out_block_cfg.apply_presets([preset_name]) + return cfg + + +@HalfUNetConfig.register_preset("recon") +def reconstruction_config(cfg: HalfUNetConfig): + """ + Apply a reconstruction preset to all blocks in the HalfUNet configuration. + + This preset is typically used for image reconstruction or superresolution tasks, + where the output is expected to be a continuous value (e.g., pixel intensity). + """ + _apply_preset_to_all_blocks(cfg, "recon") + cfg.out_activation = None + return cfg + + +@HalfUNetConfig.register_preset("single_class_segmentation") +def segmentation_config(cfg: HalfUNetConfig): + """ + Apply a single-class segmentation preset to all blocks in the HalfUNet configuration. + + Use this preset for binary segmentation tasks, where the output is expected to be a probability map for a single class. + """ + _apply_preset_to_all_blocks(cfg, "segmentation") + cfg.out_block_cfg.out_activation = "sigmoid" + return cfg + +@HalfUNetConfig.register_preset("multiclass_segmentation") +def segmentation_config(cfg: HalfUNetConfig): + """ + Apply a multi-class segmentation preset to all blocks in the HalfUNet configuration. + + Use this preset for multi-class segmentation tasks, where the output is expected to be a probability map for multiple classes. + """ + _apply_preset_to_all_blocks(cfg, "segmentation") + cfg.out_block_cfg.out_activation = "softmax" + return cfg + + +@HalfUNetConfig.register_preset("depthwise_separable") +def depthwise_separable_config(cfg: HalfUNetConfig): + """ + Apply a depthwise separable convolution (see `im2sim.layers.DepthwiseSeparableConv`) preset to all encoder blocks in the HalfUNet configuration. + """ + _apply_preset_to_all_blocks(cfg, "depthwise_separable") + return cfg + + +@HalfUNetConfig.register_preset("ghost_depthwise") +def ghost_dw_config(cfg: HalfUNetConfig): + """ + Apply a ghost depthwise convolution (see `im2sim.layers.GhostConv`) preset to all encoder blocks in the HalfUNet configuration. + """ + _apply_preset_to_all_blocks(cfg, "ghost_depthwise") + return cfg + + +@HalfUNetConfig.register_preset("ghost_depthwise_separable") +def ghost_dws_config(cfg: HalfUNetConfig): + """ + Apply a ghost depthwise separable convolution (see `im2sim.layers.GhostConv`) preset to all encoder blocks in the HalfUNet configuration. + """ + _apply_preset_to_all_blocks(cfg, "ghost_depthwise_separable") + return cfg + + +@HalfUNetConfig.register_preset("ECA") +def eca_config(cfg: HalfUNetConfig): + """ + Apply an Efficient Channel Attention (ECA) (see `im2sim.layers.EfficientChannelAttn`) preset to all encoder blocks in the HalfUNet configuration. + """ + cfg.encoder_block_cfg = [ + c.apply_presets(["ECA"]) for c in cfg.encoder_block_cfg + ] + return cfg + + +@HalfUNetConfig.register_preset("SE") +def squeeze_excitation_config(cfg: HalfUNetConfig): + """ + Apply a Squeeze-and-Excitation (SE) (see `im2sim.layers.SqueezeExcite`) preset to all encoder blocks in the HalfUNet configuration. + """ + cfg.encoder_block_cfg = [ + c.apply_presets(["SE"]) for c in cfg.encoder_block_cfg + ] + return cfg + + +if __name__ == "__main__": + + def cfg_print(cfg): + print("###################") + for field in fields(cfg): + value = getattr(cfg, field.name) + print(f" {field.name}: {value}") + + cfg = HalfUNetConfig(num_downsamples=3, hidden_channels=64) + print(cfg.generate_preset_docs()) + # cfg = cfg.apply_presets(["single_class_segmentation", "residual", "SE", "ghost_depthwise_separable"]) + # model = HalfUNet.build( + # rank=3, + # in_channels=20, + # out_channels=1, + # cfg=cfg, + # ) + # total_params = sum(p.numel() for p in model.parameters()) + # print(f"Parameters: {total_params:,}") + + # print(model) + + # x = torch.randn(1, 20, 64, 64, 64) # Example input for a 3D tensor + + # def check_gradients(model, x): + # model.train() + + # # Ensure input tracks gradients if you want to test input gradients + # x = x.requires_grad_(True) + + # # Forward + # y = model(x) + + # # Use a scalar loss + # loss = y.sum() + + # # Backward + # loss.backward() + + # # Check input gradient + # assert x.grad is not None, "Input gradient is None" + # assert torch.isfinite(x.grad).all(), "Input gradient contains NaN/Inf" + + # # Check parameter gradients + # for name, param in model.named_parameters(): + # if param.requires_grad: + # assert param.grad is not None, f"{name} gradient is None" + # assert torch.isfinite(param.grad).all(), f"{name} gradient contains NaN/Inf" + # assert param.grad.abs().sum() > 0, f"{name} gradient is zero" + + # return True + + # # Check gradients + # if check_gradients(model, x): + # print("Gradient check passed.") diff --git a/im2sim/src/layers/image_conv_blocks.py b/im2sim/src/layers/image_conv_blocks.py new file mode 100644 index 0000000..c7fcd4b --- /dev/null +++ b/im2sim/src/layers/image_conv_blocks.py @@ -0,0 +1,449 @@ +from dataclasses import dataclass, field + +import torch +from im2sim.src.layers.custom_image_layers import * +from im2sim.src.layers.layer_util import ( + ResidualConnectionType, + apply_residual_connection, + get_activation, + get_image_layer, +) +from im2sim.src.layers.module_config import Config, ConfigurableModule, LayerConfig, register_config +from im2sim.src.utils import api_util + + +@api_util.export("configs.ImageConvBlockConfig") +@register_config +@dataclass +class ImageConvBlockConfig(Config): + """ + Configuration class for defining the parameters of an image convolutional block. + + Attributes can either be set directly when creating an instance of the class or modified later. + + Configuration presets can be applied to quickly set up common configurations for different use cases. + + The configuration can also be saved to and loaded from a YAML file. + + Args: + + depth (int): + The number of convolutional layers in the block. Default is 2. + + activation (str | None): + The activation function to use after each convolutional layer. Default is "ReLU". + + out_activation (str | None): + The activation function to use after the final layer. Default is None. + + conv_config (LayerConfig): + Configuration for the convolutional layers, including kernel size and padding. + + norm_config (LayerConfig): + Configuration for the normalization layers, such as InstanceNorm with affine set to True. + + dropout_config (LayerConfig): + Configuration for the dropout layers. Default is no dropout. + + attn_config (LayerConfig): + Configuration for the attention layers. Default is no attention. + + dropout_position (int | list[int]): + Specifies the position(s) of the dropout layers within the block. Default is 1. + + residual_connections ( dict [int, list[ int ]] | None): + Specifies the residual connections within the block. + The keys represent the target layers, and the values are lists of source layers. Default is None. + Example: {1: [0]} means that the input to the block will be added to the output of layer 1. + + residual_type (str): + The type of residual connection to use (e.g., "add"). Default is ResidualConnectionType.ADD. + + Examples: + + To create a configuration for an image convolutional block with a depth of 3, ReLU activation, and softmax output activation, you can use the following code: + + >>> cfg = ImageConvBlockConfig(depth=3, activation="ReLU", out_activation="softmax") + + + To apply a preset configuration for a single convolutional layer without normalization or dropout, you can use: + + >>> cfg = ImageConvBlockConfig.apply_presets(cfg, ["single_conv"]) + + To save the configuration to a YAML file and load it back, you can use: + + >>> cfg.save("config.yaml") + >>> loaded_cfg = ImageConvBlockConfig().load("config.yaml") + + """ + + depth: int = 2 + activation: str | None = "ReLU" + out_activation: str | None = None + conv_config: LayerConfig = field( + default_factory=lambda: LayerConfig( + name="Conv", kwargs={"kernel_size": 3, "padding": "same"} + ) + ) + norm_config: LayerConfig = field( + default_factory=lambda: LayerConfig(name="InstanceNorm", kwargs={"affine": True}) + ) + dropout_config: LayerConfig = field(default_factory=lambda: LayerConfig(name=None, kwargs={})) + attn_config: LayerConfig = field(default_factory=lambda: LayerConfig(name=None, kwargs={})) + dropout_position: int | list[int] = 1 + residual_connections: dict[int, list[int]] = None + residual_type: str = ResidualConnectionType.ADD + + +@api_util.export("layers.ImageConvBlock") +class ImageConvBlock(torch.nn.Module, ConfigurableModule): + """ + A configurable image convolutional block that consists of a sequence of + convolutional layers, normalization layers, dropout layers, and attention + layers. The block supports residual connections and allows for flexible + configuration of its components. + + It is best used by creating a configuration object of type + :class:ImageConvBlockConfig and then calling the build method to create + an instance of the block. + + Args: + + in_channels : int + Number of input channels. + + out_channels : int + Number of output channels. + + rank : int + The rank of the convolutional layers (e.g., 2 for 2D convolutions). + + depth : int, default=2 + The number of convolutional layers in the block. + + activation : str | None, default=None + Activation function applied after each convolutional layer. + + out_activation : str | None, default=None + Activation function applied after the final layer. + + conv_config : LayerConfig | None, default=None + Configuration for convolutional layers. If None, a default configuration is used. + + norm_config : LayerConfig | None, default=None + Configuration for normalization layers. If None, a default configuration is used. + + attn_config : LayerConfig | None, default=None + Configuration for attention layers. If None, a default configuration is used. + + dropout_config : LayerConfig | None, default=None + Configuration for dropout layers. If None, a default configuration is used. + + dropout_position : int | list[int], default=1 + Position(s) of dropout layers within the block. + + residual_connections : dict[int, list[int]] | None, default=None + Specifies residual connections within the block. Keys represent target layers, + and values are lists of source layers.(e.g. {1: [0]} adds the block input to the output of layer 1.) + + residual_type : str, default=ResidualConnectionType.ADD + Type of residual connection to use (e.g., "add"). + + + + Example: + + To create an ImageConvBlock with a depth of 3, ReLU activation, and softmax output activation, you can use the following code: + + >>> cfg = ImageConvBlockConfig(depth=3, activation="ReLU", out_activation="softmax") + >>> model = ImageConvBlock.build( + >>> rank=2, + >>> in_channels=32, + >>> out_channels=32, + >>> cfg=cfg, + >>> ) + """ + def __init__( + self, + in_channels: int, + out_channels: int, + rank: int, + depth: int = 2, + activation: str | None = None, + out_activation: str | None = None, + conv_config: LayerConfig | None = None, + norm_config: LayerConfig | None = None, + attn_config: LayerConfig | None = None, + dropout_config: LayerConfig | None = None, + dropout_position: int | list[int] = 1, + residual_connections: dict[int, list[int]] = None, + residual_type: str = ResidualConnectionType.ADD, + ): + super().__init__() + + print("IN INIT") + print(conv_config) + self.in_channels = in_channels + self.out_channels = out_channels + self.rank = rank + self.depth = depth + self.activation = get_activation(activation) + self.out_activation = get_activation(out_activation) + self.conv_config = conv_config + self.norm_config = norm_config + self.attn_config = attn_config + self.dropout_config = dropout_config + self.dropout_position = ( + dropout_position if isinstance(dropout_position, list) else [dropout_position] + ) + self.residual_connections = residual_connections if residual_connections is not None else {} + self.residual_type = residual_type + + print(self.dropout_position, self.depth) + + self._set_default_configs() + self._validate_configs() + + self.layers = torch.nn.ModuleList() + in_channels_per_layer = [self.in_channels] + + for i in range(depth): + conv = get_image_layer(self.conv_config.name, rank=self.rank)( + in_channels=in_channels_per_layer[-1], + out_channels=out_channels, + **self.conv_config.kwargs, + ) + + norm = get_image_layer(self.norm_config.name, rank=self.rank)( + self.out_channels, **self.norm_config.kwargs + ) + + dropout = ( + get_image_layer(self.dropout_config.name, rank=self.rank)( + **self.dropout_config.kwargs + ) + if i in self.dropout_position + else torch.nn.Identity() + ) + + pre_residual = self.attn_config.name is not None and i in self.residual_connections + no_residual_final = len(self.residual_connections.keys()) == 0 and i == self.depth - 1 + if pre_residual or no_residual_final: + attn = get_image_layer(self.attn_config.name, rank=self.rank)( + self.out_channels, **self.attn_config.kwargs + ) + else: + attn = torch.nn.Identity() + + block = torch.nn.Sequential( + conv, + norm, + dropout, + attn, + self.activation if i < self.depth - 1 else torch.nn.Identity(), + ) + self.layers.append(block) + + in_channels_current = self.out_channels + if ( + i in self.residual_connections + and self.residual_type == ResidualConnectionType.CONCAT + ): + for src in self.residual_connections[i]: + in_channels_current += in_channels_per_layer[src] + in_channels_per_layer.append(in_channels_current) + + def _set_default_configs(self): + if self.conv_config is None: + self.conv_config = LayerConfig( + name="Conv", kwargs={"kernel_size": 3, "padding": "same"} + ) + if self.norm_config is None: + self.norm_config = LayerConfig(name=None, kwargs={}) + if self.dropout_config is None: + self.dropout_config = LayerConfig(name=None, kwargs={}) + if self.attn_config is None: + self.attn_config = LayerConfig(name=None, kwargs={}) + + def _validate_configs(self): + assert self.norm_config.name in [None, "BatchNorm", "InstanceNorm"], ( + f"Unsupported norm type: {self.norm_config.name}" + ) + assert self.attn_config.name in [None, "EfficientChannelAttn", "SqueezeExcite"], ( + f"Unsupported attention type: {self.attn_config.name}" + ) + if self.dropout_config.name is not None: + assert max(self.dropout_position) < self.depth, ( + "Dropout position must be less than depth" + ) + + def forward(self, x): + outputs = [x] + for i, layer in enumerate(self.layers): + x = layer(x) + print(i) + if i in self.residual_connections: + for src in self.residual_connections[i]: + x = apply_residual_connection( + outputs[src], x, connection_type=self.residual_type + ) + outputs.append(x) + + x = self.out_activation(x) + return x + + + +@ImageConvBlockConfig.register_preset("single_conv") +def single_conv_config(cfg: ImageConvBlockConfig): + """ + Converts the blcok into a single convolutional layer with no normalization, dropout, or residual connections. + """ + cfg.depth = 1 + cfg.norm_config = None + cfg.dropout_config = None + cfg.residual_connections = None + cfg.activation = None + return cfg + + +@ImageConvBlockConfig.register_preset("single_block") +def single_block_config(cfg: ImageConvBlockConfig): + """ + Sets the block depth to 1 and removes dropout and residual connections, but keeps normalization and activation. + """ + cfg.depth = 1 + cfg.dropout_config = None + cfg.residual_connections = None + return cfg + + +@ImageConvBlockConfig.register_preset("0_residual") +def half_unet_residual_type(cfg: ImageConvBlockConfig): + """ + Configures the block to have a residual connection from the input to the output of the last layer. + """ + cfg.residual_connections = {cfg.depth - 1: [0]} + cfg.residual_type = ResidualConnectionType.ADD + print("in 0_residual config", cfg) + return cfg + + +@ImageConvBlockConfig.register_preset("1_residual") +def unet_residual_type(cfg: ImageConvBlockConfig): + """ + Configures the block to have a residual connection from the output of the first layer to the output of the last layer. + """ + assert cfg.depth > 1, "Depth must be greater than 1 for 1-residual connections" + cfg.residual_connections = {cfg.depth - 1: [1]} + cfg.residual_type = ResidualConnectionType.ADD + return cfg + + +@ImageConvBlockConfig.register_preset("concat_residual") +def concat_residual_type(cfg: ImageConvBlockConfig): + """ + Configures the block to have a residual connection from the input to the output of the last layer, using concatenation instead of addition. + """ + cfg.residual_connections = {cfg.depth - 1: [0]} + cfg.residual_type = ResidualConnectionType.CONCAT + print(cfg) + return cfg + + +@ImageConvBlockConfig.register_preset("recon") +def reconstruction_config(cfg: ImageConvBlockConfig): + """ + Configures the block for reconstruction tasks by removing normalization and dropout layers. + """ + cfg.norm_config = None + cfg.dropout_config = None + return cfg + + +@ImageConvBlockConfig.register_preset("segmentation") +def segmentation_config(cfg: ImageConvBlockConfig): + """ + Configures the block for segmentation tasks by using InstanceNorm with trainable parameters. + """ + cfg.norm_config = LayerConfig(name="InstanceNorm", kwargs={"affine": True}) + return cfg + + +@ImageConvBlockConfig.register_preset("depthwise_separable") +def depthwise_separable_config(cfg: ImageConvBlockConfig): + """ + Configures the block to use depthwise separable convolutions (see `im2sim.layers.DepthwiseSeparableConv`) instead of standard convolutions. + """ + cfg.conv_config = LayerConfig(name="DepthwiseSeparableConv", kwargs={}) + return cfg + + +@ImageConvBlockConfig.register_preset("ghost_depthwise") +def ghost_depthwise_config(cfg: ImageConvBlockConfig): + """ + Configures the block to use Ghost convolutions instead of standard convolutions. + """ + cfg.conv_config = LayerConfig(name="GhostConv", kwargs={}) + print("in ghost config", cfg) + return cfg + + +@ImageConvBlockConfig.register_preset("ghost_depthwise_separable") +def ghost_separable_config(cfg: ImageConvBlockConfig): + """ + Configures the block to use Ghost depthwise separable convolutions instead of standard convolutions. + """ + cfg.conv_config = LayerConfig(name="GhostConv", kwargs={"separable": True}) + return cfg + + +@ImageConvBlockConfig.register_preset("dilated_convs") +def dilated_convs_config(cfg: ImageConvBlockConfig): + """ + Configures the block to use dilated convolutions with dilation of 2 instead of standard convolutions. + """ + cfg.conv_config.kwargs["dilation"] = 2 + return cfg + + +@ImageConvBlockConfig.register_preset("ECA") +def eca_config(cfg: ImageConvBlockConfig): + """ + Configures the block to use Efficient Channel Attention (ECA) + + + """ + cfg.attn_config = LayerConfig(name="EfficientChannelAttn", kwargs={}) + return cfg + + +@ImageConvBlockConfig.register_preset("SE") +def se_config(cfg: ImageConvBlockConfig): + """ + Configures the block to use Squeeze-and-Excitation (SE) attention + """ + cfg.attn_config = LayerConfig(name="SqueezeExcite", kwargs={}) + print("in SE config", cfg) + return cfg + + +if __name__ == "__main__": + cfg = ImageConvBlockConfig(depth=3, activation="ReLU", out_activation="softmax") + cfg = ImageConvBlockConfig.apply_presets(cfg, ["ghost_depthwise", "0_residual", "SE"]) + cfg.save("test_config.yaml") + cfg2 = ImageConvBlockConfig().load("test_config.yaml") + cfg2.save("test_config2.yaml") + +# model = ImageConvBlock.build( +# rank=2, +# in_channels=32, +# out_channels=32, +# cfg=cfg, +# ) +# # # print(cfg) + +# # print(model) +# # x = torch.randn(1, 32, 64, 64) +# # y = model(x) +# # print(y.shape) diff --git a/im2sim/src/layers/layer_util.py b/im2sim/src/layers/layer_util.py new file mode 100644 index 0000000..cfa788e --- /dev/null +++ b/im2sim/src/layers/layer_util.py @@ -0,0 +1,221 @@ +import inspect +import re +from collections.abc import Callable +from copy import copy +from enum import Enum +from typing import Any + +import torch +import torch_geometric.nn as gnn + + + +def make_registry(lib: Any, regex: Callable): + + registry = NormalizedDict( + {name: getattr(lib, name) for name in dir(lib) if re.search(regex, name)} + ) + + def register(cls=None, *, name=None): + def decorator(obj): + registry[name or obj.__name__] = obj + return obj + + return decorator(cls) if cls else decorator + + return registry, register + + +def normalize_key(key: str) -> str: + return key.replace(" ", "").replace("_", "").lower() + + +class NormalizedDict: + def __init__(self, data: dict): + self._data = {} + for key, val in data.items(): + self.__setitem__(key, val) + + def __setitem__(self, key, value): + self._data[normalize_key(key)] = value + + def __getitem__(self, key): + return self._data[normalize_key(key)] + + def __str__(self): + return self._data.__str__() + + +activation_pattern = re.compile( + r"(ReLU|ELU|LeakyReLU|PReLU|RReLU|GELU|SiLU|" + r"Sigmoid|Tanh|Softmax|Softplus|SELU|CELU|" + r"Threshold|Hardtanh|Hardswish|Mish)" +) +ACTIVATIONS, register_activation = make_registry(torch.nn, activation_pattern) + + +layer_pattern = r"(Conv|Pool|Norm|Upsample|PixelShuffle|Dropout)" + + +IMAGE_LAYERS, register_image_layer = make_registry(torch.nn, layer_pattern) +GRAPH_LAYERS, register_graph_layer = make_registry(gnn, layer_pattern) + + +def register_with_ranks(base_name, ranks=(1, 2, 3)): + def decorator(cls): + for r in ranks: + name = f"{base_name}{r}d" + + layer_cls = type( + name, + (cls,), + { + "__init__": lambda self, *args, _rank=r, **kwargs: cls.__init__( + self, *args, rank=_rank, **kwargs + ) + }, + ) + + register_image_layer(name=name)(layer_cls) + + return cls + + return decorator + + +def get_image_layer(name: str, rank: int) -> torch.nn.Module: + """ + Get a PyTorch layer by name, with optional arguments. + """ + + if name is None: + return torch.nn.Identity + + rank_name = f"{name}{rank}d" + + try: + return IMAGE_LAYERS[rank_name] + except KeyError: + pass + + try: + return IMAGE_LAYERS[name] + except KeyError: + ValueError(f"Layer {name} with rank {rank} not found in PyTorch layers registry") + + +def get_activation(name: str | None) -> torch.nn.Module: + return ACTIVATIONS[name]() if name is not None else torch.nn.Identity() + + +class PyGParameterError(TypeError): + pass + + +class PyGWrapperError(TypeError): + pass + + +def _match_attrs_to_signature(graph, module): + sig = inspect.signature(module.forward) + + attrs = [] + + for name, _param in sig.parameters.items(): + if name == "self": + continue + + if hasattr(graph, name): + value = getattr(graph, name) + + # Ignore methods/functions + if callable(value): + continue + + attrs.append(name) + + return attrs + + +# This will not be useable for pooling layers as they have multiple return Tensors. If requiered we will have to add this functionality. +class PyG_Wrapper(torch.nn.Module): + """ + A wrapper for PyG modules to make the forward method accept and return a PyG Data object instead of separated attributes + Needs to be initialised with a pre-initialised PyG Module. + """ + + def __init__(self, pyg_module: torch.nn.Module): + super().__init__() + self.pyg_module = pyg_module + + def forward(self, graph): + attrs = _match_attrs_to_signature(graph, self.pyg_module) + out = self.pyg_module(**{attr: getattr(graph, attr) for attr in attrs}) + if not isinstance(out, torch.Tensor): + raise PyGWrapperError( + f"PyG layers that have multiple outputs like {self.pyg_module.__class__.__name__} are not currently supported. Consider changing PyG layer or writing a custom wrapper" + ) + + out_graph = copy(graph) + out_graph.x = out + return out_graph + + +def get_graph_layer( + name: str, args: list[Any] = None, kwargs: dict[str, Any] = None +) -> PyG_Wrapper: + + if args is None: + args = [] + if kwargs is None: + kwargs = {} + + module = GRAPH_LAYERS[name](*args, **kwargs) + return PyG_Wrapper(module) + + +def standardize_spatial_factors(factors, rank): + """ + Convert a sequence of spatial factors into a standardized list of tuples. + """ + standardized = [] + + for f in factors: + if isinstance(f, int): + standardized.append(tuple([f] * rank)) + elif isinstance(f, (tuple, list)): + standardized.append(tuple(f)) + else: + raise TypeError(f"Each factor must be an int, tuple, or list, got {type(f).__name__}") + + return standardized + + +class ResidualConnectionType(Enum): + ADD = "add" # Standard addition residual connection + CONCAT = "concat" # Concatenation residual connection + MULTIPLY = "multiply" # Element-wise multiplication residual connection + AVERAGE = "average" # Element-wise average residual connection + + +def apply_residual_connection(*inputs, connection_type: ResidualConnectionType): + if len(inputs) == 0: + raise ValueError("At least one input tensor is required") + + if connection_type == ResidualConnectionType.ADD: + return torch.stack(inputs, dim=0).sum(dim=0) + + elif connection_type == ResidualConnectionType.CONCAT: + return torch.cat(inputs, dim=1) + + elif connection_type == ResidualConnectionType.MULTIPLY: + result = inputs[0] + for x in inputs[1:]: + result = result * x + return result + + elif connection_type == ResidualConnectionType.AVERAGE: + return torch.stack(inputs, dim=0).mean(dim=0) + + else: + raise ValueError(f"Unsupported residual connection type: {connection_type}") diff --git a/im2sim/src/layers/module_config.py b/im2sim/src/layers/module_config.py new file mode 100644 index 0000000..95238d6 --- /dev/null +++ b/im2sim/src/layers/module_config.py @@ -0,0 +1,324 @@ +import json +from enum import Enum +from copy import deepcopy +from dataclasses import dataclass, fields +from typing import Any, TypeVar, get_args, get_origin, get_type_hints +import inspect + +from im2sim.src.utils import api_util + + +T = TypeVar("T", bound="Config") + + +@api_util.export("_internal.Config") +@dataclass +class Config: + """ + Base class for recursively serialisable configuration objects. + + This class provides: + - Recursive (de)serialization to/from dictionaries and JSON + - Preset system for modifying configs declaratively + - Type-aware reconstruction using type hints + + Subclasses should be defined as dataclasses. + + Example: + ```python + cfg = MyConfig(...) + cfg = cfg.apply_presets(["fast", "lightweight"]) + cfg.save("config.json") + + cfg2 = MyConfig().load("config.json") + ``` + """ + + + def __init_subclass__(cls): + """ Automatically initialise a preset registry for each subclass. """ + super().__init_subclass__() + cls._presets = {} + + @classmethod + def register_preset(cls, name): + """ + Register a preset function for this config class. + + A preset is a function that takes a config instance and modifies it. + Args: + name (str): Name of the preset. + Returns: decorator: Function decorator. + + Example: + ```python + @MyConfig.register_preset("small") + def small(cfg): + cfg.hidden_dim = 32 + return cfg + ``` + """ + def decorator(fn): + cls._presets[name] = fn + return fn + return decorator + + def apply_presets(self, names: list[str]): + """ + Apply a sequence of presets to a copy of this config. + + Presets are applied in order. + + Args: + names (list[str]): List of preset names. + + Returns: + Config: Modified config instance. + """ + + cfg = deepcopy(self) + for name in names: + cfg = self._presets[name](cfg) + return cfg + + + def as_kwargs(self): + """ + Convert config fields into keyword arguments. + + Returns: + dict: Mapping of field names to values. + """ + return { + f.name: getattr(self, f.name) + for f in fields(self) + } + + def to_dict(self): + """ + Recursively convert config into a serialisable dictionary. + + Returns: + dict: Serialized representation. """ + return { + "__class__": self.__class__.__name__, + **{ + f.name: self._serialize_value(getattr(self, f.name)) + for f in fields(self) + } + } + + @classmethod + def from_dict(cls: type[T], data: dict[str, Any]) -> T: + """ + Reconstruct a config object from a dictionary. + Uses type hints to correctly deserialize nested configs, enums, lists, etc. + + Args: + data (dict): Serialized config. + + Returns: + Config: Reconstructed config object. """ + + hints = get_type_hints(cls) + + kwargs = {} + + for key, value in data.items(): + if key == "__class__": + continue + + field_type = hints.get(key) + + if field_type is not None: + value = cls._deserialize_value(value, field_type) + + kwargs[key] = value + + return cls(**kwargs) + + + + @staticmethod + def _serialize_value(value): + """ Recursively serialize values into JSON-compatible structures. """ + if isinstance(value, Config): + return value.to_dict() + + if isinstance(value, Enum): + return { + "__enum__": value.__class__.__name__, + "value": value.value, + } + + if isinstance(value, list): + return [Config._serialize_value(v) for v in value] + + if isinstance(value, dict): + return { + k: Config._serialize_value(v) + for k, v in value.items() + } + + return value + + @staticmethod + def _deserialize_value(value, typ): + """ Recursively deserialize values based on type hints. """ + origin = get_origin(typ) + args = get_args(typ) + + # Optional / Union + if origin is type(None): + return value + + if origin is list: + subtype = args[0] + return [ + Config._deserialize_value(v, subtype) + for v in value + ] + + if origin is dict: + key_type, val_type = args + return { + k: Config._deserialize_value(v, val_type) + for k, v in value.items() + } + + # Handle Optional[T] / Union[T, None] + if origin is not None and origin.__name__ == "Union": + for subtype in args: + if subtype is type(None): + continue + try: + return Config._deserialize_value(value, subtype) + except Exception: + pass + return value + + # Nested configs + if isinstance(typ, type) and issubclass(typ, Config): + return typ.from_dict(value) + + # Enums + if isinstance(typ, type) and issubclass(typ, Enum): + return typ(value) + + return value + + def save(self, filepath): + """ + Save config to a JSON file. + + Args: + filepath (str): Path to file. + """ + with open(filepath, 'w') as f: + json.dump(self.to_dict(), f, indent=4) + + def load(self, filepath): + """ + Load config from a JSON file. + + Args: + filepath (str): Path to file. + + Returns: + Config: Loaded config instance. + """ + with open(filepath) as f: + data = json.load(f) + return self.from_dict(data) + + @classmethod + def generate_documentation(cls) -> str: + """ + Generate documentation for all registered presets. + + Returns: + str: Formatted documentation string. + """ + cls_doc = inspect.getdoc(fn) or "No class docstring provided." + preset_docs = [] + for name, fn in cls._presets.items(): + doc = inspect.getdoc(fn) or "No description provided." + preset_docs.append(f""" + {name} + {'-' * len(name)} + + {doc} + """.strip() + ) + preset_text = "\n\n".join(preset_docs) + return f""" + {cls_doc} + + Preset Configurations + ===================== + + {preset_text} + """.strip() + + + +CONFIG_REGISTRY = {} + +@api_util.export("_internal.register_config") +def register_config(cls): + """ + Register a config class globally. + Useful for dynamic lookup and deserialization. + + Args: + cls (type): Config class. + + Returns: + type: Registered class. + """ + CONFIG_REGISTRY[cls.__name__] = cls + return cls + + +@api_util.export("configs.LayerConfig") +@register_config +@dataclass +class LayerConfig(Config): + """ + Configuration for a single layer/module. + Attributes: + name (str): The name of the layer/module. (e.g., 'Conv', 'Linear', 'BatchNorm', etc.) + kwargs (dict[str, Any]): A dictionary of keyword arguments for the layer/module. (e.g. {'kernel_size': 3, 'stride': 1, 'padding': 1}) + """ + name: str + kwargs: dict[str, Any] = None + + +@api_util.export("_internal.ConfigurableModule") +class ConfigurableModule: + """ + Base class for modules that can be constructed from a Config. + Provides a standard interface for building modules from configs. + """ + @classmethod + def build(cls, + rank:int, + in_channels:int, + out_channels:int, + cfg: Config): + """ + Instantiate a module using a config object. + + Args: + rank (int): Spatial rank (1D, 2D, 3D). + in_channels (int): Input channels. + out_channels (int): Output channels. + cfg (Config): Configuration object. + + Returns: + nn.Module: Instantiated module. + """ + return cls(in_channels, out_channels, **cfg.as_kwargs(), rank=rank) + + + diff --git a/im2sim/src/layers/projections.py b/im2sim/src/layers/projections.py new file mode 100644 index 0000000..b619bb5 --- /dev/null +++ b/im2sim/src/layers/projections.py @@ -0,0 +1,130 @@ +import logging + +import torch +import torch.nn.functional as F +from torch import nn + +logger = logging.getLogger(__name__) + + +class TrilinearProjection(nn.Module): + def __init__(self, domain_size): + super().__init__() + self.domain_size = domain_size + + def forward(self, encoder_outputs, graph_coords, batch): + projections = [] + + n_dims = graph_coords.shape[1] + for i in torch.unique(batch).to(torch.int16): + coords = graph_coords[batch == i] + n_nodes = coords.shape[0] + + grid = torch.stack( + [(2 * coords[:, j] / (d - 1)) - 1 for j, d in enumerate(self.domain_size)], + axis=-1, + ) # normalise coords [-1,1] and divide by scale + + grid = grid.reshape(1, n_nodes, 1, 1, n_dims) # [N,3]->[1,N,1,1,3] + + grid = grid.type_as(encoder_outputs) + + projections.append( + F.grid_sample( + encoder_outputs[i].unsqueeze(0), + grid, + align_corners=True, + padding_mode="border", + ) + .reshape(encoder_outputs.shape[1], -1) + .permute(1, 0) + ) # [1,C,N,1,1] -> [1,C,N] -> [N,C] + + projections = torch.cat(projections, dim=0) + + return projections + + +class OGProjection(nn.Module): + def __init__(self, image_dim): + super().__init__() + self.image_dim = image_dim + + def forward(self, image_features, graph_features, batch): + projections = [] + for i in torch.unique(batch).to(torch.int16): + # TensorFlow tf.shape equivalents + h = image_features[i].shape[-3] + w = image_features[i].shape[-2] + d = image_features[i].shape[-1] + + # Last 3 coords + x = graph_features[batch == i, -3] + y = graph_features[batch == i, -2] + z = graph_features[batch == i, -1] + + factor = torch.tensor(self.image_dim / h, dtype=x.dtype, device=x.device) + + x = x / factor + y = y / factor + z = z / factor + + # floor / ceil with clamp + x1 = torch.minimum(torch.floor(x), torch.tensor(h - 1, dtype=x.dtype, device=x.device)) + x2 = torch.minimum(torch.ceil(x), torch.tensor(h - 1, dtype=x.dtype, device=x.device)) + y1 = torch.minimum(torch.floor(y), torch.tensor(w - 1, dtype=x.dtype, device=x.device)) + y2 = torch.minimum(torch.ceil(y), torch.tensor(w - 1, dtype=x.dtype, device=x.device)) + z1 = torch.minimum(torch.floor(z), torch.tensor(d - 1, dtype=x.dtype, device=x.device)) + z2 = torch.minimum(torch.ceil(z), torch.tensor(d - 1, dtype=x.dtype, device=x.device)) + + # cast to int for indexing + x1 = x1.long() + x2 = x2.long() + y1 = y1.long() + y2 = y2.long() + z1 = z1.long() + z2 = z2.long() + + # mimic tf.gather_nd(image_features[0], ...) + img0 = image_features[i] + + def gather(img, xi, yi, zi): + return img[..., xi, yi, zi] + + # --- z1 plane --- + q11 = gather(img0, x1, y1, z1) + q21 = gather(img0, x2, y1, z1) + q12 = gather(img0, x1, y2, z1) + q22 = gather(img0, x2, y2, z1) + + wx = (x - x1.float()).unsqueeze(0) + wx2 = (x2.float() - x).unsqueeze(0) + + lerp_x1 = q21 * wx + q11 * wx2 + lerp_x2 = q22 * wx + q12 * wx2 + + wy = (y - y1.float()).unsqueeze(0) + wy2 = (y2.float() - y).unsqueeze(0) + + lerp_y1 = lerp_x2 * wy + lerp_x1 * wy2 + + # --- z2 plane --- + q11 = gather(img0, x1, y1, z2) + q21 = gather(img0, x2, y1, z2) + q12 = gather(img0, x1, y2, z2) + q22 = gather(img0, x2, y2, z2) + + lerp_x1 = q21 * wx + q11 * wx2 + lerp_x2 = q22 * wx + q12 * wx2 + + lerp_y2 = lerp_x2 * wy + lerp_x1 * wy2 + + # --- z interpolation --- + wz = (z - z1.float()).unsqueeze(0) + wz2 = (z2.float() - z).unsqueeze(0) + + lerp_z = lerp_y2 * wz + lerp_y1 * wz2 + projections.append(lerp_z) + + projections = torch.cat(projections, dim=0).permute(1, 0) + return projections diff --git a/im2sim/src/layers/reverse_halfunet.py b/im2sim/src/layers/reverse_halfunet.py new file mode 100644 index 0000000..d1f49c2 --- /dev/null +++ b/im2sim/src/layers/reverse_halfunet.py @@ -0,0 +1,302 @@ +from dataclasses import dataclass, field, fields + +import torch +from im2sim.src.layers.image_conv_blocks import ImageConvBlock, ImageConvBlockConfig +from im2sim.src.layers.layer_util import ( + ResidualConnectionType, + apply_residual_connection, + get_image_layer, +) +from im2sim.src.layers.module_config import Config, ConfigurableModule, LayerConfig, register_config + +@register_config +@dataclass +class ReverseHalfUNetConfig(Config): + hidden_channels: int = 64 + num_downsamples: int = 4 + pool_spec: LayerConfig | list[LayerConfig] = field( + default_factory=lambda: LayerConfig(name="MaxPool", kwargs={"kernel_size": 2}) + ) + upsample_spec: LayerConfig | list[LayerConfig] = field( + default_factory=lambda: LayerConfig( + name="Upsample", kwargs={"scale_factor": 2, "mode": "trilinear"} + ) + ) + block_cfg: ImageConvBlockConfig = field(default_factory=lambda: ImageConvBlockConfig()) + blocks_per_level: int = 2 + out_activation: str | None = None + stem_block_cfg: ImageConvBlockConfig | None = None + decoder_block_cfg: list[ImageConvBlockConfig] | ImageConvBlockConfig | None = None + out_block_cfg: ImageConvBlockConfig | None = None + fusion_type: ResidualConnectionType = ResidualConnectionType.ADD + + def __post_init__(self): + if self.stem_block_cfg is None: + self.stem_block_cfg = self.block_cfg.apply_presets(["single_block"]) + + + if self.decoder_block_cfg is None: + self.decoder_block_cfg = [self.block_cfg] * self.num_downsamples + elif isinstance(self.decoder_block_cfg, ImageConvBlockConfig): + self.decoder_block_cfg = [self.decoder_block_cfg] * self.num_downsamples + + if self.out_block_cfg is None: + self.out_block_cfg = self.block_cfg.apply_presets(["single_conv"]) + self.out_block_cfg.out_activation = self.out_activation + + +class ReverseHalfUNet(torch.nn.Module, ConfigurableModule): + def __init__( + self, + in_channels: int, + out_channels: int, + rank: int, + hidden_channels: int = 64, + num_downsamples: int = 4, + pool_spec: LayerConfig | list[LayerConfig] | None = None, + upsample_spec: LayerConfig | list[LayerConfig] | None = None, + block_cfg: ImageConvBlockConfig | None = None, + blocks_per_level: int = 2, + out_activation: str | None = None, + stem_block_cfg: ImageConvBlockConfig | None = None, + decoder_block_cfg: list[ImageConvBlockConfig] | ImageConvBlockConfig | None = None, + out_block_cfg: ImageConvBlockConfig | None = None, + fusion_type: ResidualConnectionType = ResidualConnectionType.ADD, + ): + super().__init__() + + self.rank = rank + self.in_channels = in_channels + self.out_channels = out_channels + self.hidden_channels = hidden_channels + self.num_downsamples = num_downsamples + + self.fusion_type = fusion_type + fusion_channels = ( + hidden_channels * 2 + if self.fusion_type is ResidualConnectionType.CONCAT + else hidden_channels + ) + + if pool_spec is None: + pool_spec = LayerConfig(name="MaxPool", kwargs={"kernel_size": 2}) + if upsample_spec is None: + mode = "nearest" if rank == 1 else "bilinear" if rank == 2 else "trilinear" + upsample_spec = LayerConfig(name="Upsample", kwargs={"scale_factor": 2, "mode": mode}) + if block_cfg is None: + block_cfg = ImageConvBlockConfig() + + if isinstance(pool_spec, LayerConfig): + pool_spec = [pool_spec] * num_downsamples + if isinstance(upsample_spec, LayerConfig): + upsample_spec = [upsample_spec] * num_downsamples + + for p, u in zip(pool_spec, upsample_spec, strict=True): + assert p.name.lower() in ["maxpool", "averagepool"], f"pool type {p.name} not supported" + assert u.name.lower() in ["upsample", "pixelshuffle"], ( + f"upsample type {u.name} not supported" + ) + + self.pools = torch.nn.ModuleList( + [get_image_layer(pool.name, rank)(**pool.kwargs) for pool in pool_spec] + ) + self.ups = torch.nn.ModuleList( + [get_image_layer(upsample.name, rank)(**upsample.kwargs) for upsample in upsample_spec] + ) + + if stem_block_cfg is None: + stem_block_cfg = block_cfg.apply_presets(["single_block"]) + + self.stem = ImageConvBlock.build(rank, in_channels, hidden_channels, stem_block_cfg) + print(type(self.stem)) + + if decoder_block_cfg is None: + decoder_block_cfg = [block_cfg] * num_downsamples + elif isinstance(decoder_block_cfg, ImageConvBlockConfig): + decoder_block_cfg = [decoder_block_cfg] * num_downsamples + + self.decoder_blocks = torch.nn.ModuleList( + [ + torch.nn.Sequential( + *[ImageConvBlock.build(rank, fusion_channels, hidden_channels, cfg)] + * blocks_per_level + ) + for cfg in decoder_block_cfg + ] + ) + + if out_block_cfg is None: + out_block_cfg = block_cfg.apply_presets(["single_conv"]) + out_block_cfg.out_activation = out_activation + + self.out_block = ImageConvBlock.build( + rank, hidden_channels, out_channels, out_block_cfg + ) + + def forward(self, x): + x = self.stem(x) + + pooled_features = [x] + for pool in self.pools: + x = pool(x) + pooled_features.append(x) + + pooled_features = pooled_features[::-1] # Reverse the order for decoding + + fused = None + for pf, block, up in zip(pooled_features[:-1], self.decoder_blocks, self.ups, strict=True): + if fused is None: + fused = block(pf) + else: + fused = block( + apply_residual_connection(fused, pf, connection_type=self.fusion_type) + ) + + fused = up(fused) + + out = self.out_block( + apply_residual_connection(fused, pooled_features[-1], connection_type=self.fusion_type) + ) + return out + + + + +@ReverseHalfUNetConfig.register_preset("residual") +def half_unet_residual_type(cfg: ReverseHalfUNetConfig): + cfg.decoder_block_cfg = [ + c.apply_presets(["0_residual"]) for c in cfg.decoder_block_cfg + ] + return cfg + + +@ReverseHalfUNetConfig.register_preset("dilated_bottleneck") +def unet_residual_type(cfg: ReverseHalfUNetConfig): + cfg.decoder_block_cfg[-1] = cfg.decoder_block_cfg[-1].apply_presets(cfg.block_cfg, presets=["dilated_convs"]) + return cfg + + +def _apply_preset_to_all_blocks(cfg: ReverseHalfUNetConfig, preset_name: str): + cfg.stem_block_cfg = cfg.stem_block_cfg.apply_presets([preset_name]) + cfg.decoder_block_cfg = [ + c.apply_presets([preset_name]) for c in cfg.decoder_block_cfg + ] + cfg.out_block_cfg = cfg.out_block_cfg.apply_presets([preset_name]) + return cfg + + +@ReverseHalfUNetConfig.register_preset("recon") +def reconstruction_config(cfg: ReverseHalfUNetConfig): + _apply_preset_to_all_blocks(cfg, "recon") + cfg.out_activation = None + return cfg + + +@ReverseHalfUNetConfig.register_preset("single_class_segmentation") +def segmentation_config(cfg: ReverseHalfUNetConfig): + _apply_preset_to_all_blocks(cfg, "segmentation") + cfg.out_block_cfg.out_activation = "sigmoid" + return cfg + +@ReverseHalfUNetConfig.register_preset("multiclass_segmentation") +def segmentation_config(cfg: ReverseHalfUNetConfig): + _apply_preset_to_all_blocks(cfg, "segmentation") + cfg.out_block_cfg.out_activation = "softmax" + return cfg + + +@ReverseHalfUNetConfig.register_preset("depthwise_separable") +def depthwise_separable_config(cfg: ReverseHalfUNetConfig): + cfg.decoder_block_cfg = [ + c.apply_presets(["depthwise_separable"]) + for c in cfg.decoder_block_cfg + ] + return cfg + +@ReverseHalfUNetConfig.register_preset("ghost_depthwise") +def ghost_dw_config(cfg: ReverseHalfUNetConfig): + cfg.decoder_block_cfg = [ + c.apply_presets(["ghost_depthwise"]) + for c in cfg.decoder_block_cfg + ] + return cfg + + +@ReverseHalfUNetConfig.register_preset("ghost_depthwise_separable") +def ghost_dws_config(cfg: ReverseHalfUNetConfig): + cfg.decoder_block_cfg = [ + c.apply_presets(["ghost_depthwise_separable"]) + for c in cfg.decoder_block_cfg + ] + return cfg + + +@ReverseHalfUNetConfig.register_preset("ECA") +def eca_config(cfg: ReverseHalfUNetConfig): + cfg.decoder_block_cfg = [ + c.apply_presets(["ECA"]) for c in cfg.decoder_block_cfg + ] + return cfg + + +@ReverseHalfUNetConfig.register_preset("SE") +def squeeze_excitation_config(cfg: ReverseHalfUNetConfig): + cfg.decoder_block_cfg = [ + c.apply_presets(["SE"]) for c in cfg.decoder_block_cfg + ] + return cfg + + +if __name__ == "__main__": + + def cfg_print(cfg): + print("###################") + for field in fields(cfg): + value = getattr(cfg, field.name) + print(f" {field.name}: {value}") + + cfg = ImageConvBlockConfig() + cfg = ReverseHalfUNetConfig(num_downsamples=3) + cfg = ReverseHalfUNetConfig.apply_presets(cfg, ["ghost_depthwise", "residual"]) + + model = ReverseHalfUNet.build(rank=3, in_channels=1, out_channels=1, cfg=cfg) + + print(model) + + x = torch.randn(1, 1, 128, 128, 128) # Example input for a 3D tensor + + def check_gradients(model, x): + model.train() + + # Ensure input tracks gradients if you want to test input gradients + x = x.requires_grad_(True) + + # Forward + y = model(x) + + # Use a scalar loss + loss = y.sum() + + # Backward + loss.backward() + + # Check input gradient + assert x.grad is not None, "Input gradient is None" + assert torch.isfinite(x.grad).all(), "Input gradient contains NaN/Inf" + + # Check parameter gradients + for name, param in model.named_parameters(): + if param.requires_grad: + assert param.grad is not None, f"{name} gradient is None" + assert torch.isfinite(param.grad).all(), f"{name} gradient contains NaN/Inf" + assert param.grad.abs().sum() > 0, f"{name} gradient is zero" + + return True + + # # Check gradients + # if check_gradients(model, x): + # print("Gradient check passed.") + + x = torch.randn(1, 1, 128, 128, 128) + y = model(x) + print(y.shape) diff --git a/im2sim/src/losses/__init__.py b/im2sim/src/losses/__init__.py new file mode 100644 index 0000000..0286229 --- /dev/null +++ b/im2sim/src/losses/__init__.py @@ -0,0 +1,3 @@ +from im2sim.src.losses import feature, mesh, pointcloud + +__all__ = ["feature", "mesh", "pointcloud"] diff --git a/im2sim/losses/contents.md b/im2sim/src/losses/contents.md similarity index 100% rename from im2sim/losses/contents.md rename to im2sim/src/losses/contents.md diff --git a/im2sim/src/losses/feature.py b/im2sim/src/losses/feature.py new file mode 100644 index 0000000..1fcc494 --- /dev/null +++ b/im2sim/src/losses/feature.py @@ -0,0 +1,33 @@ +import logging + +import torch +from torch_geometric.nn import knn_interpolate + +logger = logging.getLogger(__name__) + +# def mse(gr1, gr2): +# return torch.mean((gr1.x[...,3:] - gr2.x[...,3:])**2) + + +def mse(x1, x2): + return torch.mean((x1 - x2) ** 2) + + +class KnnMSE(torch.nn.Module): + def __init__(self, k=3): + super().__init__() + self.k = k + + def forward(self, true_graph, pred_graph): + c1 = true_graph.x[:, :3] + c2 = pred_graph.x[:, :3] + + f1 = true_graph.x[:, 3:] + f2 = pred_graph.x[:, 3:] + + b1 = true_graph.batch + b2 = pred_graph.batch + + f1_interp = knn_interpolate(f1, c1, c2, b1, b2, k=self.k) + + return mse(f1_interp, f2) diff --git a/im2sim/src/losses/mesh.py b/im2sim/src/losses/mesh.py new file mode 100644 index 0000000..9dfa9e0 --- /dev/null +++ b/im2sim/src/losses/mesh.py @@ -0,0 +1,135 @@ +import logging +from itertools import combinations + +import torch +import torch.nn.functional as F + +from im2sim.src.data.mesh_utils import compute_edge_lengths + +logger = logging.getLogger(__name__) + + +def edge_length_deviation_loss(gr1, gr2): + ed1 = _edge_length_deviation(gr1.x[:, :3], gr1.edge_index) + ed2 = _edge_length_deviation(gr2.x[:, :3], gr2.edge_index) + return F.relu(ed2 - ed1) ** 2 + + +def _edge_length_deviation(points, edges): + lengths = compute_edge_lengths(points, edges) + dev = lengths.std() / (lengths.mean() + 1e-8) + return dev + + +def _aspect_ratio(x, cells): + tet_vertices = x[cells, :] + vert_ids = list(combinations(range(4), 2)) + edge_coords = tet_vertices[vert_ids, :] + distances = torch.linalg.norm(edge_coords[:, 0, :, :] - edge_coords[:, 1, :, :], dim=-1) + aspect_ratio = distances.max(0).values / distances.mean(0) + return aspect_ratio.mean() + + +class AspectRatioLoss(torch.nn.Module): + def __init__(self, cell_key): + super().__init__() + if isinstance(cell_key, str): + self.select = lambda obj: getattr(obj, cell_key) + else: + raise TypeError(f"face_key must be a graph attribute but is {cell_key}") + + def forward(self, gr1, gr2): + ar1 = _aspect_ratio(x=gr1.x[:, :3], cells=self.select(gr1)) + ar2 = _aspect_ratio(x=gr2.x[:, :3], cells=self.select(gr2)) + return F.relu(ar2 - ar1) ** 2 + + +def _face_norm(face_verts): + side1 = face_verts[1] - face_verts[0] + side2 = face_verts[2] - face_verts[0] + + norm_vec = torch.cross(side1, side2, dim=-1) + unit_norm = norm_vec / (torch.norm(norm_vec, dim=-1, keepdim=True) + 1e-8) + return unit_norm + + +def face_norm_loss(x1, x2, b1, b2, f1, f2): + # x:[N,3], f:[3,M], norm: [3,M,3] + norm1 = _face_norm(x1[f1, :]) + norm2 = _face_norm(x2[f2, :]) + + batch1 = b1[f1[0]] + batch2 = b2[f2[0]] + + consistency = torch.Tensor([0.0]).to(norm1.device) + similarity = torch.Tensor([0.0]).to(norm1.device) + + for b in torch.unique(b1).tolist(): + mask1 = batch1 == b + mask2 = batch2 == b + consistency += torch.norm(norm2[mask2].std(0)) + similarity += torch.norm(norm1[mask1].mean(0) - norm2[mask2].mean(0)) + + return consistency + similarity + + +class FaceNormalLoss(torch.nn.Module): + def __init__(self, face_key=None): + super().__init__() + if isinstance(face_key, str): + self.select = lambda obj: getattr(obj, face_key) + else: + raise TypeError(f"face_key must be a graph attribute but is {face_key}") + + def forward(self, gr1, gr2): + faces1 = self.select(gr1) + faces2 = self.select(gr2) + loss = face_norm_loss( + x1=gr1.x[:, :3], + x2=gr2.x[:, :3], + b1=gr1.batch, + b2=gr2.batch, + f1=faces1, + f2=faces2, + ) + return loss + + +def tet_det(x, cells): + """Return signed 6*volume per tet (scalar triple product).""" + + a = x[cells[0]] + b = x[cells[1]] + c = x[cells[2]] + d = x[cells[3]] + + e1 = b - a + e2 = c - a + e3 = d - a + + det = (torch.cross(e1, e2) * e3).sum(-1) # signed det(D) = signed 6V + return det + + +def inversion_loss(x, cells, min_vol=1e-3): + + det6 = tet_det(x, cells) + + vol = det6 / 6.0 + + return torch.maximum(torch.zeros(1).to(vol.device), min_vol - vol).mean() + + +class InversionLoss(torch.nn.Module): + def __init__(self, cell_key, min_vol=1e-3): + super().__init__() + if isinstance(cell_key, str): + self.select = lambda obj: getattr(obj, cell_key) + else: + raise TypeError(f"face_key must be a graph attribute but is {cell_key}") + self.min_vol = min_vol + + def forward(self, gr1, gr2): + cells = self.select(gr2) + loss = inversion_loss(x=gr2.x[:, :3], cells=cells, min_vol=self.min_vol) + return loss diff --git a/im2sim/losses/pointcloud.py b/im2sim/src/losses/pointcloud.py similarity index 60% rename from im2sim/losses/pointcloud.py rename to im2sim/src/losses/pointcloud.py index bd4beb3..701a455 100644 --- a/im2sim/losses/pointcloud.py +++ b/im2sim/src/losses/pointcloud.py @@ -1,36 +1,41 @@ -import logging import inspect +import logging import torch import torch_geometric.nn as gnn logger = logging.getLogger(__name__) + def _compute_batch_chamfer(y1, y2, b1=None, b2=None): - if b1==None: + if b1 is None: b1 = torch.zeros(y1.shape[0]) - if b2==None: + if b2 is None: b2 = torch.zeros(y2.shape[0]) - logging.debug("shapes - y1:%s, y2:%s, b1:%s, b2%s", - tuple(y1.shape),tuple(y2.shape),tuple(b1.shape),tuple(b2.shape)) + logger.debug( + "shapes - y1:%s, y2:%s, b1:%s, b2%s", + tuple(y1.shape), + tuple(y2.shape), + tuple(b1.shape), + tuple(b2.shape), + ) nns1 = gnn.pool.knn(x=y2, y=y1, batch_x=b2, batch_y=b1, k=1) - logging.debug("nn shape: %s", nns1.shape) + logger.debug("nn shape: %s", nns1.shape) if nns1.shape[-1] == 0: - return (y2*0).sum() - + return (y2 * 0).sum() + d1 = torch.linalg.norm(y1 - y2[nns1[1]], dim=-1).mean() nns2 = gnn.pool.knn(x=y1, y=y2, batch_x=b1, batch_y=b2, k=1) if nns2.shape[-1] == 0: - return (y2*0).sum() - logging.debug("nn shape: %s", nns2.shape) + return (y2 * 0).sum() + logger.debug("nn shape: %s", nns2.shape) d2 = torch.linalg.norm(y2 - y1[nns2[1]], dim=-1).mean() return d1 + d2 class ChamferLoss(torch.nn.Module): - - def __init__(self, mask = None): + def __init__(self, mask=None): super().__init__() if isinstance(mask, str): self.mask = lambda obj: getattr(obj, mask) @@ -39,14 +44,14 @@ def __init__(self, mask = None): else: raise ValueError("mask must be either a graph attribute or a function") - def forward(self, gr1, gr2): mask1 = self.mask(gr1) mask2 = self.mask(gr2) - logging.debug("mask_type - %s",type(mask1)) - loss = _compute_batch_chamfer(y1 = gr1.x[mask1,:3], - y2 = gr2.x[mask2,:3], - b1 = gr1.batch[mask1], - b2 = gr2.batch[mask2]) + logger.debug("mask_type - %s", type(mask1)) + loss = _compute_batch_chamfer( + y1=gr1.x[mask1, :3], + y2=gr2.x[mask2, :3], + b1=gr1.batch[mask1], + b2=gr2.batch[mask2], + ) return loss - diff --git a/im2sim/src/losses/seg.py b/im2sim/src/losses/seg.py new file mode 100644 index 0000000..eacf99c --- /dev/null +++ b/im2sim/src/losses/seg.py @@ -0,0 +1,44 @@ +import torch + + +def dice_calc(y1: torch.Tensor, y2: torch.Tensor, smooth: float = 1e-5) -> torch.Tensor: + """ + A function to compute the DICE coefficient for two masks + + Args: + y1 (torch.Tensor): True one-hot encoded mask of shape [B,C,H,D] or [B,C,H,D,W] + y2 (torch.Tensor): Predicted one-hot encoded mask of shape [B,C,H,D] or [B,C,H,D,W] + smooth (float): float value to avoid divide-by-zero + + Returns: + dice_coeff (torch.Tensor): A tensor of shape [B,C] containing the DICE scores for each sample and channel + """ + intersection = torch.sum(y2 * y1, dim=2) # (N, C) + union = torch.sum(y2.pow(2), dim=2) + torch.sum(y1, dim=2) # (N, C) + ## p^2 + t^2 >= 2*p*t, target_onehot^2 == target_onehot + dice_coef = (2 * intersection + smooth) / (union + smooth) # (N, C) + return dice_coef + + +def dice_loss( + y1: torch.Tensor, + y2: torch.Tensor, + smooth: float = 1e-5, + channel_weights: torch.Tensor = None, +) -> torch.Tensor: + """ + A function to compute the DICE loss + + Args: + y1 (torch.Tensor): True one-hot encoded mask of shape [B,C,H,D] or [B,C,H,D,W] + y2 (torch.Tensor): Predicted one-hot encoded mask of shape [B,C,H,D] or [B,C,H,D,W] + smooth (float): float value to avoid divide-by-zero + channel_weights (torch.Tensor, optional): tensor of weights for each channels DICE score of shape [C] + + Returns: + dice_coeff (torch.Tensor): A tensor of shape [B,C] containing the DICE scores for each sample and channel + """ + if channel_weights is None: + return torch.mean(1 - dice_calc(y1, y2, smooth)) + else: + return torch.mean(torch.mean(1 - dice_calc(y1, y2, smooth), dim=0) * channel_weights) diff --git a/im2sim/src/losses/utils.py b/im2sim/src/losses/utils.py new file mode 100644 index 0000000..766f4e1 --- /dev/null +++ b/im2sim/src/losses/utils.py @@ -0,0 +1,21 @@ +import inspect + +from torch import nn + + +class GraphLoss(nn.Module): + def __init__(self, loss_fn, kwargs): + self.loss_fn = loss_fn + self.params = inspect.signature(loss_fn).parameters + self.kwargs = kwargs + + def forward(self, true_graph, pred_graph): + gr_dict = {"true": true_graph, "pred": pred_graph} + call_args = { + key: getattr( + gr_dict[key.split("_")[0]], key.split[1] + ) # key is in format _ + for key in self.params + } + loss = self.loss_fn(**call_args, **self.kwargs) + return loss diff --git a/im2sim/models/UNet_time.py b/im2sim/src/models/UNet_time.py similarity index 100% rename from im2sim/models/UNet_time.py rename to im2sim/src/models/UNet_time.py diff --git a/im2sim/src/models/__init__.py b/im2sim/src/models/__init__.py new file mode 100644 index 0000000..4685966 --- /dev/null +++ b/im2sim/src/models/__init__.py @@ -0,0 +1,4 @@ +# # from .image_to_graph import I2GUNet as I2GUNet +# from .image_to_graph import SimpleI2G as SimpleI2G +# from .utils import get_model_config as get_model_config +# # from .UNet import UNet, StandardUNet diff --git a/im2sim/models/contents.md b/im2sim/src/models/contents.md similarity index 100% rename from im2sim/models/contents.md rename to im2sim/src/models/contents.md diff --git a/im2sim/src/models/image_to_graph.py b/im2sim/src/models/image_to_graph.py new file mode 100644 index 0000000..1c7e483 --- /dev/null +++ b/im2sim/src/models/image_to_graph.py @@ -0,0 +1,210 @@ +# import logging + +# import torch + +# # import torch_geometric.nn as gnn +# from torch import nn + +# # from torch_geometric.nn import TopKPooling +# # from torch_geometric.data import Data +# from ..layers import * + +# logger = logging.getLogger(__name__) + + +# class SimpleI2G(nn.Module): +# def __init__( +# self, +# in_channels, +# out_channels, +# cnn_filters=(16, 32, 64, 128, 256), +# cnn_kernel_size=3, +# cnn_res_depth=3, +# cnn_res_blocks_per_level=2, +# cnn_rank=3, +# cnn_norm_type=None, +# cnn_pool_type="MaxPool", +# cnn_pool_size=2, +# cnn_activation="relu", +# cnn_dropout_rate=None, +# projection_ids=((3, 4), (1, 2), (0, 1)), +# gnn_filters=((384, 288), (144, 96), (64, 32)), +# gnn_res_depth=3, +# gnn_n_process_blocks=1, +# gnn_n_deform_blocks=3, +# template_edge_index=None, +# gnn_conv_type="GATConv", +# gnn_conv_kwargs=None, +# gnn_activation="relu", +# out_activation="linear", +# gnn_norm_type="InstanceNorm", +# batched_ops=True, +# ): +# super().__init__() + +# logger.debug("Defining SimpleI2G layers...") +# self.batched_ops = batched_ops + +# self.encoder = ImageResEncoder( +# in_channels=in_channels, +# filters=cnn_filters, +# kernel_size=cnn_kernel_size, +# res_depth=cnn_res_depth, +# res_blocks_per_level=cnn_res_blocks_per_level, +# rank=cnn_rank, +# norm_type=cnn_norm_type, +# pool_type=cnn_pool_type, +# pool_size=cnn_pool_size, +# activation=cnn_activation, +# dropout_rate=cnn_dropout_rate, +# ) +# # self.encoder = Encoder3D(in_channels=in_channels) + +# # self.projection_layers = nn.ModuleList([TrilinearProjection() for _ in range(len(cnn_filters))]) +# self.projection_ids = projection_ids + +# projection_channels = _get_projection_channels(cnn_filters, self.projection_ids) +# self.decoder_blocks = nn.ModuleList( +# [ +# GraphResDecoderBlock( +# projection_channels=projection_channels[i], +# graph_channels=out_channels if i == 0 else gnn_filters[i - 1][1], +# out_channels=out_channels, +# filters=gnn_filters[i], +# res_depth=gnn_res_depth, +# # n_process_blocks = gnn_n_process_blocks, +# n_deform_blocks=gnn_n_deform_blocks, +# template_edge_index=template_edge_index, +# conv_type=gnn_conv_type, +# conv_kwargs=gnn_conv_kwargs, +# activation=gnn_activation, +# out_activation=out_activation, +# norm_type=gnn_norm_type, +# ) +# for i in range(len(gnn_filters)) +# ] +# ) +# logger.debug("Done") + +# def forward(self, x, template): +# logger.debug("In model forward pass...") +# encoder_outputs = self.encoder(x) +# outputs = [] +# graph_features = template.x.clone() +# curr_mesh = template.x.clone() +# for dec, ids in zip(self.decoder_blocks, self.projection_ids): +# proj_inp = torch.cat( +# [ +# OGProjection(image_dim=x.shape[-1])( +# encoder_outputs[id], curr_mesh[:, :3], template.batch +# ) +# for id in ids +# ], +# dim=-1, +# ) +# # proj_inp = torch.cat([TrilinearProjection(domain_size=[x.shape[-1]]*3, batch_ops=False)(encoder_outputs[id], curr_mesh[:,:3], template.batch) +# # for id in ids], dim=-1) +# graph_features, curr_mesh = dec( +# graph_features, proj_inp, curr_mesh, template.edge_index +# ) +# out_graph = template.clone() +# out_graph.x = curr_mesh +# outputs.append(out_graph) +# return outputs + + +# # class I2GUNet(nn.Module): + +# # def __init__(self, +# # in_channels, +# # out_channels, +# # domain_size, +# # filters=(16,32,64,128,256), +# # cnn_kernel_size=3, +# # cnn_res_depth=3, +# # cnn_res_blocks_per_level=2, +# # cnn_rank=3, +# # cnn_norm_type='InstanceNorm', +# # cnn_pool_type='MaxPool', +# # cnn_pool_size=2, +# # cnn_activation='leaky_relu', +# # gnn_res_depth = 3, +# # gnn_n_align_blocks = 1, +# # gnn_n_deform_blocks = 3, +# # gnn_conv_type="GATConv", +# # gnn_conv_kwargs=None, +# # gnn_activation="leaky_relu", +# # gnn_norm_type="InstanceNorm", +# # batched_ops=True): +# # super().__init__() + +# # logger.debug("Defining I2G layers...") +# # self.batched_ops = batched_ops +# # self.n_levels = len(filters) +# # self.n_channels = out_channels +# # self.encoder = ImageResEncoder(in_channels=in_channels, +# # filters=filters, +# # kernel_size=cnn_kernel_size, +# # res_depth=cnn_res_depth, +# # res_blocks_per_level=cnn_res_blocks_per_level, +# # rank=cnn_rank, +# # norm_type=cnn_norm_type, +# # pool_type=cnn_pool_type, +# # pool_size=cnn_pool_size, +# # activation=cnn_activation) + + +# # self.gpool = RecursiveClusterPooling(n_levels=self.n_levels) + +# # self.decoder = nn.ModuleList([ +# # GraphUNetDecoderBlock(#in_channels = out_channels if i==1 else filters[-i+1], +# # out_channels = out_channels, +# # filters = filters[-i], +# # domain_size = domain_size, +# # res_depth = gnn_res_depth, +# # n_align_blocks = 0 if i==1 else gnn_n_align_blocks, +# # n_deform_blocks = gnn_n_deform_blocks, +# # conv_type=gnn_conv_type, +# # conv_kwargs=gnn_conv_kwargs, +# # activation=gnn_activation, +# # norm_type=gnn_norm_type, +# # batched_ops=batched_ops) +# # for i in range(1,self.n_levels+1) +# # ]) + + +# # logger.debug("Done") + + +# # def forward(self, img, template): +# # encoder_features = self.encoder(img) +# # encoder_features.reverse() + +# # multi_template = self.gpool(template) +# # multi_template.reverse() + +# # outputs = [] +# # deformation = torch.zeros_like(multi_template[0].x) + +# # for i, (dec_layer, enc) in enumerate(zip(self.decoder, encoder_features)): +# # t = multi_template[i] +# # deformation = dec_layer(enc,deformation,t.x, t.edge_index, t.batch) +# # if i < self.n_levels-1: +# # out_graph = Data(x=t.x+deformation, edge_index=t.edge_index, batch=t.batch) +# # outputs.append(out_graph) +# # deformation = gnn.unpool.knn_interpolate(deformation, t.x, multi_template[i+1].x) + +# # out_graph = template.clone() +# # out_graph.x = template.x+deformation +# # outputs.append(out_graph) +# # return outputs + + +# def _get_projection_channels(filters, ids): +# channels = [] +# for id_list in ids: +# sum = 0 +# for id in id_list: +# sum += filters[id] +# channels.append(sum) +# return channels diff --git a/im2sim/src/models/unet.py b/im2sim/src/models/unet.py new file mode 100644 index 0000000..a33a632 --- /dev/null +++ b/im2sim/src/models/unet.py @@ -0,0 +1,518 @@ +# import torch.nn.functional as F +# from torch import nn + +# from ..layers.layer_util import + + +# class ImageConvBlock(nn.Module): +# """ +# A convolutional block for image data + +# Args: +# in_channels (int): The number of channels in the input to the layer. +# filters (int, optional): The number of filters in each convolutional layer (default: 32) +# kernel_size (int, optional): The kernel(filter) size for the convolutional layers (default: 3) +# depth (int, optional): The number of successive convolutional layers (default: 2) +# rank (int, optional): The number of spatial dimensions in the data i.e., 2D, 3D (default:2), +# activation (str, optional): The activation function applied after each convolution (default: "relu", options: "leakyrelu","gelu","sigmoid","linear") +# norm_type (str, optional): The normalization method to apply between convolutions (default:None, options: "BatchNorm", "InstanceNorm", "LayerNorm") +# dropout_rate (float, optional): The spatial dropout rate to be applied to the final convolution output (default:None) + +# Returns: +# A `torch.nn.Module` object. + +# """ + +# def __init__( +# self, +# in_channels, +# filters=32, +# kernel_size=3, +# depth=1, +# rank=3, +# activation="relu", +# norm_type=None, +# dropout_rate=None, +# ): +# super().__init__() + +# conv = IMAGE_LAYERS[f"Conv{rank}d"] +# drop = IMAGE_LAYERS[f"Dropout{rank}d"] +# self.convs = nn.ModuleList( +# [ +# conv( +# in_channels if i == 0 else filters, +# filters, +# kernel_size, +# padding=kernel_size // 2, +# ) +# for i in range(depth) +# ] +# ) + +# self.norms = nn.ModuleList( +# [ +# IMAGE_LAYERS(f"{norm_type}{rank}d")(filters) +# if norm_type +# else nn.Identity() +# for _ in range(depth) +# ] +# ) +# self.drop = drop(p=dropout_rate) if dropout_rate else nn.Identity() + +# self.act = ACTIVATIONS[activation]() + +# def forward(self, x): +# """ +# Args: +# x (torch.Tensor): Input feature maps in image space [in_channels, ...] where the number of dims in ... corresponds to rank + +# Returns: +# torch.Tensor: Output feature maps [out_channels, ...] +# """ + +# for conv, norm in zip(self.convs, self.norms): +# x = norm(self.act(conv(x))) +# return self.drop(x) + + +# class ImageEncoder(nn.Module): +# """ +# A CNN encoder for images. Structured like the encoder of a UNet. + +# Args: +# in_channels (int): The number of channels in the input image. +# filters (List[int], optional): The number of convolutional filters in each encoder level (default: [16,32,64,128,256]) +# kernel_size (int, optional): The kernel(filter) size for the convolutional layers (default: 3) +# conv_blocks_per_level (int, optional): The number of successive convolutional blocks per encoder level (default: 1) +# rank (int, optional): The number of spatial dimensions in the data i.e., 2D, 3D (default:3), +# activation (str, optional): The activation function applied after each convolution (default: "relu", options: "leakyrelu","gelu","sigmoid","linear") +# norm_type (str, optional): The normalization method to apply between convolutions (default:None, options: "BatchNorm", "InstanceNorm", "LayerNorm") +# dropout_rate (float, optional): The spatial dropout rate to be applied to each residual block prior to residual connection (default:None) + +# Returns: +# A `torch.nn.Module` object. +# """ + +# def __init__( +# self, +# in_channels, +# filters=(16, 32, 64, 128, 256), +# pool_sizes=None, +# kernel_size=3, +# conv_blocks_per_level=1, +# rank=3, +# norm_type=None, +# pool_type="MaxPool", +# activation="relu", +# dropout_rate=None, +# ): +# super().__init__() + +# n_levels = len(filters) +# self.conv_blocks = nn.ModuleList( +# [ +# ImageConvBlock( +# in_channels=in_channels if i == 0 else filters[i - 1], +# filters=filters[i], +# kernel_size=kernel_size, +# depth=conv_blocks_per_level, +# rank=rank, +# activation=activation, +# norm_type=norm_type, +# dropout_rate=dropout_rate, +# ) +# for i in range(n_levels) +# ] +# ) + +# if pool_sizes is None: +# pool_sizes = 2 + +# pool_sizes_standard = standardize_spatial_factors(pool_sizes, rank) + +# pool = IMAGE_LAYERS[f"{pool_type}{rank}d"] +# self.maxpools = nn.ModuleList( +# [ +# pool(pool_sizes_standard[i - 1]) if i > 0 else nn.Identity() +# for i in range(n_levels) +# ] +# ) + +# def forward(self, x): +# """ +# Args: +# x (torch.Tensor): Input image [in_channels, ...] + +# Returns: +# List[torch.Tensor]: Output feature maps from each level ordered from top to bottom [Tensor([filters[0], ...], ..., Tensor([filters[N], ...]) +# """ +# outputs = [] +# for pool, conv in zip(self.maxpools, self.conv_blocks): +# x = conv(pool(x)) +# outputs.append(x) +# return outputs + + +# class ImageDecoder(nn.Module): +# """ +# CNN decoder for images. Mirrors ImageEncoder like a UNet decoder. + +# Args: +# filters (List[int]): Encoder filter sizes in top→bottom order. +# kernel_size (int): Convolution kernel size. +# conv_blocks_per_level (int): Number of conv blocks per level. +# rank (int): Spatial rank (2 or 3). +# upsample_type (str): "ConvTranspose" or "Upsample". +# activation (str): Activation name. +# norm_type (str): Normalization type. +# dropout_rate (float): Dropout rate. +# skip (bool): Use skip connections. +# """ + +# def __init__( +# self, +# filters=(16, 32, 64, 128, 256), +# kernel_size=3, +# pool_sizes=None, +# upsample_sizes=None, +# conv_blocks_per_level=1, +# rank=3, +# upsample_type="ConvTranspose", +# activation="relu", +# norm_type=None, +# dropout_rate=None, +# skip=True, +# ): +# super().__init__() + +# self.skip = skip +# self.rank = rank + +# if pool_sizes is None: +# pool_sizes = 2 + +# pool_sizes_standard = standardize_spatial_factors(pool_sizes, rank) + +# n_levels = len(filters) + +# rev_filters = filters[::-1] +# rev_pool_sizes = pool_sizes_standard[::-1] + +# if upsample_sizes is None: +# upsample_sizes = rev_pool_sizes +# else: +# upsample_sizes = standardize_spatial_factors(upsample_sizes, rank) + +# if upsample_type.lower() == "upsample": +# self.ups = nn.ModuleList( +# [ +# nn.Upsample( +# scale_factor=upsample_sizes[i], +# mode="trilinear" if rank == 3 else "bilinear", +# align_corners=True, +# ) +# for i in range(n_levels - 1) +# ] +# ) +# else: +# up_layer = IMAGE_LAYERS(f"{upsample_type}{rank}d") +# self.ups = nn.ModuleList( +# [ +# up_layer( +# rev_filters[i], +# rev_filters[i + 1], +# kernel_size=upsample_sizes[i], +# stride=upsample_sizes[i], +# ) +# for i in range(n_levels - 1) +# ] +# ) + +# self.conv_blocks = nn.ModuleList( +# [ +# ImageConvBlock( +# in_channels=rev_filters[i + 1] * (2 if skip else 1), +# filters=rev_filters[i + 1], +# kernel_size=kernel_size, +# depth=conv_blocks_per_level, +# rank=rank, +# activation=activation, +# norm_type=norm_type, +# dropout_rate=dropout_rate, +# ) +# for i in range(n_levels - 1) +# ] +# ) + +# def _match_size(self, x, skip): +# """ +# Match skip spatial size to x spatial size using: +# - center crop if skip is larger +# - interpolation if skip is smaller + +# Args: +# x: decoder tensor, shape [B, C, ...] +# skip: encoder skip tensor, shape [B, C, ...] + +# Returns: +# skip resized to have spatial shape x.shape[2:] +# """ +# target_size = x.shape[2:] +# skip_size = skip.shape[2:] + +# if skip_size == target_size: +# return skip + +# # First crop any dimensions where skip is too large +# slices = [slice(None), slice(None)] +# needs_crop = False + +# for s, t in zip(skip_size, target_size): +# if s > t: +# start = (s - t) // 2 +# end = start + t +# slices.append(slice(start, end)) +# needs_crop = True +# else: +# slices.append(slice(None)) + +# if needs_crop: +# skip = skip[tuple(slices)] + +# # Then upsample if any dimensions are still too small +# if skip.shape[2:] != target_size: +# mode = "trilinear" if self.rank == 3 else "bilinear" +# skip = F.interpolate(skip, size=target_size, mode=mode, align_corners=True) + +# return skip + +# def forward(self, encoder_outputs): +# """ +# Args: +# encoder_outputs: List of tensors from encoder (top→bottom). + +# Returns: +# Decoded tensor at highest resolution. +# """ + +# # Reverse the encoder outputs so we traverse from bottleneck to top +# rev_enc = encoder_outputs[::-1] + +# # Start from bottleneck +# x = rev_enc[0] + +# # Traverse decoder levels +# for i, (up, conv) in enumerate(zip(self.ups, self.conv_blocks)): +# x = up(x) + +# if self.skip: +# skip_feat = rev_enc[i + 1] # next encoder feature +# skip_feat = self._match_size(x, skip_feat) +# x = torch.cat([x, skip_feat], dim=1) + +# x = conv(x) + +# return x + + +# class UNet(nn.Module): +# """ +# Flexible UNet for 2D or 3D images. + +# Args: +# in_channels (int): Input channels. +# out_channels (int): Output channels. +# filters (List[int]): Encoder filter sizes. +# kernel_size (int): Conv kernel size. +# pool_sizes (Tuple or list): pool sizes per level , +# upsample_sizes (Tuple or list): upsample sizes per level, +# conv_blocks_per_level (int): Depth per level. +# rank (int): Spatial rank. +# activation (str): Activation function. +# norm_type (str): Normalization type. +# dropout_rate (float): Dropout. +# final_activation (str): Output activation. +# """ + +# def __init__( +# self, +# in_channels, +# out_channels, +# filters=(16, 32, 64, 128, 256), +# pool_sizes=None, +# upsample_sizes=None, +# kernel_size=3, +# conv_blocks_per_level=1, +# rank=3, +# activation="relu", +# norm_type=None, +# dropout_rate=None, +# final_activation="linear", +# ): + +# pool_sizes_temp = [] +# if pool_sizes is None: +# for i in range(len(filters) - 1): +# pool_sizes_temp.append(2) +# pool_sizes = pool_sizes_temp + +# if not isinstance(pool_sizes, (list, tuple)): +# raise TypeError("pool_sizes must be a list or tuple") + +# for i, p in enumerate(pool_sizes): +# if isinstance(p, int) and not isinstance(p, bool): +# if p < 1: +# raise ValueError("pool_sizes must be positive") +# elif isinstance(p, (tuple, list)): +# if len(p) != rank: +# raise ValueError("pool_sizes tuple must be same length as rank") +# for j, ps in enumerate(p): +# if not isinstance(ps, int) or isinstance(ps, bool): +# raise TypeError("pool_sizes must be an int") +# if ps < 1: +# raise ValueError("pool_sizes must be positive") +# else: +# raise TypeError( +# "each entry in pool_sizes must be either an int or a tuple or list" +# ) + +# if len(filters) != (len(pool_sizes) + 1): +# raise ValueError( +# f"pool_sizes do not match number of filters. For {len(filters)}, please input {len(filters) - 1} number of pools." +# ) + +# if upsample_sizes is not None: +# if not isinstance(upsample_sizes, (list, tuple)): +# raise TypeError("upsample_sizes must be a list or tuple") + +# for i, p in enumerate(upsample_sizes): +# if isinstance(p, int) and not isinstance(p, bool): +# if p < 1: +# raise ValueError("upsample_sizes must be positive") +# elif isinstance(p, (tuple, list)): +# if len(p) != rank: +# raise ValueError( +# "upsample_sizes tuple must be same length as rank" +# ) +# for j, ps in enumerate(p): +# if not isinstance(ps, int) or isinstance(ps, bool): +# raise TypeError("upsample_sizes must be an int") +# if ps < 1: +# raise ValueError("upsample_sizes must be positive") +# else: +# raise TypeError( +# "each entry in upsample_sizes must be either an int or a tuple or list" +# ) + +# if len(filters) != (len(upsample_sizes) + 1): +# raise ValueError( +# f"upsample_sizes do not match number of filters. For {len(filters)}, please input {len(filters) - 1} number of upsamples." +# ) + +# super().__init__() + +# self.encoder = ImageEncoder( +# in_channels=in_channels, +# filters=filters, +# pool_sizes=pool_sizes, +# kernel_size=kernel_size, +# conv_blocks_per_level=conv_blocks_per_level, +# rank=rank, +# activation=activation, +# norm_type=norm_type, +# dropout_rate=dropout_rate, +# ) + +# self.decoder = ImageDecoder( +# filters=filters, +# pool_sizes=pool_sizes, +# upsample_sizes=upsample_sizes, +# kernel_size=kernel_size, +# conv_blocks_per_level=conv_blocks_per_level, +# rank=rank, +# activation=activation, +# norm_type=norm_type, +# dropout_rate=dropout_rate, +# ) + +# conv = IMAGE_LAYERS[f"Conv{rank}d"] + +# self.final_conv = conv(filters[0], out_channels, kernel_size=1) + +# self.final_act = ( +# ACTIVATIONS(final_activation)() +# if final_activation.lower() != "linear" +# else nn.Identity() +# ) + +# def forward(self, x): +# enc_feats = self.encoder(x) +# x = self.decoder(enc_feats) +# x = self.final_conv(x) +# x = self.final_act(x) +# return x + + +# class StandardUNet(UNet): +# """ +# Standaed UNet for 2D or 3D images. + +# Args: +# in_channels (int): Input channels. +# out_channels (int): Output channels. +# filters (List[int]): Encoder filter sizes. +# kernel_size (int): Conv kernel size. +# pool_sizes (Tuple or list): pool sizes per dim , +# conv_blocks_per_level (int): Depth per level. +# rank (int): Spatial rank. +# activation (str): Activation function. +# norm_type (str): Normalization type. +# dropout_rate (float): Dropout. +# final_activation (str): Output activation. +# """ + +# def __init__( +# self, +# in_channels, +# out_channels, +# filters=(16, 32, 64, 128, 256), +# pool_size=2, +# kernel_size=3, +# conv_blocks_per_level=1, +# rank=3, +# activation="relu", +# norm_type=None, +# dropout_rate=None, +# final_activation="linear", +# ): + +# pool_size_standard = [] +# if isinstance(pool_size, int): +# pool_size_single = [] +# for j in range(rank): +# pool_size_single.append(pool_size) +# pool_size_single = tuple(pool_size_single) +# for i in range(len(filters) - 1): +# pool_size_standard.append(pool_size_single) +# elif isinstance(pool_size, (tuple, list)): +# if len(pool_size) == rank: +# for i in range(len(filters) - 1): +# pool_size_standard.append(pool_size) +# else: +# raise ValueError("pool_size must be an int or have same length as rank") + +# super().__init__( +# in_channels=in_channels, +# out_channels=out_channels, +# filters=filters, +# pool_sizes=pool_size_standard, +# upsample_sizes=None, +# kernel_size=kernel_size, +# conv_blocks_per_level=conv_blocks_per_level, +# rank=rank, +# activation=activation, +# norm_type=norm_type, +# dropout_rate=dropout_rate, +# final_activation=final_activation, +# ) diff --git a/im2sim/src/models/utils.py b/im2sim/src/models/utils.py new file mode 100644 index 0000000..ee7ff73 --- /dev/null +++ b/im2sim/src/models/utils.py @@ -0,0 +1,68 @@ +# def get_model_config(name): +# """Get the default config for a specified type of model. + +# Args: +# name: A `str`. The name of the model type. + +# Returns: +# A config dictionary + +# Raises: +# ValueError: If the requested model config doesn't exist. +# """ +# try: +# return _CONFIGS[(name)] +# except KeyError as err: +# raise ValueError(f"Could not find config for model with name '{name}'") from err + + +# _CONFIGS = { +# "Image2Flow": { +# "cnn_filters": (16, 48, 96, 192, 384), +# "cnn_kernel_size": 3, +# "cnn_res_depth": 3, +# "cnn_res_blocks_per_level": 2, +# "cnn_rank": 3, +# "cnn_norm_type": "InstanceNorm", +# "cnn_pool_type": "MaxPool", +# "cnn_pool_size": 2, +# "cnn_activation": "leaky_relu", +# "cnn_dropout_rate": 0.3, +# "projection_ids": ((3, 4), (1, 2), (0, 1)), +# "gnn_filters": ((384, 288), (96, 64), (48, 32)), +# "gnn_res_depth": 3, +# "gnn_n_process_blocks": 1, +# "gnn_n_deform_blocks": 3, +# "template_edge_index": None, +# "gnn_conv_type": "ChebConv", +# "gnn_conv_kwargs": {"K": 1}, +# "gnn_activation": "leaky_relu", +# "out_activation": "leaky_relu", +# "gnn_norm_type": "InstanceNorm", +# "batched_ops": False, +# }, +# "Image2Mesh": { +# "cnn_filters": (16, 32, 64, 128, 256), +# "cnn_kernel_size": 3, +# "cnn_res_depth": 3, +# "cnn_res_blocks_per_level": 2, +# "cnn_rank": 3, +# "cnn_norm_type": "InstanceNorm", +# "cnn_pool_type": "MaxPool", +# "cnn_pool_size": 2, +# "cnn_activation": "leaky_relu", +# "cnn_dropout_rate": 0.3, +# "projection_ids": ((3, 4), (1, 2), (0, 1)), +# "gnn_filters": ((384, 288), (96, 64), (48, 32)), +# "gnn_res_depth": 3, +# "gnn_n_process_blocks": 1, +# "gnn_n_deform_blocks": 3, +# "template_edge_index": None, +# "gnn_conv_type": "ChebConv", +# "gnn_conv_kwargs": {"K": 1}, +# "gnn_activation": "leaky_relu", +# "out_activation": "leaky_relu", +# "gnn_norm_type": "InstanceNorm", +# "batched_ops": False, +# }, +# } diff --git a/im2sim/src/plot/__init__.py b/im2sim/src/plot/__init__.py new file mode 100644 index 0000000..54710f4 --- /dev/null +++ b/im2sim/src/plot/__init__.py @@ -0,0 +1 @@ +from im2sim.src.plot.pointcloud import PointCloudPlot as PointCloudPlot diff --git a/im2sim/src/plot/pointcloud.py b/im2sim/src/plot/pointcloud.py new file mode 100644 index 0000000..27dc124 --- /dev/null +++ b/im2sim/src/plot/pointcloud.py @@ -0,0 +1,264 @@ +import matplotlib.pyplot as plt +import numpy as np +from matplotlib import animation + + +class PointCloudPlot: + def __init__( + self, + nrows, + ncols, + point_sets, + color_sets=None, + figsize=None, + cmap="Blues_r", + norm_mode="none", # 'all', 'row', 'col', 'none' + bound_mode="all", + titles=None, # NEW: optional titles + elev=20, + azim=90, + ): + + self.cmap = cmap + self.elev = elev + self.azim = azim + self.nrows = nrows + self.ncols = ncols + self.norm_mode = norm_mode + + if color_sets is None: + is_colored = False + color_sets = [0.1 * np.ones(points.shape[0]) for points in point_sets] + else: + is_colored = True + + if figsize is None: + figsize = (ncols * 3, nrows * 3) + + self.fig, axes = plt.subplots( + nrows, ncols, figsize=figsize, subplot_kw={"projection": "3d"} + ) + + self.axes = np.array(axes).reshape(-1) + self.scatters = [] + + # ----------------------------- + # Titles handling + # ----------------------------- + if titles is not None: + if isinstance(titles, str): + self.fig.suptitle(titles) + titles = [None] * len(self.axes) + elif len(titles) != len(self.axes): + raise ValueError("titles must match number of subplots") + + # ----------------------------- + # Bounds (same as your logic) + # ----------------------------- + maxs = np.max(point_sets[-1], axis=0) + mins = np.min(point_sets[-1], axis=0) + + # ----------------------------- + # Normalization helpers + # ----------------------------- + def compute_norm_ranges(color_sets): + color_sets = list(color_sets) + + if norm_mode == "none": + return [(None, None)] * len(color_sets) + + elif norm_mode == "all": + all_vals = np.concatenate(color_sets) + vmin, vmax = all_vals.min(), all_vals.max() + return [(vmin, vmax)] * len(color_sets) + + elif norm_mode == "row": + ranges = [] + for r in range(nrows): + row_vals = [] + for c in range(ncols): + idx = r * ncols + c + row_vals.append(color_sets[idx]) + row_vals = np.concatenate(row_vals) + vmin, vmax = row_vals.min(), row_vals.max() + for _ in range(ncols): + ranges.append((vmin, vmax)) + return ranges + + elif norm_mode == "col": + ranges = [None] * len(color_sets) + for c in range(ncols): + col_vals = [] + for r in range(nrows): + idx = r * ncols + c + col_vals.append(color_sets[idx]) + col_vals = np.concatenate(col_vals) + vmin, vmax = col_vals.min(), col_vals.max() + for r in range(nrows): + idx = r * ncols + c + ranges[idx] = (vmin, vmax) + return ranges + + else: + raise ValueError("norm_mode must be 'all', 'row', 'col', or 'none'") + + norm_ranges = compute_norm_ranges(color_sets) + + # ----------------------------- + # Plot creation + # ----------------------------- + for i, (ax, points, colors) in enumerate( + zip(self.axes, point_sets, color_sets, strict=True) + ): + vmin, vmax = norm_ranges[i] + + sc = ax.scatter( + points[:, 0], + points[:, 1], + points[:, 2], + c=colors, + cmap=cmap, + vmin=vmin, + vmax=vmax, + ) + + ax.view_init(elev=elev, azim=azim, vertical_axis="y") + + ax.set_xlim(mins[0], maxs[0]) + ax.set_ylim(mins[1], maxs[1]) + ax.set_zlim(mins[2], maxs[2]) + + # Apply subplot title + if titles is not None and titles[i] is not None: + ax.set_title(titles[i]) + + if is_colored: + plt.colorbar(sc, ax=ax, shrink=0.5) + + self.scatters.append(sc) + + self.point_sets = point_sets + self.color_sets = color_sets + + # --------------------------------------------------------- + # DRAW ONE FRAME + # --------------------------------------------------------- + + def draw_frame(self, point_sets=None, color_sets=None): + + if point_sets is None: + point_sets = self.point_sets + + if color_sets is None: + color_sets = self.color_sets + + # recompute normalization each frame + def compute_norm_ranges(color_sets): + color_sets = list(color_sets) + + if self.norm_mode == "none": + return [(None, None)] * len(color_sets) + + elif self.norm_mode == "all": + all_vals = np.concatenate(color_sets) + vmin, vmax = all_vals.min(), all_vals.max() + return [(vmin, vmax)] * len(color_sets) + + elif self.norm_mode == "row": + ranges = [] + for r in range(self.nrows): + row_vals = [] + for c in range(self.ncols): + idx = r * self.ncols + c + row_vals.append(color_sets[idx]) + row_vals = np.concatenate(row_vals) + vmin, vmax = row_vals.min(), row_vals.max() + for _ in range(self.ncols): + ranges.append((vmin, vmax)) + return ranges + + elif self.norm_mode == "col": + ranges = [None] * len(color_sets) + for c in range(self.ncols): + col_vals = [] + for r in range(self.nrows): + idx = r * self.ncols + c + col_vals.append(color_sets[idx]) + col_vals = np.concatenate(col_vals) + vmin, vmax = col_vals.min(), col_vals.max() + for r in range(self.nrows): + idx = r * self.ncols + c + ranges[idx] = (vmin, vmax) + return ranges + + norm_ranges = compute_norm_ranges(color_sets) + + new_scatters = [] + + for i, (ax, sc, pts, colors) in enumerate( + zip(self.axes, self.scatters, point_sets, color_sets, strict=True) + ): + vmin, vmax = norm_ranges[i] + + if sc is None or len(pts) != len(sc.get_offsets()): + if sc is not None: + sc.remove() + + sc = ax.scatter( + pts[:, 0], + pts[:, 1], + pts[:, 2], + c=colors, + cmap=self.cmap, + vmin=vmin, + vmax=vmax, + ) + + else: + sc._offsets3d = (pts[:, 0], pts[:, 1], pts[:, 2]) + sc.set_array(colors) + if vmin is not None: + sc.set_clim(vmin, vmax) + + new_scatters.append(sc) + + self.scatters = new_scatters + return self.scatters + + # --------------------------------------------------------- + # SAVE IMAGE + # --------------------------------------------------------- + + def save_image(self, filename, dpi=200): + plt.tight_layout() + self.fig.savefig(filename, dpi=dpi) + plt.close(self.fig) + + # --------------------------------------------------------- + # ANIMATE + # --------------------------------------------------------- + + def animate( + self, + point_sequence_sets, + color_sequence_sets=None, + filename="animation.gif", + fps=15, + ): + + n_frames = len(point_sequence_sets) + + def update(frame): + + colors = None if color_sequence_sets is None else color_sequence_sets[frame] + + return self.draw_frame(point_sets=point_sequence_sets[frame], color_sets=colors) + + ani = animation.FuncAnimation(self.fig, update, frames=n_frames, blit=False) + + if filename.endswith(".gif"): + ani.save(filename, writer="pillow", fps=fps) + else: + ani.save(filename, writer="ffmpeg", fps=fps) + + plt.close(self.fig) diff --git a/im2sim/callbacks/__init__.py b/im2sim/src/utils/__init__.py similarity index 100% rename from im2sim/callbacks/__init__.py rename to im2sim/src/utils/__init__.py diff --git a/im2sim/src/utils/api_util.py b/im2sim/src/utils/api_util.py new file mode 100644 index 0000000..7c66134 --- /dev/null +++ b/im2sim/src/utils/api_util.py @@ -0,0 +1,197 @@ +# Copyright 2026 University College London. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + +"""Utilities to export symbols to the API.""" + +import importlib +import importlib.abc +import sys + + +_API_SYMBOLS = dict() + +_API_ATTR = '_api_names' + +_SUBMODULE_NAMES = [ + 'data', + 'layers', + 'configs', + 'ops', + 'losses', + 'models', + 'plot', + '_internal' +] + +_SUBMODULE_DOCSTRINGS = { + 'data': 'Data loading and preprocessing utilities.', + 'layers': 'Custom layers for building deep learning models.', + 'configs': 'Configuration classes for model and training settings.', + 'ops': 'Custom operations for deep learning models.', + 'losses': 'Custom loss functions for training deep learning models.', + 'models': 'Predefined deep learning models for various tasks.', + 'plot': 'Utilities for visualizing data and model outputs.', + '_internal': 'Internal utilities and functions for development and testing purposes.', +} + + +def get_api_symbols(): + """Returns a live reference to the global API symbols dictionary.""" + return _API_SYMBOLS + + +def get_submodule_names(): + """Returns a list of TFMRI submodule names.""" + return _SUBMODULE_NAMES + + +def get_symbol_from_name(name): + """Get API symbol from its name. + + Args: + name: Name of the symbol. + + Returns: + API symbol. + """ + return _API_SYMBOLS.get(name) + + +def get_symbols_in_submodule(name): + """Returns the symbols in the given submodule. + + Args: + name: Name of the submodule. + + Returns: + A dict containing the API symbols in the submodule. + """ + symbols = {} + for k, v in _API_SYMBOLS.items(): + if k.startswith(name): + symbols[k] = v + return symbols + + +def get_docstring_for_submodule(name): + """Returns the docstring for the given submodule. + + Args: + name: Name of the submodule. + + Returns: + The docstring for the submodule. + """ + return _SUBMODULE_DOCSTRINGS[name] + + +def get_canonical_name_for_symbol(symbol): + """Get canonical name for the API symbol. + + Args: + symbol: API function or class. + + Returns: + Canonical name for the API symbol. + """ + if not hasattr(symbol, '__dict__'): + return None + if _API_ATTR not in symbol.__dict__: + return None + + api_names = getattr(symbol, '_api_names') + # Canonical name is the first name in the list. + canonical_name = api_names[0] + + return canonical_name + + +def export(*names): + """Returns a decorator to export a symbol to the API. + + Args: + *names: List of API names under which the object should be exported. + + Returns: + A decorator to export a symbol to the API. + """ + def decorator(symbol): + """Decorator to export a symbol to the API. + + Args: + symbol: Symbol to decorate. + + Returns: + The input symbol with the `_api_names` attribute set. + + Raises: + ValueError: If the name is invalid or already used. + """ + for name in names: + # API name must have format "namespace.name". + if name.count('.') != 1: + raise ValueError(f"Invalid API name: {name}") + # API namespace must be one of the supported ones. + namespace, _ = name.split('.') + if namespace not in _SUBMODULE_NAMES: + raise ValueError(f"Invalid API namespace: {namespace}") + # API name must be unique. + if name in _API_SYMBOLS: + raise ValueError( + f"Name {name} already used for exported symbol {symbol}") + # Add symbol to the API symbols table. + _API_SYMBOLS[name] = symbol + # Set the _api_names attribute. + setattr(symbol, _API_ATTR, names) + return symbol + + return decorator + + +class APILoader(importlib.abc.Loader): # pylint: disable=abstract-method + """Loader for the public API.""" + def __init__(self, *args, **kwargs): + self._namespace = kwargs.pop('namespace') + super().__init__(*args, **kwargs) + + def exec_module(self, module): + """Executes the module. + + Args: + module: module. + """ + # Import public API. + for name, symbol in _API_SYMBOLS.items(): + namespace, name = name.split('.') + if namespace == self._namespace: + setattr(module, name, symbol) + + +def import_namespace(namespace): + """Imports a namespace. + + Args: + namespace: Namespace to import. + + Returns: + The imported module. + """ + spec = importlib.machinery.ModuleSpec( + f'tensorflow_mri.{namespace}', APILoader(namespace=namespace)) + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + module.__doc__ = _SUBMODULE_DOCSTRINGS[namespace] + return module diff --git a/im2sim/utils/contents.md b/im2sim/src/utils/contents.md similarity index 100% rename from im2sim/utils/contents.md rename to im2sim/src/utils/contents.md diff --git a/im2sim/utils/__init__.py b/im2sim/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..278580e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,126 @@ +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" + + +[project] +name = "im2sim" +version = "0.1.0" +description = "A framework for ML-accelerated digital twins" +readme = "README.rst" +requires-python = ">=3.10" +license = { text = "ElasticV2" } +authors = [ + { name = "Anirudh Raman", email = "anirudh.raman.24@ucl.ac.uk" } +] + +dependencies = [ + "torch==2.3.1", + "torch_geometric==2.7.0", + "pyvista==0.47.0", + "numpy", + "matplotlib", + "scikit-learn" +] + + +[project.optional-dependencies] +dev = [ + "pytest", + "pytest-cov", + "ruff", + "pre-commit", +] +docs = [ + "sphinx", + "sphinx-autodoc-typehints", + "sphinx-last-updated-by-git", + "sphinx-sitemap", + "sphinx_rtd_theme", + "myst-nb", + "myst-parser", + "shibuya", +] + +[tool.setuptools.packages.find] +where = ["im2sim"] + + +# ------------------------- +# Ruff configuration +# ------------------------- + +[tool.ruff] +line-length = 100 +target-version = "py310" + +exclude = [ + ".git", + ".venv", + "venv", + "build", + "dist", + "__pycache__", +] + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "F", # pyflakes + "I", # import sorting + "B", # bugbear + "UP", # pyupgrade + "SIM", # simplify +] + +ignore = [ + "E501", # handled by formatter +] + +fixable = [ + "ALL", +] + +[tool.ruff.lint.isort] +known-first-party = [ + "im2sim", +] + + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +line-ending = "auto" + + +# ------------------------- +# Pytest configuration +# ------------------------- + +[tool.pytest.ini_options] +testpaths = [ + "tests", +] + +addopts = [ + "-ra", + "--strict-markers", +] + + +# ------------------------- +# Coverage configuration +# ------------------------- + +[tool.coverage.run] +branch = true +source = [ + "im2sim", +] + +[tool.coverage.report] +show_missing = true +exclude_lines = [ + "if TYPE_CHECKING:", + "pragma: no cover", +] \ No newline at end of file diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..5e5d5cf --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +filterwarnings = + ignore:.*torch_geometric\.distributed.*:DeprecationWarning \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index d2ba424..d8c6e00 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ torch==2.3.1 torch-cluster==1.6.3 +torch-scatter==2.1.2 torch_geometric==2.7.0 pyvista==0.47.0 numpy diff --git a/setup.py b/setup.py deleted file mode 100644 index c086f4a..0000000 --- a/setup.py +++ /dev/null @@ -1,7 +0,0 @@ -from setuptools import setup, find_packages - -setup( - name="im2sim", - version="0.1.0", - packages=find_packages(), # auto-finds your Python packages -) \ No newline at end of file diff --git a/tests/integration/data/test_data_pipeline.py b/tests/integration/data/test_data_pipeline.py new file mode 100644 index 0000000..ad057be --- /dev/null +++ b/tests/integration/data/test_data_pipeline.py @@ -0,0 +1,148 @@ +import copy + +import pytest +import torch +from torch_geometric.data import Data + +from im2sim.src.data import Dataset, Pipeline, transforms + + +def make_toy_dataset(): + """ + Mimics: + input.x -> model inputs + gt.x -> targets + + but with tiny synthetic data. + """ + + samples = {} + cases = ["1", "2", "3", "4", "5"] + for case in cases: + num_nodes = 10 + + sample = { + "input": Data( + x=torch.randn(num_nodes, 7), + edge_index=torch.tensor( + [ + [0, 1, 2, 3], + [1, 2, 3, 4], + ] + ), + ), + "gt": Data( + x=torch.randn(num_nodes, 4) * 10, + edge_index=torch.tensor( + [ + [0, 1, 2, 3], + [1, 2, 3, 4], + ] + ), + ), + } + + samples[case] = sample + + def load(case): + return samples[case] + + ds = Dataset(load, cases) + + return ds + + +@pytest.fixture +def pipeline(): + + dataset = make_toy_dataset() + + pipeline = Pipeline( + [ + transforms.PowerScaling( + exp=1 / 3, + preserve_sign=True, + keys=["gt"], + attr="x", + channels=[0], + ), + transforms.FitZScore( + keys=["gt"], + attr="x", + channels=[0, 1, 2, 3], + per_channel=True, + ), + transforms.FitZScore( + keys=["input"], + attr="x", + channels=[0, 1, 2, 3, 4, 5, 6], + per_channel=True, + ), + ] + ) + + pipeline.fit(dataset) + + return pipeline + + +def test_pipeline_forward(pipeline): + + sample = make_toy_dataset()[0] + + transformed = pipeline(sample) + + assert transformed["input"].x.shape == (10, 7) + assert transformed["gt"].x.shape == (10, 4) + + assert torch.isfinite(transformed["input"].x).all() + assert torch.isfinite(transformed["gt"].x).all() + + +def test_pipeline_inverse(pipeline): + + sample = make_toy_dataset()[0] + + # keep a copy before modification + original = {key: value.x.clone() for key, value in sample.items()} + + transformed = pipeline(sample) + + recovered = pipeline.inverse(transformed) + + for key in ["input", "gt"]: + torch.testing.assert_close( + recovered[key].x, + original[key], + rtol=1e-5, + atol=1e-6, + ) + + +def test_integrated_pipeline(pipeline): + + dataset1 = make_toy_dataset() + dataset2 = copy.deepcopy(dataset1) + + dataset1.add_transforms(pipeline) + + sample1 = dataset1[0] + recovered = pipeline.inverse(sample1) + + sample2 = dataset2[0] + transformed = pipeline(sample2) + + for key in ["input", "gt"]: + torch.testing.assert_close( + recovered[key].x, + sample2[key].x, + rtol=1e-5, + atol=1e-6, + ) + + torch.testing.assert_close( + sample1[key].x, + transformed[key].x, + rtol=1e-5, + atol=1e-6, + ) diff --git a/tests/integration/data/test_pv_mesh_utils.py b/tests/integration/data/test_pv_mesh_utils.py new file mode 100644 index 0000000..6f1b046 --- /dev/null +++ b/tests/integration/data/test_pv_mesh_utils.py @@ -0,0 +1,238 @@ +import hypothesis.strategies as st +import numpy as np +import pyvista as pv +from hypothesis import given +from torch_geometric.data import Data + +from im2sim.src.data.mesh_utils import * + + +def build_tetra_mesh(): + points = np.array( + [ + [0, 0, 0], # 0 + [1, 0, 0], # 1 + [0, 1, 0], # 2 + [0, 0, 1], # 3 + [1, 1, 1], # 4 (second tetra apex) + ] + ).astype(np.float32) + + # Two tetrahedra + cells = np.hstack( + [ + [4, 0, 1, 2, 3], # tetra 1 + [4, 1, 2, 3, 4], # tetra 2 + ] + ) + + celltypes = np.array([pv.CellType.TETRA, pv.CellType.TETRA]) + grid = pv.UnstructuredGrid(cells, celltypes, points) + + grid["CellEntityIds"] = np.array([0] * 2) # Assign the same entity ID to both tetrahedra + grid["vtkOriginalPointIds"] = np.arange(len(points)) + return grid + + +def build_triangle_mesh(): + # 4 points forming a square + points = np.array( + [ + [0, 0, 0], # 0 + [1, 0, 0], # 1 + [1, 1, 0], # 2 + [0, 1, 0], # 3 + ] + ).astype(np.float32) + + # Two triangle faces: (0,1,2) and (0,2,3) + faces = np.hstack( + [ + [3, 0, 1, 2], + [3, 0, 2, 3], + ] + ) + grid = pv.PolyData(points, faces) + grid["CellEntityIds"] = np.array([0] * 2) # Assign the same entity ID to both tetrahedra + grid["vtkOriginalPointIds"] = np.arange(len(points)) + return grid + + +def make_square_mesh(): + # Four nodes in a square + x = torch.tensor( + [ + [0.0, 0.0], + [1.0, 0.0], + [1.0, 1.0], + [0.0, 1.0], + ] + ) + + # Undirected edges + edge_index = torch.tensor( + [ + [0, 1, 2, 3, 0, 1, 2, 3], + [1, 0, 3, 2, 3, 2, 1, 0], + ] + ) + + return Data(x=x, edge_index=edge_index) + + +def to_set(elems): + """ + Convert edge/cell array to set of sorted tuples. + Works for edges/cells + - (N, m) + where N is number of edges/cells and m is nodes per edge/cell + + """ + return {tuple(sorted(e)) for e in elems} + + +def test_get_edges_surf(): + mesh = build_triangle_mesh() + + edges = get_edges_surf(mesh).numpy().T + print(edges.shape) + edge_set = to_set(edges) + + expected = np.array( + [[0, 1], [1, 2], [0, 2], [2, 3], [0, 3], [1, 0], [2, 1], [2, 0], [3, 2], [3, 0]] + ) + + expected = to_set(expected) + + assert edge_set == expected + assert len(edge_set) == 5 + + +def test_get_edges_tet(): + mesh = build_tetra_mesh() + + edges = get_edges_tet(mesh).numpy().T + edge_set = to_set(edges) + + expected = np.array( + [ + [0, 1], + [0, 2], + [0, 3], + [1, 2], + [1, 3], + [2, 3], + [1, 4], + [2, 4], + [3, 4], + [1, 0], + [2, 0], + [3, 0], + [2, 1], + [3, 1], + [3, 2], + [4, 1], + [4, 2], + [4, 3], + ] + ) + + expected = to_set(expected) + + assert edge_set == expected + assert len(edge_set) == 9 + + +def test_get_structure_cells(): + tet_mesh = build_tetra_mesh() + + tet_cells = get_structure_cells(tet_mesh, {0: "vol"})["vol_cell_index"].numpy().T + + tet_cell_set = to_set(tet_cells) + + tet_expected = np.array([[0, 1, 2, 3], [1, 2, 3, 4]]) + + tet_expected = to_set(tet_expected) + + assert tet_cell_set == tet_expected + assert len(tet_cell_set) == 2 + + +def test_get_structure_ids(): + tet_mesh = build_tetra_mesh() + + tet_ids = set(get_structure_ids(tet_mesh, {0: "vol"})["vol_index"].numpy().tolist()) + + tet_expected = {0, 1, 2, 3, 4} + + assert tet_ids == tet_expected + assert len(tet_ids) == 5 + + +def test_cluster_pool_reduces_nodes(): + mesh = make_square_mesh() + + pooled = cluster_pool(mesh) + + assert isinstance(pooled, Data) + assert pooled.x.shape[1] == mesh.x.shape[1] + assert pooled.x.shape[0] <= mesh.x.shape[0] + assert pooled.edge_index.shape[0] == 2 + + +def test_cluster_pool_preserves_feature_range(): + mesh = make_square_mesh() + + pooled = cluster_pool(mesh) + + # Averaging cannot create values outside the original range + assert torch.all(pooled.x >= mesh.x.min()) + assert torch.all(pooled.x <= mesh.x.max()) + + +def test_cluster_pool_handles_zero_length_edges(): + mesh = Data( + x=torch.tensor([[0.0, 0.0], [0.0, 0.0], [1.0, 0.0]]), + edge_index=torch.tensor([[0, 1, 1], [1, 0, 2]]), + ) + + pooled = cluster_pool(mesh) + + assert torch.isfinite(pooled.x).all() + + +def test_rasterize_output_shape(): + points = torch.tensor([[0.0, 0.0, 0.0]]) + + result = rasterize(points, im_shape=[4, 4, 4], vox_sizes=[1.0, 1.0, 1.0]) + + assert result.shape == (4, 4, 4) + + +def test_rasterize_single_point_minimum(): + + points = torch.tensor([[0.5, 0.5, 0.5]]) + + result = rasterize(points, im_shape=[4, 4, 4], vox_sizes=[1.0, 1.0, 1.0]) + + assert result[0, 0, 0] == 0 + + +def test_rasterize_two_points(): + + points = torch.tensor([[0.0, 0.0, 0.0], [3.0, 3.0, 3.0]]) + + result = rasterize(points, im_shape=[4, 4, 4], vox_sizes=[1.0, 1.0, 1.0]) + + assert result[0, 0, 0] == result[-1, -1, -1] + assert result[0, 0, 0] < result[1, 1, 1] + + +@given(st.integers(min_value=1, max_value=10)) +def test_rasterize_never_returns_negative(n): + + points = torch.rand(n, 3) + + result = rasterize(points, [8, 8, 8], [1.0, 1.0, 1.0]) + + assert torch.all(result >= 0) diff --git a/tests/unit/data/test_loader.py b/tests/unit/data/test_loader.py new file mode 100644 index 0000000..2be2e25 --- /dev/null +++ b/tests/unit/data/test_loader.py @@ -0,0 +1,127 @@ +import pytest +import torch +from torch_geometric.data import Batch, Data + +from im2sim.src.data import DataLoader, Dataset, collate + +# ------------------------- +# Fixtures +# ------------------------- + + +@pytest.fixture +def tensor_sample(): + return { + "x": torch.randn(3, 4), + "y": torch.randn(1), + } + + +@pytest.fixture +def pyg_sample(): + return {"graph": Data(x=torch.randn(5, 3), edge_index=torch.tensor([[0, 1], [1, 2]]))} + + +@pytest.fixture +def mixed_sample(): + return { + "x": torch.randn(3, 4), + "graph": Data(x=torch.randn(5, 3), edge_index=torch.tensor([[0, 1], [1, 2]])), + } + + +# ------------------------- +# collate tests +# ------------------------- + + +def test_collate_tensors(tensor_sample): + batch = [tensor_sample, tensor_sample] + out = collate(batch) + + assert isinstance(out, dict) + assert all(isinstance(v, torch.Tensor) for v in out.values()) + assert out["x"].shape[0] == 2 # batched dimension + + +def test_collate_pyg(pyg_sample): + batch = [pyg_sample, pyg_sample] + out = collate(batch) + + assert isinstance(out["graph"], Batch) + assert out["graph"].num_graphs == 2 + + +def test_collate_mixed(mixed_sample): + batch = [mixed_sample, mixed_sample] + out = collate(batch) + + assert isinstance(out["x"], torch.Tensor) + assert isinstance(out["graph"], Batch) + + +def test_collate_invalid_type(): + batch = [{"bad": "not allowed"}, {"bad": "still not allowed"}] + + with pytest.raises(TypeError): + collate(batch) + + +# ------------------------- +# Dataset tests +# ------------------------- + + +def dummy_load_fn(case: str): + return {"x": torch.tensor([len(case)])} + + +def test_dataset_len(): + cases = ["a", "bb", "ccc"] + ds = Dataset(load_fn=dummy_load_fn, cases=cases) + + assert len(ds) == 3 + + +def test_dataset_getitem(): + cases = ["a"] + ds = Dataset(load_fn=dummy_load_fn, cases=cases) + + sample = ds[0] + assert isinstance(sample, dict) + assert torch.equal(sample["x"], torch.tensor([1])) + + +def test_dataset_no_transforms(): + ds = Dataset(load_fn=dummy_load_fn, cases=["a"], transforms=None) + sample = ds[0] + + assert "x" in sample # should still work + + +# ------------------------- +# DataLoader tests +# ------------------------- + + +def test_dataloader_basic(): + ds = Dataset(load_fn=dummy_load_fn, cases=["a", "bb", "ccc"]) + + loader = DataLoader(ds, batch_size=2) + + batch = next(iter(loader)) + assert isinstance(batch, dict) + assert "x" in batch + assert batch["x"].shape[0] == 2 + + +def test_dataloader_with_pyg(): + def load_fn(case): + return {"graph": Data(x=torch.randn(3, 2), edge_index=torch.tensor([[0, 1], [1, 2]]))} + + ds = Dataset(load_fn=load_fn, cases=["a", "b", "c"]) + loader = DataLoader(ds, batch_size=2) + + batch = next(iter(loader)) + assert isinstance(batch["graph"], Batch) + assert batch["graph"].num_graphs == 2 diff --git a/tests/unit/data/test_mesh_utils.py b/tests/unit/data/test_mesh_utils.py new file mode 100644 index 0000000..7bef03d --- /dev/null +++ b/tests/unit/data/test_mesh_utils.py @@ -0,0 +1,78 @@ +import torch + +from im2sim.src.data.mesh_utils import * + + +def test_make_padded_batch_shapes(): + x = torch.tensor([[1], [2], [3], [4], [5]]) + batch = torch.tensor([0, 0, 0, 1, 2]) + + padded_x, mask = make_padded_batch(x, batch) + + assert padded_x.shape == (3, 3, 1) + assert mask.shape == (3, 3) + + +def test_make_padded_batch_mask_correct(): + x = torch.randn(5, 2) + batch = torch.tensor([0, 0, 0, 1, 2]) + + _, mask = make_padded_batch(x, batch) + + expected = torch.tensor( + [ + [True, True, True], + [True, False, False], + [True, False, False], + ] + ) + + assert torch.equal(mask, expected) + + +def test_compute_edge_lengths_simple(): + points = torch.tensor( + [ + [0.0, 0.0], + [3.0, 4.0], + ] + ) + edges = torch.tensor([[0], [1]]) + + distances = compute_edge_lengths(points, edges) + + assert torch.allclose(distances, torch.tensor([5.0])) + + +def test_hard_threshold(): + y = torch.tensor([0.5, 1.0, 1.5]) + + out = hard_threshold(y, threshold=1.0) + + expected = torch.tensor([1.0, 0.0, 0.0]) + assert torch.equal(out, expected) + + +def test_soft_threshold_range(): + y = torch.tensor([0.0, 1.5, 3.0]) + out = soft_threshold(y) + + assert torch.all(out >= 0) + assert torch.all(out <= 1) + + +def test_soft_threshold_behavior(): + y_low = torch.tensor([0.0]) + y_high = torch.tensor([10.0]) + + assert soft_threshold(y_low) > soft_threshold(y_high) + + +def test_set_attrs(): + data = Data() + attrs = {"x": torch.tensor([[1.0]]), "edge_index": torch.tensor([[0], [0]])} + + set_attrs(data, attrs) + + assert torch.equal(data.x, attrs["x"]) + assert torch.equal(data.edge_index, attrs["edge_index"]) diff --git a/tests/unit/data/test_ops.py b/tests/unit/data/test_ops.py new file mode 100644 index 0000000..1434c7e --- /dev/null +++ b/tests/unit/data/test_ops.py @@ -0,0 +1,90 @@ +import numpy as np + +from im2sim.src.data.ops import * + +eps = 1e-8 + + +def test_normtorange_basic(): + x = np.array([0, 5, 10]) + result = normtorange(x, min=0, max=10, a=0, b=1) + expected = np.array([0.0, 0.5, 1.0]) + assert np.allclose(result, expected) + + +def test_normtorange_auto_min_max(): + x = np.array([2, 4, 6]) + result = normtorange(x) + expected = np.array([0.0, 0.5, 1.0]) + assert np.allclose(result, expected) + + +def test_normtorange_custom_range(): + x = np.array([0, 5, 10]) + result = normtorange(x, min=0, max=10, a=-1, b=1) + expected = np.array([-1.0, 0.0, 1.0]) + assert np.allclose(result, expected) + + +def test_inv_normtorange_basic(): + x = np.array([0.0, 0.5, 1.0]) + result = inv_normtorange(x, min=0, max=10, a=0, b=1) + expected = np.array([0, 5, 10]) + assert np.allclose(result, expected) + + +def test_norm_inverse_consistency(): + x = np.random.rand(10) + normed = normtorange(x) + recovered = inv_normtorange(normed, min=x.min(), max=x.max()) + assert np.allclose(x, recovered) + + +def test_normalise_wrapper(): + x = np.array([1, 2, 3]) + assert np.allclose(normalise(x), normtorange(x)) + + +def test_inv_normalise_wrapper(): + x = np.array([0.0, 0.5, 1.0]) + assert np.allclose(inv_normalise(x, 0, 10), inv_normtorange(x, 0, 10)) + + +def test_standardise_basic(): + x = np.array([1, 2, 3]) + result = standardise(x) + assert np.isclose(result.mean(), 0.0, atol=1e-7) + assert np.isclose(result.std(), 1.0, atol=1e-7) + + +def test_standardise_with_given_params(): + x = np.array([1, 2, 3]) + result = standardise(x, mean=2, std=1) + expected = np.array([-1, 0, 1]) + assert np.allclose(result, expected) + + +def test_inv_standardise_basic(): + x = np.array([-1, 0, 1]) + result = inv_standardise(x, mean=2, std=1) + expected = np.array([1, 2, 3]) + assert np.allclose(result, expected) + + +def test_standardise_inverse_consistency(): + x = np.random.rand(10) + mean = x.mean() + std = x.std() + standardised = standardise(x, mean, std) + recovered = inv_standardise(standardised, mean, std) + assert np.allclose(x, recovered) + + +def test_zero_variance_handling(): + x = np.array([5, 5, 5]) + result = standardise(x) + # should not produce NaN due to eps + assert not np.any(np.isnan(result)) + + +################## FITTABLE OPS ################# diff --git a/tests/unit/data/test_pipeline.py b/tests/unit/data/test_pipeline.py new file mode 100644 index 0000000..964bfd0 --- /dev/null +++ b/tests/unit/data/test_pipeline.py @@ -0,0 +1,292 @@ +import copy + +import pytest +import torch + +from im2sim.src.data import ( + FittableOperation, + InvertibleOperation, + Operation, + Pipeline, + Transform, + register_op, +) + +# ----------------------------- +# Helpers / Dummy Ops +# ----------------------------- + + +class DummyOp(Operation): + def forward(self, x): + return x + 1 + + +class DummyInvertibleOp(InvertibleOperation): + def forward(self, x): + return x + 1 + + def inverse(self, x): + return x - 1 + + +@register_op +class DummyFittableOp(FittableOperation): + def __init__(self): + self.total = 0 + self.count = 0 + + def fit_step(self, x): + self.total += x.sum() + self.count += x.numel() + + def complete_fit(self): + self.mean = self.total / self.count + + def forward(self, x): + return x - self.mean + + def inverse(self, x): + return x + self.mean + + +# ----------------------------- +# Transform Tests +# ----------------------------- + + +def test_transform_single_key_forward(): + t = Transform(DummyOp(), keys="a") + data = {"a": torch.tensor([1.0])} + + out = t.forward(data) + assert torch.allclose(out["a"], torch.tensor([2.0])) + + +def test_transform_multiple_keys(): + t = Transform(DummyOp(), keys=["a", "b"]) + data = {"a": torch.tensor([1.0]), "b": torch.tensor([2.0])} + + out = t.forward(data) + assert torch.allclose(out["a"], torch.tensor([2.0])) + assert torch.allclose(out["b"], torch.tensor([3.0])) + + +def test_transform_missing_attr(): + t = Transform(DummyOp(), keys="a", attr="missing") + data = {"a": object()} + + with pytest.raises(ValueError): + t.forward(data) + + +# ----------------------------- +# Channel Handling +# ----------------------------- + + +def test_transform_channel_subset(): + t = Transform(DummyOp(), keys="a", channels=[0], channel_dim=0) + data = {"a": torch.tensor([[1.0], [2.0]])} + + out = t.forward(data) + assert out["a"][0] == 2.0 + assert out["a"][1] == 2.0 # unchanged + + +def test_transform_per_channel(): + t = Transform(DummyOp(), keys="a", per_channel=True, channel_dim=0) + data = {"a": torch.tensor([[1.0], [2.0]])} + + out = t.forward(data) + assert torch.allclose(out["a"], torch.tensor([[2.0], [3.0]])) + + +# ----------------------------- +# Fittable Transform +# ----------------------------- + + +def test_fittable_requires_fit(): + t = Transform(DummyFittableOp(), keys="a") + data = {"a": torch.tensor([1.0])} + + with pytest.raises(RuntimeError): + t.forward(data) + + +def test_fittable_fit_and_forward(): + t = Transform(DummyFittableOp(), keys="a") + + dataset = [{"a": torch.tensor([1.0])}, {"a": torch.tensor([3.0])}] + + class DummyLoader: + def __iter__(self): + return iter(dataset) + + t.fit(DummyLoader()) + + out = t.forward({"a": torch.tensor([3.0])}) + assert torch.allclose(out["a"], torch.tensor([3.0 - 2.0])) + + +# ----------------------------- +# Invertibility +# ----------------------------- + + +def test_invertible_transform(): + t = Transform(DummyInvertibleOp(), keys="a") + t.is_invertible = True # simulate isinstance check + + data = {"a": torch.tensor([5.0])} + + out = t.forward(copy.deepcopy(data)) + inv = t.inverse(out) + + assert torch.allclose(inv["a"], data["a"]) + + +def test_non_invertible_raises(): + t = Transform(DummyOp(), keys="a") + data = {"a": torch.tensor([1.0])} + + with pytest.raises(RuntimeError): + t.inverse(data) + + +# ----------------------------- +# Pipeline Tests +# ----------------------------- + + +def test_pipeline_sequential(): + t1 = Transform(DummyOp(), keys="a") + t2 = Transform(DummyOp(), keys="a") + + p = Pipeline([t1, t2]) + data = {"a": torch.tensor([1.0])} + + out = p(data) + assert torch.allclose(out["a"], torch.tensor([3.0])) + + +def test_pipeline_inverse_order(): + t1 = Transform(DummyInvertibleOp(), keys="a") + t1.is_invertible = True + t2 = Transform(DummyInvertibleOp(), keys="a") + t2.is_invertible = True + + p = Pipeline([t1, t2]) + data = {"a": torch.tensor([1.0])} + + out = p(copy.deepcopy(data)) + inv = p.inverse(out) + + assert torch.allclose(inv["a"], data["a"]) + + +def test_pipeline_skips_missing_keys(): + t = Transform(DummyOp(), keys="b") + p = Pipeline([t]) + + data = {"a": torch.tensor([1.0])} + out = p(data) + + assert "a" in out + assert "b" not in out + + +# ----------------------------- +# Pipeline Fit +# ----------------------------- + + +def test_pipeline_fit_only_fittable(): + t1 = Transform(DummyOp(), keys="a") + t2 = Transform(DummyFittableOp(), keys="a") + + dataset = [{"a": torch.tensor([1.0])}, {"a": torch.tensor([3.0])}] + + class DummyDataset: + def __init__(self): + self.data = dataset + self.transforms = [] + + def __len__(self): + return len(self.data) + + def __getitem__(self, idx): + item = self.data[idx] + for t in self.transforms: + item = t(item) + return item + + p = Pipeline([t1, t2]) + p.fit(DummyDataset()) + + assert t2.fitted is True + + +# ----------------------------- +# Serialization +# ----------------------------- + + +def test_transform_state_dict_roundtrip(): + t = Transform(DummyFittableOp(), keys="a") + + dataset = [{"a": torch.tensor([2.0])}] + + class DummyLoader: + def __iter__(self): + return iter(dataset) + + t.fit(DummyLoader()) + state = t.state_dict() + + t2 = Transform(DummyFittableOp(), keys="a") + t2.load_state_dict(state) + + assert t2.fitted is True + + +def test_pipeline_state_dict_roundtrip(tmp_path): + t = Transform(DummyFittableOp(), keys="a") + + dataset = [{"a": torch.tensor([2.0])}] + + class DummyLoader: + def __iter__(self): + return iter(dataset) + + t.fit(DummyLoader()) + + p = Pipeline([t]) + path = tmp_path / "pipeline.pt" + + torch.save({"config": p.config(), "state": p.state_dict()}, path) + + obj = torch.load(path) + p2 = Pipeline.from_config(obj["config"]) + p2.load_state_dict(obj["state"]) + + assert list(p2.state_dict().keys()) == list(p.state_dict().keys()) + + +# ----------------------------- +# Edge Cases +# ----------------------------- + + +def test_empty_pipeline(): + p = Pipeline([]) + data = {"a": torch.tensor([1.0])} + + out = p(data) + assert out == data + + +def test_no_keys_transform(): + with pytest.raises(ValueError): + Transform(DummyOp(), keys=[]) diff --git a/tests/unit/layers/test_graph_blocks.py b/tests/unit/layers/test_graph_blocks.py new file mode 100644 index 0000000..fb87c27 --- /dev/null +++ b/tests/unit/layers/test_graph_blocks.py @@ -0,0 +1,279 @@ +import pytest +import torch +from torch_geometric.data import Data + +from im2sim.src.layers import GraphConvBlock, GraphConvResBlock + + +@pytest.fixture +def graph(): + """ + Simple graph: + 0 -- 1 + | | + 2 -- 3 + """ + x = torch.randn(4, 8) + + edge_index = torch.tensor( + [ + [0, 1, 2, 3, 0, 2], + [1, 0, 3, 2, 2, 0], + ], + dtype=torch.long, + ) + + return Data( + x=x, + edge_index=edge_index, + ) + + +graph_blocks = {"GraphConv": GraphConvBlock, "GraphRes": GraphConvResBlock} + + +@pytest.mark.parametrize( + "conv_type", + [ + "GCNConv", + "GATConv", + ], +) +@pytest.mark.parametrize( + "graph_block_type", + [ + "GraphConv", + "GraphRes", + ], +) +def test_forward_shape(graph, conv_type, graph_block_type): + filters = 16 + + model = graph_blocks[graph_block_type]( + in_channels=8, + filters=filters, + depth=2, + conv_type=conv_type, + conv_kwargs={}, + ) + + print(type(model), type(graph)) + out = model(graph) + + assert out.x.shape == (graph.num_nodes, filters) + + +@pytest.mark.parametrize("depth", [1, 2, 4]) +@pytest.mark.parametrize( + "graph_block_type", + [ + "GraphConv", + "GraphRes", + ], +) +def test_depth_changes_number_of_layers(depth, graph_block_type): + model = graph_blocks[graph_block_type]( + in_channels=8, + filters=16, + depth=depth, + conv_type="GCNConv", + conv_kwargs={}, + ) + + assert len(model.convs) == depth + assert len(model.norms) == depth + + +@pytest.mark.parametrize( + "graph_block_type", + [ + "GraphConv", + "GraphRes", + ], +) +def test_graph_structure_is_preserved(graph, graph_block_type): + model = graph_blocks[graph_block_type]( + in_channels=8, + filters=16, + conv_type="GCNConv", + conv_kwargs={}, + ) + + out = model(graph) + + assert torch.equal( + out.edge_index, + graph.edge_index, + ) + + +@pytest.mark.parametrize( + "graph_block_type", + [ + "GraphConv", + "GraphRes", + ], +) +def test_input_graph_is_not_modified(graph, graph_block_type): + model = graph_blocks[graph_block_type]( + in_channels=8, + filters=16, + conv_type="GCNConv", + conv_kwargs={}, + ) + + original_x = graph.x.clone() + + _ = model(graph) + + assert torch.allclose( + graph.x, + original_x, + ) + + +@pytest.mark.parametrize( + "graph_block_type", + [ + "GraphConv", + "GraphRes", + ], +) +def test_output_is_finite(graph, graph_block_type): + model = graph_blocks[graph_block_type]( + in_channels=8, + filters=16, + conv_type="GCNConv", + conv_kwargs={}, + ) + + out = model(graph) + + assert torch.isfinite(out.x).all() + + +@pytest.mark.parametrize( + "graph_block_type", + [ + "GraphConv", + "GraphRes", + ], +) +def test_gradients_flow(graph, graph_block_type): + graph.x.requires_grad_(True) + + model = graph_blocks[graph_block_type]( + in_channels=8, + filters=16, + depth=2, + conv_type="GCNConv", + conv_kwargs={}, + ) + + out = model(graph) + + loss = out.x.sum() + loss.backward() + + assert graph.x.grad is not None + assert torch.isfinite(graph.x.grad).all() + + for name, param in model.named_parameters(): + assert param.grad is not None, f"{name} has no gradient" + assert torch.isfinite(param.grad).all() + + +@pytest.mark.parametrize( + "graph_block_type", + [ + "GraphConv", + "GraphRes", + ], +) +@pytest.mark.parametrize( + "activation", + ["ReLU", "leakyrelu", "gelu", "sigmoid", None], +) +def test_supported_activations(graph, activation, graph_block_type): + model = graph_blocks[graph_block_type]( + in_channels=8, + filters=16, + activation=activation, + conv_type="GCNConv", + conv_kwargs={}, + ) + + out = model(graph) + + assert out.x.shape == (4, 16) + + +@pytest.mark.parametrize( + "graph_block_type", + [ + "GraphConv", + "GraphRes", + ], +) +def test_no_normalisation(graph, graph_block_type): + model = graph_blocks[graph_block_type]( + in_channels=8, + filters=16, + norm_type=None, + conv_type="GCNConv", + conv_kwargs={}, + ) + + assert isinstance( + model.norms[0], + torch.nn.Identity, + ) + + out = model(graph) + + assert out.x.shape == (4, 16) + + +@pytest.mark.parametrize( + "graph_block_type", + [ + "GraphConv", + "GraphRes", + ], +) +def test_serialisation(graph, tmp_path, graph_block_type): + model = graph_blocks[graph_block_type]( + in_channels=8, + filters=16, + conv_type="GCNConv", + conv_kwargs={}, + ) + + model.eval() + + before = model(graph).x + + path = tmp_path / "model.pt" + + torch.save( + model.state_dict(), + path, + ) + + model2 = graph_blocks[graph_block_type]( + in_channels=8, + filters=16, + conv_type="GCNConv", + conv_kwargs={}, + ) + + model2.load_state_dict(torch.load(path)) + + model2.eval() + + after = model2(graph).x + + assert torch.allclose( + before, + after, + ) diff --git a/tests/unit/layers/test_image_blocks.py b/tests/unit/layers/test_image_blocks.py new file mode 100644 index 0000000..47ee50b --- /dev/null +++ b/tests/unit/layers/test_image_blocks.py @@ -0,0 +1,632 @@ +import pytest +import torch +import torch.nn as nn + +from im2sim.src.layers import ( + ImageConvBlock, + ImageConvResBlock, + ImageEncoder, + ImageResEncoder, + ImageDecoder, +) + + +# --------------------------------------------------------- +# Fixtures +# --------------------------------------------------------- + + +@pytest.fixture +def image_2d(): + # BCHW + return torch.randn(2, 3, 64, 64, requires_grad=True) + + +@pytest.fixture +def image_3d(): + # BCDHW + return torch.randn(2, 3, 32, 32, 32, requires_grad=True) + + +@pytest.fixture +def small_filters(): + return (8, 16, 32) + + +# --------------------------------------------------------- +# ImageConvBlock +# --------------------------------------------------------- + + +@pytest.mark.parametrize( + "rank,input_shape,expected_spatial", + [ + (2, (2, 3, 64, 64), (64, 64)), + (3, (2, 3, 32, 32, 32), (32, 32, 32)), + ], +) +def test_image_conv_block_shapes(rank, input_shape, expected_spatial): + + model = ImageConvBlock( + in_channels=3, + filters=16, + depth=2, + rank=rank, + ) + + x = torch.randn(*input_shape) + + y = model(x) + + assert y.shape == ( + input_shape[0], + 16, + *expected_spatial, + ) + + +@pytest.mark.parametrize( + "activation", + [ + "ReLU", + "relu", + "gelu", + "sigmoid", + None, + ], +) +def test_image_conv_block_activations(activation): + + model = ImageConvBlock( + in_channels=3, + filters=8, + rank=2, + activation=activation, + ) + + x = torch.randn(1, 3, 32, 32) + + y = model(x) + + assert y.shape == (1, 8, 32, 32) + + +@pytest.mark.parametrize( + "norm", + [ + None, + "BatchNorm", + "InstanceNorm", + ], +) +def test_image_conv_block_normalisation(norm): + + model = ImageConvBlock( + in_channels=3, + filters=8, + rank=2, + norm_type=norm, + ) + + x = torch.randn(2, 3, 32, 32) + + y = model(x) + + assert y.shape == (2, 8, 32, 32) + + +def test_image_conv_block_dropout(): + + model = ImageConvBlock( + in_channels=3, + filters=8, + rank=2, + dropout_rate=0.5, + ) + + assert isinstance(model.drop, nn.Dropout2d) + + x = torch.randn(2, 3, 32, 32) + + model.train() + + y1 = model(x) + y2 = model(x) + + assert not torch.equal(y1, y2) + + +def test_image_conv_block_gradients(): + + model = ImageConvBlock( + in_channels=3, + filters=8, + rank=2, + depth=3, + ) + + x = torch.randn( + 2, + 3, + 32, + 32, + requires_grad=True, + ) + + y = model(x) + + loss = y.mean() + + loss.backward() + + assert x.grad is not None + + for p in model.parameters(): + assert p.grad is not None + + +def test_image_conv_block_parameterisation(): + + model = ImageConvBlock( + in_channels=3, + filters=16, + depth=4, + kernel_size=5, + rank=2, + ) + + # 4 convolution layers + assert len(model.convs) == 4 + + params = sum(p.numel() for p in model.parameters()) + + assert params > 0 + + +# --------------------------------------------------------- +# Residual Block +# --------------------------------------------------------- + + +def test_res_block_shape(): + + model = ImageConvResBlock( + in_channels=3, + filters=16, + rank=2, + ) + + x = torch.randn( + 2, + 3, + 64, + 64, + ) + + y = model(x) + + assert y.shape == ( + 2, + 16, + 64, + 64, + ) + + +def test_res_block_gradient(): + + model = ImageConvResBlock( + in_channels=3, + filters=8, + rank=2, + ) + + x = torch.randn( + 1, + 3, + 32, + 32, + requires_grad=True, + ) + + y = model(x) + + y.mean().backward() + + assert x.grad is not None + + +def test_res_block_depth_parameter(): + + model = ImageConvResBlock( + in_channels=3, + filters=8, + depth=5, + ) + + # main block receives depth-2 layers + assert len(model.main_conv.convs) == 3 + + +# --------------------------------------------------------- +# Encoder +# --------------------------------------------------------- + + +@pytest.mark.parametrize( + "rank", + [2, 3], +) +def test_image_encoder_shapes(rank, small_filters): + + model = ImageEncoder( + in_channels=3, + filters=small_filters, + rank=rank, + ) + + if rank == 2: + x = torch.randn(1, 3, 64, 64) + + expected = [ + (1, 8, 64, 64), + (1, 16, 32, 32), + (1, 32, 16, 16), + ] + + else: + x = torch.randn( + 1, + 3, + 32, + 32, + 32, + ) + + expected = [ + (1, 8, 32, 32, 32), + (1, 16, 16, 16, 16), + (1, 32, 8, 8, 8), + ] + + outputs = model(x) + + assert len(outputs) == len(expected) + + for out, shape in zip(outputs, expected): + assert out.shape == shape + + +def test_encoder_parameters(): + + model = ImageEncoder( + in_channels=3, + filters=(8, 16), + rank=2, + conv_blocks_per_level=2, + ) + + assert len(model.conv_blocks) == 2 + + params = sum(p.numel() for p in model.parameters()) + + assert params > 0 + + +# --------------------------------------------------------- +# Residual Encoder +# --------------------------------------------------------- + + +def test_res_encoder_outputs(): + + model = ImageResEncoder( + in_channels=3, + filters=(8, 16, 32), + rank=2, + res_blocks_per_level=2, + ) + + x = torch.randn( + 1, + 3, + 64, + 64, + ) + + outputs = model(x) + + assert len(outputs) == 3 + + assert outputs[0].shape == (1, 8, 64, 64) + + assert outputs[-1].shape == (1, 32, 16, 16) + + +# --------------------------------------------------------- +# Decoder +# --------------------------------------------------------- + + +def test_decoder_reconstructs_resolution(): + + encoder = ImageEncoder( + in_channels=3, + filters=(8, 16, 32), + rank=2, + ) + + decoder = ImageDecoder( + filters=(8, 16, 32), + rank=2, + ) + + x = torch.randn( + 1, + 3, + 64, + 64, + ) + + enc_features = encoder(x) + + out = decoder(enc_features) + + assert out.shape == ( + 1, + 8, + 64, + 64, + ) + + +@pytest.mark.parametrize( + "skip", + [ + True, + False, + ], +) +def test_decoder_skip_parameter(skip): + + decoder = ImageDecoder( + filters=(8, 16, 32), + rank=2, + skip=skip, + ) + + assert decoder.skip == skip + + +@pytest.mark.parametrize( + "upsample_type", + [ + "Upsample", + "ConvTranspose", + ], +) +def test_decoder_upsampling_modes(upsample_type): + + encoder = ImageEncoder( + in_channels=3, + filters=(8, 16, 32), + rank=2, + ) + + decoder = ImageDecoder( + filters=(8, 16, 32), + rank=2, + upsample_type=upsample_type, + ) + + x = torch.randn( + 1, + 3, + 64, + 64, + ) + + features = encoder(x) + + out = decoder(features) + + assert out.shape == ( + 1, + 8, + 64, + 64, + ) + + +# --------------------------------------------------------- +# End-to-end gradient test +# --------------------------------------------------------- + + +def test_encoder_decoder_end_to_end_gradient(): + + encoder = ImageEncoder( + in_channels=3, + filters=(8, 16), + rank=2, + ) + + decoder = ImageDecoder( + filters=(8, 16), + rank=2, + ) + + x = torch.randn( + 2, + 3, + 32, + 32, + requires_grad=True, + ) + + output = decoder(encoder(x)) + + loss = output.mean() + + loss.backward() + + assert x.grad is not None + + encoder_grads = [p.grad for p in encoder.parameters() if p.requires_grad] + + decoder_grads = [p.grad for p in decoder.parameters() if p.requires_grad] + + assert all(g is not None for g in encoder_grads) + + assert all(g is not None for g in decoder_grads) + + +# --------------------------------------------------------- +# Decoder crop logic +# --------------------------------------------------------- + + +def test_decoder_match_size_crops_skip_connection(): + + decoder = ImageDecoder( + filters=(8, 16), + rank=2, + skip=True, + ) + + # decoder feature map + x = torch.randn( + 1, + 16, + 30, + 30, + ) + + # encoder skip feature map is larger + skip = torch.randn( + 1, + 8, + 32, + 32, + ) + + cropped = decoder._match_size(x, skip) + + assert cropped.shape == ( + 1, + 8, + 30, + 30, + ) + + +def test_decoder_match_size_center_crop(): + + decoder = ImageDecoder( + filters=(8, 16), + rank=2, + ) + + # Put a known pattern in the skip tensor + skip = ( + torch.arange(32 * 32) + .reshape( + 1, + 1, + 32, + 32, + ) + .float() + ) + + x = torch.zeros( + 1, + 16, + 28, + 28, + ) + + cropped = decoder._match_size( + x, + skip, + ) + + # difference is 4 pixels -> crop 2 from each side + expected = skip[ + ..., + 2:30, + 2:30, + ] + + assert torch.equal( + cropped, + expected, + ) + + +def test_decoder_match_size_no_crop(): + + decoder = ImageDecoder( + filters=(8, 16), + rank=2, + ) + + x = torch.randn( + 1, + 16, + 32, + 32, + ) + + skip = torch.randn( + 1, + 8, + 32, + 32, + ) + + output = decoder._match_size( + x, + skip, + ) + + # should return same tensor + assert output.shape == skip.shape + assert torch.equal(output, skip) + + +# --------------------------------------------------------- +# Full decoder with odd image dimensions +# --------------------------------------------------------- + + +def test_decoder_handles_odd_spatial_dimensions(): + + encoder = ImageEncoder( + in_channels=3, + filters=(8, 16, 32), + rank=2, + ) + + decoder = ImageDecoder( + filters=(8, 16, 32), + rank=2, + skip=True, + ) + + # Odd dimensions trigger possible mismatch + x = torch.randn( + 1, + 3, + 65, + 65, + ) + + features = encoder(x) + + output = decoder(features) + + assert output.shape[2:] == ( + 64, + 64, + ) diff --git a/tools/assets/download_icon_white.svg b/tools/assets/download_icon_white.svg new file mode 100644 index 0000000..46bc259 --- /dev/null +++ b/tools/assets/download_icon_white.svg @@ -0,0 +1,211 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/assets/download_icon_white_32px.png b/tools/assets/download_icon_white_32px.png new file mode 100644 index 0000000..3d1e4fd Binary files /dev/null and b/tools/assets/download_icon_white_32px.png differ diff --git a/tools/assets/im2sim_logo.png b/tools/assets/im2sim_logo.png new file mode 100644 index 0000000..84b764a Binary files /dev/null and b/tools/assets/im2sim_logo.png differ diff --git a/tools/assets/tfmri_icon.svg b/tools/assets/tfmri_icon.svg new file mode 100644 index 0000000..1206fd3 --- /dev/null +++ b/tools/assets/tfmri_icon.svg @@ -0,0 +1,248 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/assets/tfmri_icon_128px.png b/tools/assets/tfmri_icon_128px.png new file mode 100644 index 0000000..49145d3 Binary files /dev/null and b/tools/assets/tfmri_icon_128px.png differ diff --git a/tools/assets/tfmri_icon_192px.png b/tools/assets/tfmri_icon_192px.png new file mode 100644 index 0000000..cf524b0 Binary files /dev/null and b/tools/assets/tfmri_icon_192px.png differ diff --git a/tools/assets/tfmri_icon_24px.png b/tools/assets/tfmri_icon_24px.png new file mode 100644 index 0000000..113b0f1 Binary files /dev/null and b/tools/assets/tfmri_icon_24px.png differ diff --git a/tools/assets/tfmri_icon_256px.png b/tools/assets/tfmri_icon_256px.png new file mode 100644 index 0000000..de3254e Binary files /dev/null and b/tools/assets/tfmri_icon_256px.png differ diff --git a/tools/assets/tfmri_icon_32px.png b/tools/assets/tfmri_icon_32px.png new file mode 100644 index 0000000..74285e0 Binary files /dev/null and b/tools/assets/tfmri_icon_32px.png differ diff --git a/tools/assets/tfmri_icon_384px.png b/tools/assets/tfmri_icon_384px.png new file mode 100644 index 0000000..5176005 Binary files /dev/null and b/tools/assets/tfmri_icon_384px.png differ diff --git a/tools/assets/tfmri_icon_48px.png b/tools/assets/tfmri_icon_48px.png new file mode 100644 index 0000000..86e9ca6 Binary files /dev/null and b/tools/assets/tfmri_icon_48px.png differ diff --git a/tools/assets/tfmri_icon_96px.png b/tools/assets/tfmri_icon_96px.png new file mode 100644 index 0000000..3bea671 Binary files /dev/null and b/tools/assets/tfmri_icon_96px.png differ diff --git a/tools/assets/tfmri_logo.svg b/tools/assets/tfmri_logo.svg new file mode 100644 index 0000000..8eba502 --- /dev/null +++ b/tools/assets/tfmri_logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tools/assets/tfmri_logo_dark.svg b/tools/assets/tfmri_logo_dark.svg new file mode 100644 index 0000000..39b5e7d --- /dev/null +++ b/tools/assets/tfmri_logo_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tools/assets/thumb.png b/tools/assets/thumb.png new file mode 100644 index 0000000..e19d466 Binary files /dev/null and b/tools/assets/thumb.png differ diff --git a/tools/build/build_pip_pkg.sh b/tools/build/build_pip_pkg.sh new file mode 100755 index 0000000..98e502f --- /dev/null +++ b/tools/build/build_pip_pkg.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# Copyright 2021 University College London. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +set -e +set -x + +PLATFORM="$(uname -s | tr 'A-Z' 'a-z')" +function is_windows() { + if [[ "${PLATFORM}" =~ (cygwin|mingw32|mingw64|msys)_nt* ]]; then + true + else + false + fi +} + +if is_windows; then + PIP_FILE_PREFIX="bazel-bin/build_pip_pkg.exe.runfiles/__main__/" +else + PIP_FILE_PREFIX="bazel-bin/build_pip_pkg.runfiles/__main__/" +fi + +PYTHON=python3 +function main() { + while [[ ! -z "${1}" ]]; do + if [[ ${1} == "make" ]]; then + echo "Using Makefile to build pip package." + PIP_FILE_PREFIX="" + elif [[ ${1} == "--python" ]]; then + PYTHON=${2} + shift + else + DEST=${1} + fi + shift + done + + if [[ -z ${DEST} ]]; then + echo "No destination dir provided" + exit 1 + fi + + # Create the directory, then do dirname on a non-existent file inside it to + # give us an absolute paths with tilde characters resolved to the destination + # directory. + mkdir -p ${DEST} + if [[ ${PLATFORM} == "darwin" ]]; then + DEST=$(pwd -P)/${DEST} + else + DEST=$(readlink -f "${DEST}") + fi + echo "=== destination directory: ${DEST}" + + TMPDIR=$(mktemp -d -t tmp.XXXXXXXXXX) + + echo $(date) : "=== Using tmpdir: ${TMPDIR}" + + echo "=== Copy TensorFlow Custom op files" + + cp ${PIP_FILE_PREFIX}setup.py "${TMPDIR}" + cp ${PIP_FILE_PREFIX}MANIFEST.in "${TMPDIR}" + cp ${PIP_FILE_PREFIX}LICENSE "${TMPDIR}" + cp ${PIP_FILE_PREFIX}README.rst "${TMPDIR}" + cp ${PIP_FILE_PREFIX}requirements.txt "${TMPDIR}" + rsync -avm -L --exclude='*.h' --exclude='*.cc' --exclude='*.o' \ + --exclude='*_test.py' --exclude='__pycache__/*' \ + ${PIP_FILE_PREFIX}tensorflow_mri "${TMPDIR}" + + pushd ${TMPDIR} + echo $(date) : "=== Building wheel" + ${PYTHON} setup.py bdist_wheel > /dev/null + + if [[ "${PLATFORM}" == "linux" ]]; then + echo $(date) : "=== Auditing wheel" + auditwheel repair --plat manylinux2014_x86_64 dist/*linux_x86_64.whl -w dist/ + rm -rf dist/*linux_x86_64.whl + fi + + cp dist/*.whl "${DEST}" + popd + rm -rf ${TMPDIR} + echo $(date) : "=== Output wheel file is in: ${DEST}" +} + +main "$@" diff --git a/tools/build/create_api.py b/tools/build/create_api.py new file mode 100644 index 0000000..11489f3 --- /dev/null +++ b/tools/build/create_api.py @@ -0,0 +1,114 @@ +# Copyright 2022 University College London. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Creates the public API of TensorFlow MRI.""" + +import inspect +import pathlib +import string +import sys + + + +SCRIPT_PATH = pathlib.Path(__file__).resolve() +BUILD_PATH = SCRIPT_PATH.parent +ROOT_PATH = BUILD_PATH.parent.parent +API_PATH = ROOT_PATH / 'im2sim/_api' +INIT_PATH = ROOT_PATH / 'im2sim/__init__.py' + +sys.path.insert(0, str(ROOT_PATH)) +import im2sim.src +from im2sim.src.utils import api_util as api_util + +INIT_TEMPLATE = string.Template( +'''# This file was automatically generated by ${script_path}. +# Do not edit. +"""IM2SIM.""" +import os as _os +import sys as _sys + +from im2sim.__about__ import * + +# Import submodules. +${submodule_imports} + +# Make sure directory containing top level submodules is in +# the __path__ so that "from tensorflow_mri.foo import bar" works. +# We're using callbacks, but there's nothing special about that. +_API_MODULE = _sys.modules[__name__].layers +_im2sim_api_dir = _os.path.dirname(_os.path.dirname(_API_MODULE.__file__)) +_current_module = _sys.modules[__name__] + +if not hasattr(_current_module, '__path__'): + __path__ = [_im2sim_api_dir] +elif _im2sim_api_dir not in __path__: + __path__.append(_im2sim_api_dir) +''') + + +SUBMODULE_TEMPLATE = string.Template( +'''# This file was automatically generated by ${script_path}. +# Do not edit. +"""${docstring}""" + +${symbol_imports} +''') + +SUBMODULE_IMPORT_TEMPLATE = "from im2sim._api import {submodule_name}" +SYMBOL_IMPORT_TEMPLATE = "from {module_name} import {symbol_name} as {symbol_alias}" + +# Update the top-level __init__.py file. +submodule_imports = [ + SUBMODULE_IMPORT_TEMPLATE.format(submodule_name=submodule_name) + for submodule_name in api_util.get_submodule_names()] +submodule_imports = '\n'.join(submodule_imports) + +init_contents = INIT_TEMPLATE.substitute( + script_path=SCRIPT_PATH.relative_to(ROOT_PATH), + submodule_imports=submodule_imports) + +with open(INIT_PATH, 'w') as f: + f.write(init_contents) + +# import im2sim + +# Now generate the individual submodule APIs. +for submodule_name in api_util.get_submodule_names(): + print("SUBMODULE:", submodule_name) + docstring = api_util.get_docstring_for_submodule(submodule_name) + symbols = api_util.get_symbols_in_submodule(submodule_name) + + symbol_imports = [] + for api_name, symbol in symbols.items(): + print(" SYMBOL:", api_name, symbol) + symbol_alias = api_name.split('.')[-1] + module_name = inspect.getmodule(symbol).__name__ + symbol_name = symbol.__name__ + symbol_imports.append( + SYMBOL_IMPORT_TEMPLATE.format(module_name=module_name, + symbol_name=symbol_name, + symbol_alias=symbol_alias)) + symbol_imports = '\n'.join(symbol_imports) + + submodule_path = API_PATH / submodule_name + submodule_path.mkdir(parents=True, exist_ok=True) + submodule_path = submodule_path / '__init__.py' + + out = SUBMODULE_TEMPLATE.substitute( + script_path=SCRIPT_PATH.relative_to(ROOT_PATH), + docstring=docstring, + symbol_imports=symbol_imports) + + with open(submodule_path, 'w') as f: + f.write(out) diff --git a/tools/docs/Makefile b/tools/docs/Makefile new file mode 100644 index 0000000..a8adbe1 --- /dev/null +++ b/tools/docs/Makefile @@ -0,0 +1,21 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +PY_VERSION ?= 3.10 +SPHINXOPTS ?= +SPHINXBUILD ?= python$(PY_VERSION) -m sphinx +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/tools/docs/_build/dirhtml/.buildinfo b/tools/docs/_build/dirhtml/.buildinfo new file mode 100644 index 0000000..905ee97 --- /dev/null +++ b/tools/docs/_build/dirhtml/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file records the configuration used when building these files. When it is not found, a full rebuild will be done. +config: 46f2b4df58a5cb8a9843d408f1a0a320 +tags: d77d1c0d9ca2f4c8421862c7c5a0d620 diff --git a/tools/docs/_build/dirhtml/_images/ed739320bcec44e952e2c94e8c30931e2f1565301f14f6e2722599cc273859a5.png b/tools/docs/_build/dirhtml/_images/ed739320bcec44e952e2c94e8c30931e2f1565301f14f6e2722599cc273859a5.png new file mode 100644 index 0000000..b74b45d Binary files /dev/null and b/tools/docs/_build/dirhtml/_images/ed739320bcec44e952e2c94e8c30931e2f1565301f14f6e2722599cc273859a5.png differ diff --git a/tools/docs/_build/dirhtml/_sources/api_docs.rst.txt b/tools/docs/_build/dirhtml/_sources/api_docs.rst.txt new file mode 100644 index 0000000..c4fd190 --- /dev/null +++ b/tools/docs/_build/dirhtml/_sources/api_docs.rst.txt @@ -0,0 +1,2 @@ +IM2SIM API documentation +================================ diff --git a/tools/docs/_build/dirhtml/_sources/api_docs/im2sim.rst.txt b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim.rst.txt new file mode 100644 index 0000000..d2e1761 --- /dev/null +++ b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim.rst.txt @@ -0,0 +1,37 @@ +im2sim +===== + +.. automodule:: im2sim + +Modules +------- + +.. autosummary:: + :nosignatures: + + configs + data + layers + losses + models + ops + plot + + +Classes +------- + +.. autosummary:: + :toctree: im2sim + :template: ops/class.rst + :nosignatures: + + + +Functions +--------- + +.. autosummary:: + :toctree: im2sim + :template: ops/function.rst + :nosignatures: diff --git a/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/configs.rst.txt b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/configs.rst.txt new file mode 100644 index 0000000..7bde386 --- /dev/null +++ b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/configs.rst.txt @@ -0,0 +1,25 @@ +im2sim.configs +============= + +.. automodule:: im2sim.configs + +Classes +------- + +.. autosummary:: + :toctree: configs + :template: configs/class.rst + :nosignatures: + + HalfUNetConfig + ImageConvBlockConfig + +Functions +--------- + +.. autosummary:: + :toctree: configs + :template: configs/function.rst + :nosignatures: + + diff --git a/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/configs/HalfUNetConfig.rst.txt b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/configs/HalfUNetConfig.rst.txt new file mode 100644 index 0000000..93a1af0 --- /dev/null +++ b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/configs/HalfUNetConfig.rst.txt @@ -0,0 +1,8 @@ +im2sim.configs.HalfUNetConfig +============================= + +.. currentmodule:: im2sim.configs + +.. autoclass:: HalfUNetConfig + :members: + :show-inheritance: \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/configs/ImageConvBlockConfig.rst.txt b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/configs/ImageConvBlockConfig.rst.txt new file mode 100644 index 0000000..52efb4c --- /dev/null +++ b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/configs/ImageConvBlockConfig.rst.txt @@ -0,0 +1,8 @@ +im2sim.configs.ImageConvBlockConfig +=================================== + +.. currentmodule:: im2sim.configs + +.. autoclass:: ImageConvBlockConfig + :members: + :show-inheritance: \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/data.rst.txt b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/data.rst.txt new file mode 100644 index 0000000..2500ac8 --- /dev/null +++ b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/data.rst.txt @@ -0,0 +1,24 @@ +im2sim.data +========== + +.. automodule:: im2sim.data + +Classes +------- + +.. autosummary:: + :toctree: data + :template: data/class.rst + :nosignatures: + + + +Functions +--------- + +.. autosummary:: + :toctree: data + :template: data/function.rst + :nosignatures: + + diff --git a/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/layers.rst.txt b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/layers.rst.txt new file mode 100644 index 0000000..1ae1873 --- /dev/null +++ b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/layers.rst.txt @@ -0,0 +1,34 @@ +im2sim.layers +============ + +.. automodule:: im2sim.layers + +Classes +------- + +.. autosummary:: + :toctree: layers + :template: layers/class.rst + :nosignatures: + + ConditionedSqueezeExcite + DefaultGraphNorm + DepthwiseConv + DepthwiseSeparableConv + EfficientChannelAttn + GhostConv + GraphConvBlock + GraphConvResBlock + GraphResDecoderBlock + ImageConvBlock + SqueezeExcite + +Functions +--------- + +.. autosummary:: + :toctree: layers + :template: layers/function.rst + :nosignatures: + + diff --git a/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/layers/ConditionedSqueezeExcite.rst.txt b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/layers/ConditionedSqueezeExcite.rst.txt new file mode 100644 index 0000000..2f8cd36 --- /dev/null +++ b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/layers/ConditionedSqueezeExcite.rst.txt @@ -0,0 +1,8 @@ +im2sim.layers.ConditionedSqueezeExcite +====================================== + +.. currentmodule:: im2sim.layers + +.. autoclass:: ConditionedSqueezeExcite + :members: + :show-inheritance: \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/layers/DefaultGraphNorm.rst.txt b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/layers/DefaultGraphNorm.rst.txt new file mode 100644 index 0000000..6827f3d --- /dev/null +++ b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/layers/DefaultGraphNorm.rst.txt @@ -0,0 +1,8 @@ +im2sim.layers.DefaultGraphNorm +============================== + +.. currentmodule:: im2sim.layers + +.. autoclass:: DefaultGraphNorm + :members: + :show-inheritance: \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/layers/DepthwiseConv.rst.txt b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/layers/DepthwiseConv.rst.txt new file mode 100644 index 0000000..c22f48f --- /dev/null +++ b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/layers/DepthwiseConv.rst.txt @@ -0,0 +1,8 @@ +im2sim.layers.DepthwiseConv +=========================== + +.. currentmodule:: im2sim.layers + +.. autoclass:: DepthwiseConv + :members: + :show-inheritance: \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/layers/DepthwiseSeparableConv.rst.txt b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/layers/DepthwiseSeparableConv.rst.txt new file mode 100644 index 0000000..c00bb8d --- /dev/null +++ b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/layers/DepthwiseSeparableConv.rst.txt @@ -0,0 +1,8 @@ +im2sim.layers.DepthwiseSeparableConv +==================================== + +.. currentmodule:: im2sim.layers + +.. autoclass:: DepthwiseSeparableConv + :members: + :show-inheritance: \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/layers/EfficientChannelAttn.rst.txt b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/layers/EfficientChannelAttn.rst.txt new file mode 100644 index 0000000..19563af --- /dev/null +++ b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/layers/EfficientChannelAttn.rst.txt @@ -0,0 +1,8 @@ +im2sim.layers.EfficientChannelAttn +================================== + +.. currentmodule:: im2sim.layers + +.. autoclass:: EfficientChannelAttn + :members: + :show-inheritance: \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/layers/GhostConv.rst.txt b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/layers/GhostConv.rst.txt new file mode 100644 index 0000000..8ead535 --- /dev/null +++ b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/layers/GhostConv.rst.txt @@ -0,0 +1,8 @@ +im2sim.layers.GhostConv +======================= + +.. currentmodule:: im2sim.layers + +.. autoclass:: GhostConv + :members: + :show-inheritance: \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/layers/GraphConvBlock.rst.txt b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/layers/GraphConvBlock.rst.txt new file mode 100644 index 0000000..81846f5 --- /dev/null +++ b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/layers/GraphConvBlock.rst.txt @@ -0,0 +1,8 @@ +im2sim.layers.GraphConvBlock +============================ + +.. currentmodule:: im2sim.layers + +.. autoclass:: GraphConvBlock + :members: + :show-inheritance: \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/layers/GraphConvResBlock.rst.txt b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/layers/GraphConvResBlock.rst.txt new file mode 100644 index 0000000..5d0e180 --- /dev/null +++ b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/layers/GraphConvResBlock.rst.txt @@ -0,0 +1,8 @@ +im2sim.layers.GraphConvResBlock +=============================== + +.. currentmodule:: im2sim.layers + +.. autoclass:: GraphConvResBlock + :members: + :show-inheritance: \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/layers/GraphResDecoderBlock.rst.txt b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/layers/GraphResDecoderBlock.rst.txt new file mode 100644 index 0000000..ffaadf0 --- /dev/null +++ b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/layers/GraphResDecoderBlock.rst.txt @@ -0,0 +1,8 @@ +im2sim.layers.GraphResDecoderBlock +================================== + +.. currentmodule:: im2sim.layers + +.. autoclass:: GraphResDecoderBlock + :members: + :show-inheritance: \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/layers/ImageConvBlock.rst.txt b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/layers/ImageConvBlock.rst.txt new file mode 100644 index 0000000..4bebbe3 --- /dev/null +++ b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/layers/ImageConvBlock.rst.txt @@ -0,0 +1,8 @@ +im2sim.layers.ImageConvBlock +============================ + +.. currentmodule:: im2sim.layers + +.. autoclass:: ImageConvBlock + :members: + :show-inheritance: \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/layers/SqueezeExcite.rst.txt b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/layers/SqueezeExcite.rst.txt new file mode 100644 index 0000000..a67e99b --- /dev/null +++ b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/layers/SqueezeExcite.rst.txt @@ -0,0 +1,8 @@ +im2sim.layers.SqueezeExcite +=========================== + +.. currentmodule:: im2sim.layers + +.. autoclass:: SqueezeExcite + :members: + :show-inheritance: \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/losses.rst.txt b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/losses.rst.txt new file mode 100644 index 0000000..a0ad274 --- /dev/null +++ b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/losses.rst.txt @@ -0,0 +1,24 @@ +im2sim.losses +============ + +.. automodule:: im2sim.losses + +Classes +------- + +.. autosummary:: + :toctree: losses + :template: losses/class.rst + :nosignatures: + + + +Functions +--------- + +.. autosummary:: + :toctree: losses + :template: losses/function.rst + :nosignatures: + + diff --git a/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/models.rst.txt b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/models.rst.txt new file mode 100644 index 0000000..f7e36b9 --- /dev/null +++ b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/models.rst.txt @@ -0,0 +1,24 @@ +im2sim.models +============ + +.. automodule:: im2sim.models + +Classes +------- + +.. autosummary:: + :toctree: models + :template: models/class.rst + :nosignatures: + + HalfUNet + +Functions +--------- + +.. autosummary:: + :toctree: models + :template: models/function.rst + :nosignatures: + + diff --git a/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/models/HalfUNet.rst.txt b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/models/HalfUNet.rst.txt new file mode 100644 index 0000000..ef0a032 --- /dev/null +++ b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/models/HalfUNet.rst.txt @@ -0,0 +1,8 @@ +im2sim.models.HalfUNet +====================== + +.. currentmodule:: im2sim.models + +.. autoclass:: HalfUNet + :members: + :show-inheritance: \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/ops.rst.txt b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/ops.rst.txt new file mode 100644 index 0000000..fa21c8d --- /dev/null +++ b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/ops.rst.txt @@ -0,0 +1,24 @@ +im2sim.ops +========= + +.. automodule:: im2sim.ops + +Classes +------- + +.. autosummary:: + :toctree: ops + :template: ops/class.rst + :nosignatures: + + + +Functions +--------- + +.. autosummary:: + :toctree: ops + :template: ops/function.rst + :nosignatures: + + normtorange diff --git a/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/ops/normtorange.rst.txt b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/ops/normtorange.rst.txt new file mode 100644 index 0000000..62e9378 --- /dev/null +++ b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/ops/normtorange.rst.txt @@ -0,0 +1,6 @@ +im2sim.ops.normtorange +====================== + +.. currentmodule:: im2sim.ops + +.. autofunction:: normtorange \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/plot.rst.txt b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/plot.rst.txt new file mode 100644 index 0000000..ebd4801 --- /dev/null +++ b/tools/docs/_build/dirhtml/_sources/api_docs/im2sim/plot.rst.txt @@ -0,0 +1,24 @@ +im2sim.plot +========== + +.. automodule:: im2sim.plot + +Classes +------- + +.. autosummary:: + :toctree: plot + :template: plot/class.rst + :nosignatures: + + + +Functions +--------- + +.. autosummary:: + :toctree: plot + :template: plot/function.rst + :nosignatures: + + diff --git a/tools/docs/_build/dirhtml/_sources/guide.rst.txt b/tools/docs/_build/dirhtml/_sources/guide.rst.txt new file mode 100644 index 0000000..ac46b84 --- /dev/null +++ b/tools/docs/_build/dirhtml/_sources/guide.rst.txt @@ -0,0 +1,2 @@ +IM2SIM guide +==================== diff --git a/tools/docs/_build/dirhtml/_sources/guide/contribute.ipynb.txt b/tools/docs/_build/dirhtml/_sources/guide/contribute.ipynb.txt new file mode 100644 index 0000000..98b4165 --- /dev/null +++ b/tools/docs/_build/dirhtml/_sources/guide/contribute.ipynb.txt @@ -0,0 +1,32 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Contributing\n", + "\n", + "Coming soon..." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.8.10 64-bit", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.8.10" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "916dbcbb3f70747c44a77c7bcd40155683ae19c65e1c03b4aa3499c5328201f1" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tools/docs/_build/dirhtml/_sources/guide/faq.rst.txt b/tools/docs/_build/dirhtml/_sources/guide/faq.rst.txt new file mode 100644 index 0000000..a699674 --- /dev/null +++ b/tools/docs/_build/dirhtml/_sources/guide/faq.rst.txt @@ -0,0 +1,15 @@ +Frequently Asked Questions +========================== + +**When trying to install TensorFlow MRI, I get an error about OpenEXR which +includes: +``OpenEXR.cpp:36:10: fatal error: ImathBox.h: No such file or directory``. What +do I do?** + +OpenEXR is needed by TensorFlow Graphics, which is a dependency of TensorFlow +MRI. This issue can be fixed by installing the OpenEXR library. On +Debian/Ubuntu: + +.. code-block:: console + + $ apt install libopenexr-dev diff --git a/tools/docs/_build/dirhtml/_sources/guide/install.rst.txt b/tools/docs/_build/dirhtml/_sources/guide/install.rst.txt new file mode 100644 index 0000000..404c4a7 --- /dev/null +++ b/tools/docs/_build/dirhtml/_sources/guide/install.rst.txt @@ -0,0 +1,89 @@ +Install TensorFlow MRI +====================== + +Requirements +------------ + +TensorFlow MRI should work in most Linux systems that meet the +`requirements for TensorFlow `_. + +.. warning:: + + TensorFlow MRI is not yet available for Windows or macOS. + `Help us support them! `_. + + +TensorFlow compatibility +~~~~~~~~~~~~~~~~~~~~~~~~ + +Each TensorFlow MRI release is compiled against a specific version of +TensorFlow. To ensure compatibility, it is recommended to install matching +versions of TensorFlow and TensorFlow MRI according to the +:ref:`TensorFlow compatibility table`. + +.. warning:: + + Each TensorFlow MRI version aims to target and support the latest TensorFlow + version only. A new version of TensorFlow MRI will be released shortly after + each TensorFlow release. TensorFlow MRI versions that target older versions + of TensorFlow will not generally receive any updates. + + +Set up your system +------------------ + +You will need a working TensorFlow installation. Follow the `TensorFlow +installation instructions `_ if you do not +have one already. + + +Use a GPU +~~~~~~~~~ + +If you need GPU support, we suggest that you use one of the +`TensorFlow Docker images `_. +These come with a GPU-enabled TensorFlow installation and are the easiest way +to run TensorFlow and TensorFlow MRI on your system. + +.. code-block:: console + + $ docker pull tensorflow/tensorflow:latest-gpu + +Alternatively, make sure you follow +`these instructions `_ when setting up +your system. + + +Download from PyPI +------------------ + +TensorFlow MRI is available on the Python package index (PyPI) and can be +installed using the ``pip`` package manager: + +.. code-block:: console + + $ pip install tensorflow-mri + + +Run in Google Colab +------------------- + +To get started without installing anything on your system, you can use +`Google Colab `_. +Simply create a new notebook and use ``pip`` to install TensorFlow MRI. + +.. code:: python + + !pip install tensorflow-mri + + +The Colab environment is already configured to run TensorFlow and has GPU +support. + + +TensorFlow compatibility table +------------------------------ + +.. include:: ../../../README.rst + :start-after: start-compatibility-table + :end-before: end-compatibility-table diff --git a/tools/docs/_build/dirhtml/_sources/guide/linalg.ipynb.txt b/tools/docs/_build/dirhtml/_sources/guide/linalg.ipynb.txt new file mode 100644 index 0000000..f45442d --- /dev/null +++ b/tools/docs/_build/dirhtml/_sources/guide/linalg.ipynb.txt @@ -0,0 +1,32 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Linear algebra\n", + "\n", + "Coming soon..." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.8.10 64-bit", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.8.10" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "916dbcbb3f70747c44a77c7bcd40155683ae19c65e1c03b4aa3499c5328201f1" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tools/docs/_build/dirhtml/_sources/guide/nufft.ipynb.txt b/tools/docs/_build/dirhtml/_sources/guide/nufft.ipynb.txt new file mode 100644 index 0000000..8a07f65 --- /dev/null +++ b/tools/docs/_build/dirhtml/_sources/guide/nufft.ipynb.txt @@ -0,0 +1,435 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Non-uniform fast Fourier transform (NUFFT)\n", + "TensorFlow MRI provides an efficient NUFFT operator for both CPU and GPU, based\n", + "on the algorithms by the Flatiron Institute (see\n", + "[this paper](https://doi.org/10.1137/18M120885X) and\n", + "[this paper](https://doi.ieeecomputersociety.org/10.1109/IPDPSW52791.2021.00105)\n", + "for more details). The operator is available as\n", + "[`tfmri.signal.nufft`](https://mrphys.github.io/tensorflow-mri/api_docs/tfmri/signal/nufft/).\n", + "\n", + ":::{note}\n", + "The `tfmri.signal.nufft` function is an alias of the `nufft` function in the\n", + "[TensorFlow NUFFT](https://mrphys.github.io/tensorflow-nufft/)\n", + "stand-alone package. Please direct any issues about the NUFFT function directly\n", + "to the TensorFlow NUFFT [repository](https://github.com/mrphys/tensorflow-nufft/).\n", + ":::\n", + "\n", + ":::{warning}\n", + "The current NUFFT implementation uses the [FFTW](https://www.fftw.org/) library,\n", + "which is released under the GNU GPL. If you are using the NUFFT for commercial\n", + "purposes, you will need to purchase a license from MIT or adapt the code to use\n", + "a different FFT library. If you do the latter, please consider\n", + "[contributing](https://mrphys.github.io/tensorflow-mri/guide/contribute/)\n", + "your modification so others may benefit.\n", + ":::\n", + "\n", + "The NUFFT function can be used to efficiently evaluate the Fourier transform\n", + "when either the input data or the output data does not lie on a uniform grid,\n", + "in which case the standard fast Fourier transform (FFT) algorithm cannot be\n", + "used. There are 3 transform types depending whether the input is non-uniform,\n", + "the output is non-uniform or both input and output are non-uniform.\n", + "\n", + "- A **type-1** transform evaluates the Fourier transform on a uniform grid\n", + " given a set of arbitrary points (i.e, non-uniform to uniform).\n", + "- A **type-2** transform evaluates the Fourier transform on a set of arbitrary\n", + " points given a uniform grid. (i.e., uniform to non-uniform).\n", + "- A **type-3** transform evaluates the Fourier transform on a set of arbitrary\n", + " points given a set of arbitrary points (i.e., non-uniform to non-uniform).\n", + "\n", + ":::{tip}\n", + "The type of the transform can be specified using the `transform_type` argument.\n", + ":::\n", + "\n", + ":::{warning}\n", + "NUFFT type-3 is not currently supported or planned, but contributions will be\n", + "accepted.\n", + ":::\n", + "\n", + "The NUFFT may be **forward** (signal to frequency domain) or **backward**\n", + "(frequency to signal domain), regardless of the transform type.\n", + "\n", + ":::{tip}\n", + "The direction of the transform can be specified using the `fft_direction`\n", + "argument.\n", + ":::" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Guided example\n", + "As an example, let's take an image of the Shepp-Logan phantom and evaluate its\n", + "Fourier transform on a set of sampling points defining a radial *k*-space\n", + "trajectory, using a forward, type-2 NUFFT. Then we will see how to recover\n", + "the image from the radial *k*-space data, using a backward, type-1 NUFFT." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[33mWARNING: You are using pip version 22.0.4; however, version 22.2 is available.\n", + "You should consider upgrading via the '/usr/local/bin/python3.8 -m pip install --upgrade pip' command.\u001b[0m\u001b[33m\n", + "\u001b[0mNote: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "%pip install -q tensorflow tensorflow-mri" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now import both packages and create an example image using\n", + "[`tfmri.image.phantom`](https://mrphys.github.io/tensorflow-mri/api_docs/tfmri/image/phantom/):" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2022-07-21 17:25:43.649824: I tensorflow/core/util/util.cc:169] oneDNN custom operations are on. You may see slightly different numerical results due to floating-point round-off errors from different computation orders. To turn them off, set the environment variable `TF_ENABLE_ONEDNN_OPTS=0`.\n", + "2022-07-21 17:25:59.264266: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:975] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero\n", + "2022-07-21 17:25:59.268928: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:975] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero\n", + "2022-07-21 17:25:59.269048: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:975] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero\n", + "2022-07-21 17:25:59.269524: I tensorflow/core/platform/cpu_feature_guard.cc:193] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operations: AVX2 AVX512F AVX512_VNNI FMA\n", + "To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags.\n", + "2022-07-21 17:25:59.270142: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:975] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero\n", + "2022-07-21 17:25:59.270251: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:975] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero\n", + "2022-07-21 17:25:59.270327: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:975] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero\n", + "2022-07-21 17:25:59.612269: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:975] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero\n", + "2022-07-21 17:25:59.612401: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:975] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero\n", + "2022-07-21 17:25:59.612481: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:975] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero\n", + "2022-07-21 17:25:59.612559: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1532] Created device /job:localhost/replica:0/task:0/device:GPU:0 with 14239 MB memory: -> device: 0, name: NVIDIA GeForce RTX 3080 Laptop GPU, pci bus id: 0000:01:00.0, compute capability: 8.6\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "image: \n", + " - shape: (256, 256)\n", + " - dtype: \n" + ] + } + ], + "source": [ + "import tensorflow as tf\n", + "import tensorflow_mri as tfmri\n", + "\n", + "# Create\n", + "image_shape = [256, 256]\n", + "image = tfmri.image.phantom(shape=image_shape, dtype=tf.complex64)\n", + "\n", + "print(\"image: \\n - shape: {}\\n - dtype: {}\".format(image.shape, image.dtype))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let us also create a *k*-space trajectory. In this example we will create a\n", + "radial trajectory." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "trajectory: \n", + " - shape: (119296, 2)\n", + " - dtype: \n", + " - range: [-3.1415927410125732, 3.141521453857422]\n" + ] + } + ], + "source": [ + "trajectory = tfmri.sampling.radial_trajectory(\n", + " base_resolution=256, views=233, flatten_encoding_dims=True)\n", + "\n", + "print(\"trajectory: \\n - shape: {}\\n - dtype: {}\\n - range: [{}, {}]\".format(\n", + " trajectory.shape, trajectory.dtype,\n", + " tf.math.reduce_min(trajectory), tf.math.reduce_max(trajectory)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The trajectory should have shape `[..., M, N]`, where `M` is the number of\n", + "points and `N` is the number of dimensions. Any additional dimensions `...` will\n", + "be treated as batch dimensions.\n", + "\n", + "Batch dimensions for `image` and `traj`, if any, will be broadcasted.\n", + "\n", + "Spatial frequencies should be provided in radians/voxel, ie, in the range\n", + "`[-pi, pi]`." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, we'll also need density compensation weights for our set of nonuniform\n", + "points. These are necessary in the adjoint transform, to compensate for the fact\n", + "that the sampling density in a radial trajectory is not uniform." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "density: \n", + " - shape: (119296,)\n", + " - dtype: \n" + ] + } + ], + "source": [ + "density = tfmri.sampling.radial_density(base_resolution=256, views=233)\n", + "density = tf.reshape(density, [-1])\n", + "\n", + "print(\"density: \\n - shape: {}\\n - dtype: {}\".format(\n", + " density.shape, density.dtype))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Forward transform (image to *k*-space)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, let's calculate the k-space coefficients for the given image and trajectory points (image to k-space transform)." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "kspace: \n", + " - shape: (119296,)\n", + " - dtype: \n" + ] + } + ], + "source": [ + "kspace = tfmri.signal.nufft(image, trajectory,\n", + " transform_type='type_2',\n", + " fft_direction='forward')\n", + "\n", + "print(\"kspace: \\n - shape: {}\\n - dtype: {}\".format(kspace.shape, kspace.dtype))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We are using a type-2 transform (uniform to nonuniform) and a forward FFT\n", + "(image domain to frequency domain). These are the default values for\n", + "`transform_type` and `fft_direction`, so providing them was not necessary in\n", + "this case." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Adjoint transform (*k*-space to image)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will now perform the adjoint transform to recover the image given the\n", + "*k*-space data. In this case, we will use a type-1 transform (nonuniform to\n", + "uniform) and a backward FFT (frequency domain to image domain). Also note that,\n", + "prior to evaluating the NUFFT, we will compensate for the nonuniform sampling\n", + "density by simply dividing the *k*-space samples by the density weights.\n", + "Finally, for type-1 transforms we need to specify an additional `grid_shape`\n", + "argument, which should be the size of the image. If there are any batch\n", + "dimensions, `grid_shape` should **not** include them." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "recon: \n", + " - shape: (256, 256)\n", + " - dtype: \n" + ] + } + ], + "source": [ + "# Apply density compensation.\n", + "kspace /= tf.cast(density, tf.complex64)\n", + "\n", + "recon = tfmri.signal.nufft(kspace, trajectory,\n", + " grid_shape=image_shape,\n", + " transform_type='type_1',\n", + " fft_direction='backward')\n", + "\n", + "print(\"recon: \\n - shape: {}\\n - dtype: {}\".format(recon.shape, recon.dtype))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally, let's visualize the images." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "def plot_images(image, recon):\n", + " _, ax = plt.subplots(1, 2, figsize=(9.6, 5.4))\n", + " ax[0].imshow(tf.abs(image), cmap='gray')\n", + " ax[0].set_title(\"Original image\")\n", + " ax[1].imshow(tf.abs(recon), cmap='gray')\n", + " ax[1].set_title(\"Image after forward\\nand adjoint NUFFT\")\n", + " plt.show()\n", + "plot_images(image, recon)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Use the linear operator\n", + "You can also use\n", + "[`tfmri.linalg.LinearOperatorNUFFT`](https://mrphys.github.io/tensorflow-mri/api_docs/tfmri/linalg/LinearOperatorNUFFT/)\n", + "to perform forward and adjoint NUFFT. This might be particularly useful when\n", + "building MRI reconstruction methods, as you can take advantage of the features\n", + "of the [linear algebra framework](https://mrphys.github.io/tensorflow-mri/guide/linalg/)." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Create the linear operator for the specified image shape, trajectory and\n", + "# density.\n", + "linop_nufft = tfmri.linalg.LinearOperatorNUFFT(\n", + " image_shape, trajectory=trajectory, density=density)\n", + "\n", + "# Apply forward transform to obtain the *k*-space signal given an image.\n", + "kspace = linop_nufft.transform(image)\n", + "\n", + "# Apply adjoint transform to obtain an image given a *k*-space signal.\n", + "recon = linop_nufft.transform(kspace, adjoint=True)\n", + "\n", + "plot_images(image, recon)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.8.2 64-bit", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.2" + }, + "vscode": { + "interpreter": { + "hash": "0adcc2737ebf6a4a119f135174df96668767fca1ef1112612db5ecadf2b6d608" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tools/docs/_build/dirhtml/_sources/guide/optim.ipynb.txt b/tools/docs/_build/dirhtml/_sources/guide/optim.ipynb.txt new file mode 100644 index 0000000..2136372 --- /dev/null +++ b/tools/docs/_build/dirhtml/_sources/guide/optim.ipynb.txt @@ -0,0 +1,32 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Optimization\n", + "\n", + "Coming soon..." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.8.2 64-bit", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.8.2" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "0adcc2737ebf6a4a119f135174df96668767fca1ef1112612db5ecadf2b6d608" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tools/docs/_build/dirhtml/_sources/guide/recon.ipynb.txt b/tools/docs/_build/dirhtml/_sources/guide/recon.ipynb.txt new file mode 100644 index 0000000..5291a70 --- /dev/null +++ b/tools/docs/_build/dirhtml/_sources/guide/recon.ipynb.txt @@ -0,0 +1,32 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# MR image reconstruction\n", + "\n", + "Coming soon..." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.8.2 64-bit", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.8.2" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "0adcc2737ebf6a4a119f135174df96668767fca1ef1112612db5ecadf2b6d608" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tools/docs/_build/dirhtml/_sources/index.rst.txt b/tools/docs/_build/dirhtml/_sources/index.rst.txt new file mode 100644 index 0000000..a8aa51c --- /dev/null +++ b/tools/docs/_build/dirhtml/_sources/index.rst.txt @@ -0,0 +1,29 @@ +IM2SIM |release| +======================== + +.. image:: https://img.shields.io/badge/-View%20on%20GitHub-128091?logo=github&labelColor=grey + :target: https://github.com/mrphys/im2sim + :alt: View on GitHub + +.. include:: ../../README.rst + :start-after: start-intro + :end-before: end-intro + + +.. toctree:: + :caption: API Documentation + :hidden: + + API documentation + api_docs/im2sim + api_docs/im2sim/configs + api_docs/im2sim/data + api_docs/im2sim/layers + api_docs/im2sim/losses + api_docs/im2sim/models + api_docs/im2sim/ops + api_docs/im2sim/plot + + +.. meta:: + :google-site-verification: 8PySedj6KJ0kc5qC1CbO6_9blFB9Nho3SgXvbRzyVOU diff --git a/tools/docs/_build/dirhtml/_sources/templates/index.rst.txt b/tools/docs/_build/dirhtml/_sources/templates/index.rst.txt new file mode 100644 index 0000000..aef3f8f --- /dev/null +++ b/tools/docs/_build/dirhtml/_sources/templates/index.rst.txt @@ -0,0 +1,23 @@ +IM2SIM |release| +======================== + +.. image:: https://img.shields.io/badge/-View%20on%20GitHub-128091?logo=github&labelColor=grey + :target: https://github.com/mrphys/im2sim + :alt: View on GitHub + +.. include:: ../../README.rst + :start-after: start-intro + :end-before: end-intro + + +.. toctree:: + :caption: API Documentation + :hidden: + + API documentation + api_docs/im2sim + ${namespaces} + + +.. meta:: + :google-site-verification: 8PySedj6KJ0kc5qC1CbO6_9blFB9Nho3SgXvbRzyVOU diff --git a/tools/docs/_build/dirhtml/_sources/tutorials.rst.txt b/tools/docs/_build/dirhtml/_sources/tutorials.rst.txt new file mode 100644 index 0000000..9c52220 --- /dev/null +++ b/tools/docs/_build/dirhtml/_sources/tutorials.rst.txt @@ -0,0 +1,10 @@ +TensorFlow MRI tutorials +======================== + +All TensorFlow MRI tutorials are written as Jupyter notebooks. + +In addition to viewing them on this website, you can run them directly in +Google Colab, a hosted notebook environment with no setup and free access to +GPUs. Click on the **Run in Colab** button to begin. + +Alternatively, you can also download the notebooks to run on your machine. diff --git a/tools/docs/_build/dirhtml/_static/basic.css b/tools/docs/_build/dirhtml/_static/basic.css new file mode 100644 index 0000000..7ebbd6d --- /dev/null +++ b/tools/docs/_build/dirhtml/_static/basic.css @@ -0,0 +1,914 @@ +/* + * Sphinx stylesheet -- basic theme. + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +div.section::after { + display: block; + content: ''; + clear: left; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; + word-wrap: break-word; + overflow-wrap : break-word; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox form.search { + overflow: hidden; +} + +div.sphinxsidebar #searchbox input[type="text"] { + float: left; + width: 80%; + padding: 0.25em; + box-sizing: border-box; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + float: left; + width: 20%; + border-left: none; + padding: 0.25em; + box-sizing: border-box; +} + + +img { + border: 0; + max-width: 100%; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin-top: 10px; +} + +ul.search li { + padding: 5px 0; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li p.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; + margin-left: auto; + margin-right: auto; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable ul { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} + +table.indextable > tbody > tr > td > ul { + padding-left: 0em; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + +/* -- general body styles --------------------------------------------------- */ + +div.body { + min-width: 360px; + max-width: 800px; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +a.headerlink { + visibility: hidden; +} + +a:visited { + color: #551A8B; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink, +caption:hover > a.headerlink, +p.caption:hover > a.headerlink, +div.code-block-caption:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, figure.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, figure.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, figure.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +img.align-default, figure.align-default, .figure.align-default { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-default { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar, +aside.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px; + background-color: #ffe; + width: 40%; + float: right; + clear: right; + overflow-x: auto; +} + +p.sidebar-title { + font-weight: bold; +} + +nav.contents, +aside.topic, +div.admonition, div.topic, blockquote { + clear: left; +} + +/* -- topics ---------------------------------------------------------------- */ + +nav.contents, +aside.topic, +div.topic { + border: 1px solid #ccc; + padding: 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- content of sidebars/topics/admonitions -------------------------------- */ + +div.sidebar > :last-child, +aside.sidebar > :last-child, +nav.contents > :last-child, +aside.topic > :last-child, +div.topic > :last-child, +div.admonition > :last-child { + margin-bottom: 0; +} + +div.sidebar::after, +aside.sidebar::after, +nav.contents::after, +aside.topic::after, +div.topic::after, +div.admonition::after, +blockquote::after { + display: block; + content: ''; + clear: both; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + margin-top: 10px; + margin-bottom: 10px; + border: 0; + border-collapse: collapse; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +table.align-default { + margin-left: auto; + margin-right: auto; +} + +table caption span.caption-number { + font-style: italic; +} + +table caption span.caption-text { +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +th > :first-child, +td > :first-child { + margin-top: 0px; +} + +th > :last-child, +td > :last-child { + margin-bottom: 0px; +} + +/* -- figures --------------------------------------------------------------- */ + +div.figure, figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption, figcaption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number, +figcaption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text, +figcaption span.caption-text { +} + +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist { + margin: 1em 0; +} + +table.hlist td { + vertical-align: top; +} + +/* -- object description styles --------------------------------------------- */ + +.sig { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; +} + +.sig-name, code.descname { + background-color: transparent; + font-weight: bold; +} + +.sig-name { + font-size: 1.1em; +} + +code.descname { + font-size: 1.2em; +} + +.sig-prename, code.descclassname { + background-color: transparent; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.sig-param.n { + font-style: italic; +} + +/* C++ specific styling */ + +.sig-inline.c-texpr, +.sig-inline.cpp-texpr { + font-family: unset; +} + +.sig.c .k, .sig.c .kt, +.sig.cpp .k, .sig.cpp .kt { + color: #0033B3; +} + +.sig.c .m, +.sig.cpp .m { + color: #1750EB; +} + +.sig.c .s, .sig.c .sc, +.sig.cpp .s, .sig.cpp .sc { + color: #067D17; +} + + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +:not(li) > ol > li:first-child > :first-child, +:not(li) > ul > li:first-child > :first-child { + margin-top: 0px; +} + +:not(li) > ol > li:last-child > :last-child, +:not(li) > ul > li:last-child > :last-child { + margin-bottom: 0px; +} + +ol.simple ol p, +ol.simple ul p, +ul.simple ol p, +ul.simple ul p { + margin-top: 0; +} + +ol.simple > li:not(:first-child) > p, +ul.simple > li:not(:first-child) > p { + margin-top: 0; +} + +ol.simple p, +ul.simple p { + margin-bottom: 0; +} + +aside.footnote > span, +div.citation > span { + float: left; +} +aside.footnote > span:last-of-type, +div.citation > span:last-of-type { + padding-right: 0.5em; +} +aside.footnote > p { + margin-left: 2em; +} +div.citation > p { + margin-left: 4em; +} +aside.footnote > p:last-of-type, +div.citation > p:last-of-type { + margin-bottom: 0em; +} +aside.footnote > p:last-of-type:after, +div.citation > p:last-of-type:after { + content: ""; + clear: both; +} + +dl.field-list { + display: grid; + grid-template-columns: fit-content(30%) auto; +} + +dl.field-list > dt { + font-weight: bold; + word-break: break-word; + padding-left: 0.5em; + padding-right: 5px; +} + +dl.field-list > dd { + padding-left: 0.5em; + margin-top: 0em; + margin-left: 0em; + margin-bottom: 0em; +} + +dl { + margin-bottom: 15px; +} + +dd > :first-child { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +.sig dd { + margin-top: 0px; + margin-bottom: 0px; +} + +.sig dl { + margin-top: 0px; + margin-bottom: 0px; +} + +dl > dd:last-child, +dl > dd:last-child > :last-child { + margin-bottom: 0; +} + +dt:target, span.highlighted { + background-color: #fbe54e; +} + +rect.highlighted { + fill: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +.classifier:before { + font-style: normal; + margin: 0 0.5em; + content: ":"; + display: inline-block; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +.translated { + background-color: rgba(207, 255, 207, 0.2) +} + +.untranslated { + background-color: rgba(255, 207, 207, 0.2) +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +pre, div[class*="highlight-"] { + clear: both; +} + +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; + white-space: nowrap; +} + +div[class*="highlight-"] { + margin: 1em 0; +} + +td.linenos pre { + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + display: block; +} + +table.highlighttable tbody { + display: block; +} + +table.highlighttable tr { + display: flex; +} + +table.highlighttable td { + margin: 0; + padding: 0; +} + +table.highlighttable td.linenos { + padding-right: 0.5em; +} + +table.highlighttable td.code { + flex: 1; + overflow: hidden; +} + +.highlight .hll { + display: block; +} + +div.highlight pre, +table.highlighttable pre { + margin: 0; +} + +div.code-block-caption + div { + margin-top: 0; +} + +div.code-block-caption { + margin-top: 1em; + padding: 2px 5px; + font-size: small; +} + +div.code-block-caption code { + background-color: transparent; +} + +table.highlighttable td.linenos, +span.linenos, +div.highlight span.gp { /* gp: Generic.Prompt */ + user-select: none; + -webkit-user-select: text; /* Safari fallback only */ + -webkit-user-select: none; /* Chrome/Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+ */ +} + +div.code-block-caption span.caption-number { + padding: 0.1em 0.3em; + font-style: italic; +} + +div.code-block-caption span.caption-text { +} + +div.literal-block-wrapper { + margin: 1em 0; +} + +code.xref, a code { + background-color: transparent; + font-weight: bold; +} + +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +span.eqno a.headerlink { + position: absolute; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/_static/doctools.js b/tools/docs/_build/dirhtml/_static/doctools.js new file mode 100644 index 0000000..0398ebb --- /dev/null +++ b/tools/docs/_build/dirhtml/_static/doctools.js @@ -0,0 +1,149 @@ +/* + * Base JavaScript utilities for all Sphinx HTML documentation. + */ +"use strict"; + +const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ + "TEXTAREA", + "INPUT", + "SELECT", + "BUTTON", +]); + +const _ready = (callback) => { + if (document.readyState !== "loading") { + callback(); + } else { + document.addEventListener("DOMContentLoaded", callback); + } +}; + +/** + * Small JavaScript module for the documentation. + */ +const Documentation = { + init: () => { + Documentation.initDomainIndexTable(); + Documentation.initOnKeyListeners(); + }, + + /** + * i18n support + */ + TRANSLATIONS: {}, + PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), + LOCALE: "unknown", + + // gettext and ngettext don't access this so that the functions + // can safely bound to a different name (_ = Documentation.gettext) + gettext: (string) => { + const translated = Documentation.TRANSLATIONS[string]; + switch (typeof translated) { + case "undefined": + return string; // no translation + case "string": + return translated; // translation exists + default: + return translated[0]; // (singular, plural) translation tuple exists + } + }, + + ngettext: (singular, plural, n) => { + const translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated !== "undefined") + return translated[Documentation.PLURAL_EXPR(n)]; + return n === 1 ? singular : plural; + }, + + addTranslations: (catalog) => { + Object.assign(Documentation.TRANSLATIONS, catalog.messages); + Documentation.PLURAL_EXPR = new Function( + "n", + `return (${catalog.plural_expr})` + ); + Documentation.LOCALE = catalog.locale; + }, + + /** + * helper function to focus on search bar + */ + focusSearchBar: () => { + document.querySelectorAll("input[name=q]")[0]?.focus(); + }, + + /** + * Initialise the domain index toggle buttons + */ + initDomainIndexTable: () => { + const toggler = (el) => { + const idNumber = el.id.substr(7); + const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); + if (el.src.substr(-9) === "minus.png") { + el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; + toggledRows.forEach((el) => (el.style.display = "none")); + } else { + el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; + toggledRows.forEach((el) => (el.style.display = "")); + } + }; + + const togglerElements = document.querySelectorAll("img.toggler"); + togglerElements.forEach((el) => + el.addEventListener("click", (event) => toggler(event.currentTarget)) + ); + togglerElements.forEach((el) => (el.style.display = "")); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); + }, + + initOnKeyListeners: () => { + // only install a listener if it is really needed + if ( + !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && + !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + ) + return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.altKey || event.ctrlKey || event.metaKey) return; + + if (!event.shiftKey) { + switch (event.key) { + case "ArrowLeft": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const prevLink = document.querySelector('link[rel="prev"]'); + if (prevLink && prevLink.href) { + window.location.href = prevLink.href; + event.preventDefault(); + } + break; + case "ArrowRight": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const nextLink = document.querySelector('link[rel="next"]'); + if (nextLink && nextLink.href) { + window.location.href = nextLink.href; + event.preventDefault(); + } + break; + } + } + + // some keyboard layouts may need Shift to get / + switch (event.key) { + case "/": + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; + Documentation.focusSearchBar(); + event.preventDefault(); + } + }); + }, +}; + +// quick alias for translations +const _ = Documentation.gettext; + +_ready(Documentation.init); diff --git a/tools/docs/_build/dirhtml/_static/documentation_options.js b/tools/docs/_build/dirhtml/_static/documentation_options.js new file mode 100644 index 0000000..4355f45 --- /dev/null +++ b/tools/docs/_build/dirhtml/_static/documentation_options.js @@ -0,0 +1,13 @@ +const DOCUMENTATION_OPTIONS = { + VERSION: '0.1.0', + LANGUAGE: 'en', + COLLAPSE_INDEX: false, + BUILDER: 'dirhtml', + FILE_SUFFIX: '.html', + LINK_SUFFIX: '.html', + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '.txt', + NAVIGATION_WITH_KEYS: false, + SHOW_SEARCH_SUMMARY: true, + ENABLE_SEARCH_SHORTCUTS: true, +}; \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/_static/download_icon_white.svg b/tools/docs/_build/dirhtml/_static/download_icon_white.svg new file mode 100644 index 0000000..46bc259 --- /dev/null +++ b/tools/docs/_build/dirhtml/_static/download_icon_white.svg @@ -0,0 +1,211 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/docs/_build/dirhtml/_static/download_icon_white_32px.png b/tools/docs/_build/dirhtml/_static/download_icon_white_32px.png new file mode 100644 index 0000000..3d1e4fd Binary files /dev/null and b/tools/docs/_build/dirhtml/_static/download_icon_white_32px.png differ diff --git a/tools/docs/_build/dirhtml/_static/file.png b/tools/docs/_build/dirhtml/_static/file.png new file mode 100644 index 0000000..a858a41 Binary files /dev/null and b/tools/docs/_build/dirhtml/_static/file.png differ diff --git a/tools/docs/_build/dirhtml/_static/im2sim_logo.png b/tools/docs/_build/dirhtml/_static/im2sim_logo.png new file mode 100644 index 0000000..84b764a Binary files /dev/null and b/tools/docs/_build/dirhtml/_static/im2sim_logo.png differ diff --git a/tools/docs/_build/dirhtml/_static/language_data.js b/tools/docs/_build/dirhtml/_static/language_data.js new file mode 100644 index 0000000..c7fe6c6 --- /dev/null +++ b/tools/docs/_build/dirhtml/_static/language_data.js @@ -0,0 +1,192 @@ +/* + * This script contains the language-specific data used by searchtools.js, + * namely the list of stopwords, stemmer, scorer and splitter. + */ + +var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"]; + + +/* Non-minified version is copied as a separate JS file, if available */ + +/** + * Porter Stemmer + */ +var Stemmer = function() { + + var step2list = { + ational: 'ate', + tional: 'tion', + enci: 'ence', + anci: 'ance', + izer: 'ize', + bli: 'ble', + alli: 'al', + entli: 'ent', + eli: 'e', + ousli: 'ous', + ization: 'ize', + ation: 'ate', + ator: 'ate', + alism: 'al', + iveness: 'ive', + fulness: 'ful', + ousness: 'ous', + aliti: 'al', + iviti: 'ive', + biliti: 'ble', + logi: 'log' + }; + + var step3list = { + icate: 'ic', + ative: '', + alize: 'al', + iciti: 'ic', + ical: 'ic', + ful: '', + ness: '' + }; + + var c = "[^aeiou]"; // consonant + var v = "[aeiouy]"; // vowel + var C = c + "[^aeiouy]*"; // consonant sequence + var V = v + "[aeiou]*"; // vowel sequence + + var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0 + var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 + var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 + var s_v = "^(" + C + ")?" + v; // vowel in stem + + this.stemWord = function (w) { + var stem; + var suffix; + var firstch; + var origword = w; + + if (w.length < 3) + return w; + + var re; + var re2; + var re3; + var re4; + + firstch = w.substr(0,1); + if (firstch == "y") + w = firstch.toUpperCase() + w.substr(1); + + // Step 1a + re = /^(.+?)(ss|i)es$/; + re2 = /^(.+?)([^s])s$/; + + if (re.test(w)) + w = w.replace(re,"$1$2"); + else if (re2.test(w)) + w = w.replace(re2,"$1$2"); + + // Step 1b + re = /^(.+?)eed$/; + re2 = /^(.+?)(ed|ing)$/; + if (re.test(w)) { + var fp = re.exec(w); + re = new RegExp(mgr0); + if (re.test(fp[1])) { + re = /.$/; + w = w.replace(re,""); + } + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = new RegExp(s_v); + if (re2.test(stem)) { + w = stem; + re2 = /(at|bl|iz)$/; + re3 = new RegExp("([^aeiouylsz])\\1$"); + re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re2.test(w)) + w = w + "e"; + else if (re3.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + else if (re4.test(w)) + w = w + "e"; + } + } + + // Step 1c + re = /^(.+?)y$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(s_v); + if (re.test(stem)) + w = stem + "i"; + } + + // Step 2 + re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step2list[suffix]; + } + + // Step 3 + re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step3list[suffix]; + } + + // Step 4 + re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + re2 = /^(.+?)(s|t)(ion)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + if (re.test(stem)) + w = stem; + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = new RegExp(mgr1); + if (re2.test(stem)) + w = stem; + } + + // Step 5 + re = /^(.+?)e$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + re2 = new RegExp(meq1); + re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) + w = stem; + } + re = /ll$/; + re2 = new RegExp(mgr1); + if (re.test(w) && re2.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + + // and turn initial Y back to y + if (firstch == "y") + w = firstch.toLowerCase() + w.substr(1); + return w; + } +} + diff --git a/tools/docs/_build/dirhtml/_static/minus.png b/tools/docs/_build/dirhtml/_static/minus.png new file mode 100644 index 0000000..d96755f Binary files /dev/null and b/tools/docs/_build/dirhtml/_static/minus.png differ diff --git a/tools/docs/_build/dirhtml/_static/mystnb.11b39860a7a0cbfd473a3ad8a317855267ff0bd372690045ca344a6b62be495e.css b/tools/docs/_build/dirhtml/_static/mystnb.11b39860a7a0cbfd473a3ad8a317855267ff0bd372690045ca344a6b62be495e.css new file mode 100644 index 0000000..9f3d764 --- /dev/null +++ b/tools/docs/_build/dirhtml/_static/mystnb.11b39860a7a0cbfd473a3ad8a317855267ff0bd372690045ca344a6b62be495e.css @@ -0,0 +1,2449 @@ +/* Dark mode support: + * if (e.g. Furo theme) or (e.g. PyData theme) has `data-theme` set, respect it. + * else default to the system color scheme + */ +@media (prefers-color-scheme: dark) { + :root { + --light: ; + --dark: initial; + } +} + +@media (prefers-color-scheme: light) { + :root { + --dark: ; + --light: initial; + } +} + +:is(html, body)[data-theme="dark"] { + --light: ; + --dark: initial; +} + +:is(html, body)[data-theme="light"] { + --dark: ; + --light: initial; +} + +/* Variables */ +:root { + /* + Following palettes are generated by using https://m2.material.io/design/color/the-color-system.html#tools-for-picking-colors + - neutral palette with #fcfcfc and danger palette with #ffdddd as base colors. + 50 means lightest, 900 means darkest; less used intermediate shades are omitted + but can be added when needed by accessing full palette from the above link. + */ + --mystnb-neutral-palette-50: #fcfcfc; + --mystnb-neutral-palette-100: #f7f7f7; + --mystnb-neutral-palette-400: #cccccc; + --mystnb-neutral-palette-500: #afafaf; + --mystnb-neutral-palette-800: #505050; + --mystnb-neutral-palette-900: #2d2d2d; + + --mystnb-danger-palette-50: #ffdddd; + --mystnb-danger-palette-100: #f5acad; + --mystnb-danger-palette-400: #c42029; + --mystnb-danger-palette-500: #b40008; + --mystnb-danger-palette-800: #850010; + --mystnb-danger-palette-900: #680010; + + /* MyST-NB specific variables; colors should be logically picked from palettes */ + --mystnb-source-bg-color: var(--light, var(--mystnb-neutral-palette-100)) var(--dark, var(--mystnb-neutral-palette-800)); + --mystnb-stdout-bg-color: var(--light, var(--mystnb-neutral-palette-50)) var(--dark, var(--mystnb-neutral-palette-900)); + --mystnb-stderr-bg-color: var(--light, var(--mystnb-danger-palette-50)) var(--dark, var(--mystnb-danger-palette-900)); + --mystnb-traceback-bg-color: var(--light, var(--mystnb-neutral-palette-50)) var(--dark, var(--mystnb-neutral-palette-900)); + --mystnb-source-border-color: var(--light, var(--mystnb-neutral-palette-400)) var(--dark, var(--mystnb-neutral-palette-500)); + --mystnb-source-margin-color: green; + --mystnb-stdout-border-color: var(--light, var(--mystnb-neutral-palette-100)) var(--dark, var(--mystnb-neutral-palette-800)); + --mystnb-stderr-border-color: var(--light, var(--mystnb-neutral-palette-100)) var(--dark, var(--mystnb-neutral-palette-800)); + --mystnb-traceback-border-color: var(--light, var(--mystnb-danger-palette-100)) var(--dark, var(--mystnb-danger-palette-800)); + --mystnb-hide-prompt-opacity: 70%; + --mystnb-source-border-radius: .4em; + --mystnb-source-border-width: 1px; + --mystnb-scrollbar-width: 0.3rem; + --mystnb-scrollbar-height: 0.3rem; + --mystnb-scrollbar-thumb-color: var(--light, var(--mystnb-neutral-palette-400)) var(--dark, var(--mystnb-neutral-palette-500)); + --mystnb-scrollbar-thumb-hover-color: var(--light, var(--mystnb-neutral-palette-500)) var(--dark, var(--mystnb-neutral-palette-400)); + --mystnb-scrollbar-thumb-border-radius: 0.25rem; +} + + +/* Whole cell */ +div.container.cell { + padding-left: 0; + margin-bottom: 1em; +} + +/* Removing all background formatting so we can control at the div level */ +.cell_input div.highlight, +.cell_output pre, +.cell_input pre, +.cell_output .output { + border: none; + box-shadow: none; +} + +.cell_output .output pre, +.cell_input pre { + margin: 0px; +} + +/* Input cells */ +div.cell > div.cell_input { + padding-left: 0em; + padding-right: 0em; + border: var(--mystnb-source-border-width) var(--mystnb-source-border-color) solid; + background-color: var(--mystnb-source-bg-color); + border-left-color: var(--mystnb-source-margin-color); + border-left-width: medium; + border-radius: var(--mystnb-source-border-radius); +} + +div.cell_input>div, +div.cell_output div.output>div.highlight { + margin: 0em !important; + border: none !important; +} + +/* All cell outputs */ +.cell_output { + padding-left: 1em; + padding-right: 0em; + margin-top: 1em; +} + +/* Text outputs from cells */ +.cell_output .output.text_plain, +.cell_output .output.traceback, +.cell_output .output.stream, +.cell_output .output.stderr { + margin-top: 1em; + margin-bottom: 0em; + box-shadow: none; +} + +.cell_output .output.text_plain:not(:has(.highlight)), +.cell_output .output.stream:not(:has(.highlight)) { + /* plain (or stream of) output, not containing a pygments-highlighted block */ + background: var(--mystnb-stdout-bg-color); + border: 1px solid var(--mystnb-stdout-border-color); +} + +.cell_output .output.stderr { + background: var(--mystnb-stderr-bg-color); + border: 1px solid var(--mystnb-stderr-border-color); +} + +.cell_output .output.traceback { + background: var(--mystnb-traceback-bg-color); + border: 1px solid var(--mystnb-traceback-border-color); +} + +/* --- Collapsible cell content --- */ + +/* +encourage summary container to blend in with its parent. +p.admonition-title should hold the title styles. +*/ +div.cell details.hide summary { + border-left: unset; + padding: inherit; + margin: inherit; + background-color: inherit; +} + +/* Neighboring input/output elements - spacing, borders */ +div.cell details.hide.above-input + details.below-input, +div.cell div.cell_input + details.below-input +{ + margin-top: 0; +} + +div.cell details.hide.above-input:has(+ details.below-input), +div.cell div.cell_input:has(+ details.below-input) +{ + margin-bottom: 0; +} + +div.cell:has(> *:nth-child(2)) div.cell_input:first-child, +div.cell:has(> *:nth-child(2)) details:first-child +{ + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +div.cell:has(> *:nth-child(2)) div.cell_input:last-child, +div.cell:has(> *:nth-child(2)) details:last-child +{ + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +/* intra-label styles for collapsibles */ +div.cell.container details.hide.above-input>summary, +div.cell.container details.hide.below-input>summary, +div.cell.container details.hide.above-output>summary +{ + display: block; + border-left: none; +} + +div.cell details.hide>summary>p.admonition-title { + display: list-item; + margin-bottom: 0; +} + +div.cell details.hide:not([open]) { + padding-bottom: 0; +} + +div.cell details.hide[open]>summary>p.collapsed { + display: none; +} + +div.cell details.hide:not([open])>summary>p.expanded { + display: none; +} + +@keyframes collapsed-fade-in { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} +div.cell details.hide[open]>summary~* { + -moz-animation: collapsed-fade-in 0.3s ease-in-out; + -webkit-animation: collapsed-fade-in 0.3s ease-in-out; + animation: collapsed-fade-in 0.3s ease-in-out; +} + +/* Clear conflicting styles for details and admonitions set by some themes */ +div.cell details.admonition summary::before { + content: unset; +} + +/* Math align to the left */ +.cell_output .MathJax_Display { + text-align: left !important; +} + +/** source code line numbers **/ +span.linenos { + opacity: 0.5; +} + +/* Inline text from `paste` operation */ + +span.pasted-text { + font-weight: bold; +} + +span.pasted-inline img { + max-height: 2em; +} + +tbody span.pasted-inline img { + max-height: none; +} + + +/* Adding scroll bars if tags: output_scroll, scroll-output, and scroll-input + * On screens, we want to scroll, but on print show all + * + * It was before in https://github.com/executablebooks/sphinx-book-theme/blob/eb1b6baf098b27605e8f2b7b2979b17ebf1b9540/src/sphinx_book_theme/assets/styles/extensions/_myst-nb.scss +*/ +div.cell:is( + .tag_output_scroll, + .tag_scroll-output, + .config_scroll_outputs + ) + div.cell_output, +div.cell.tag_scroll-input div.cell_input { + max-height: 24em; + overflow-y: auto; + max-width: 100%; + overflow-x: auto; +} + +div.cell.config_scroll_outputs div.cell_output:has(img) { + /* If the output cell has image(s), allow it to take 90% of viewport height + but still bounded between 24em and 60em */ + max-height: clamp(24em, 90vh, 60em); +} + +/* Custom scrollbars */ +div.cell:is( + .tag_output_scroll, + .tag_scroll-output, + .config_scroll_outputs + ) + div.cell_output::-webkit-scrollbar, +div.cell.tag_scroll-input div.cell_input::-webkit-scrollbar { + width: var(--mystnb-scrollbar-width); + height: var(--mystnb-scrollbar-height); +} + +div.cell:is( + .tag_output_scroll, + .tag_scroll-output, + .config_scroll_outputs + ) + div.cell_output::-webkit-scrollbar-thumb, +div.cell.tag_scroll-input div.cell_input::-webkit-scrollbar-thumb { + background: var(--mystnb-scrollbar-thumb-color); + border-radius: var(--mystnb-scrollbar-thumb-border-radius); +} + +div.cell:is( + .tag_output_scroll, + .tag_scroll-output, + .config_scroll_outputs + ) + div.cell_output::-webkit-scrollbar-thumb:hover, +div.cell.tag_scroll-input div.cell_input::-webkit-scrollbar-thumb:hover { + background: var(--mystnb-scrollbar-thumb-hover-color); +} + +/* In print mode, unset scroll styles */ +@media print { + div.cell:is( + .tag_output_scroll, + .tag_scroll-output, + .config_scroll_outputs + ) + div.cell_output, + div.cell.tag_scroll-input div.cell_input { + max-height: unset; + overflow-y: visible; + max-width: unset; + overflow-x: visible; + } +} + +/* Font colors for translated ANSI escape sequences +Color values are copied from Jupyter Notebook +https://github.com/jupyter/notebook/blob/52581f8eda9b319eb0390ac77fe5903c38f81e3e/notebook/static/notebook/less/ansicolors.less#L14-L21 +Background colors from +https://nbsphinx.readthedocs.io/en/latest/code-cells.html#ANSI-Colors +*/ +div.highlight .-Color-Bold { + font-weight: bold; +} + +div.highlight .-Color[class*=-Black] { + color: #3E424D +} + +div.highlight .-Color[class*=-Red] { + color: #E75C58 +} + +div.highlight .-Color[class*=-Green] { + color: #00A250 +} + +div.highlight .-Color[class*=-Yellow] { + color: #DDB62B +} + +div.highlight .-Color[class*=-Blue] { + color: #208FFB +} + +div.highlight .-Color[class*=-Magenta] { + color: #D160C4 +} + +div.highlight .-Color[class*=-Cyan] { + color: #60C6C8 +} + +div.highlight .-Color[class*=-White] { + color: #C5C1B4 +} + +div.highlight .-Color[class*=-BGBlack] { + background-color: #3E424D +} + +div.highlight .-Color[class*=-BGRed] { + background-color: #E75C58 +} + +div.highlight .-Color[class*=-BGGreen] { + background-color: #00A250 +} + +div.highlight .-Color[class*=-BGYellow] { + background-color: #DDB62B +} + +div.highlight .-Color[class*=-BGBlue] { + background-color: #208FFB +} + +div.highlight .-Color[class*=-BGMagenta] { + background-color: #D160C4 +} + +div.highlight .-Color[class*=-BGCyan] { + background-color: #60C6C8 +} + +div.highlight .-Color[class*=-BGWhite] { + background-color: #C5C1B4 +} + +/* Font colors for 8-bit ANSI */ + +div.highlight .-Color[class*=-C0] { + color: #000000 +} + +div.highlight .-Color[class*=-BGC0] { + background-color: #000000 +} + +div.highlight .-Color[class*=-C1] { + color: #800000 +} + +div.highlight .-Color[class*=-BGC1] { + background-color: #800000 +} + +div.highlight .-Color[class*=-C2] { + color: #008000 +} + +div.highlight .-Color[class*=-BGC2] { + background-color: #008000 +} + +div.highlight .-Color[class*=-C3] { + color: #808000 +} + +div.highlight .-Color[class*=-BGC3] { + background-color: #808000 +} + +div.highlight .-Color[class*=-C4] { + color: #000080 +} + +div.highlight .-Color[class*=-BGC4] { + background-color: #000080 +} + +div.highlight .-Color[class*=-C5] { + color: #800080 +} + +div.highlight .-Color[class*=-BGC5] { + background-color: #800080 +} + +div.highlight .-Color[class*=-C6] { + color: #008080 +} + +div.highlight .-Color[class*=-BGC6] { + background-color: #008080 +} + +div.highlight .-Color[class*=-C7] { + color: #C0C0C0 +} + +div.highlight .-Color[class*=-BGC7] { + background-color: #C0C0C0 +} + +div.highlight .-Color[class*=-C8] { + color: #808080 +} + +div.highlight .-Color[class*=-BGC8] { + background-color: #808080 +} + +div.highlight .-Color[class*=-C9] { + color: #FF0000 +} + +div.highlight .-Color[class*=-BGC9] { + background-color: #FF0000 +} + +div.highlight .-Color[class*=-C10] { + color: #00FF00 +} + +div.highlight .-Color[class*=-BGC10] { + background-color: #00FF00 +} + +div.highlight .-Color[class*=-C11] { + color: #FFFF00 +} + +div.highlight .-Color[class*=-BGC11] { + background-color: #FFFF00 +} + +div.highlight .-Color[class*=-C12] { + color: #0000FF +} + +div.highlight .-Color[class*=-BGC12] { + background-color: #0000FF +} + +div.highlight .-Color[class*=-C13] { + color: #FF00FF +} + +div.highlight .-Color[class*=-BGC13] { + background-color: #FF00FF +} + +div.highlight .-Color[class*=-C14] { + color: #00FFFF +} + +div.highlight .-Color[class*=-BGC14] { + background-color: #00FFFF +} + +div.highlight .-Color[class*=-C15] { + color: #FFFFFF +} + +div.highlight .-Color[class*=-BGC15] { + background-color: #FFFFFF +} + +div.highlight .-Color[class*=-C16] { + color: #000000 +} + +div.highlight .-Color[class*=-BGC16] { + background-color: #000000 +} + +div.highlight .-Color[class*=-C17] { + color: #00005F +} + +div.highlight .-Color[class*=-BGC17] { + background-color: #00005F +} + +div.highlight .-Color[class*=-C18] { + color: #000087 +} + +div.highlight .-Color[class*=-BGC18] { + background-color: #000087 +} + +div.highlight .-Color[class*=-C19] { + color: #0000AF +} + +div.highlight .-Color[class*=-BGC19] { + background-color: #0000AF +} + +div.highlight .-Color[class*=-C20] { + color: #0000D7 +} + +div.highlight .-Color[class*=-BGC20] { + background-color: #0000D7 +} + +div.highlight .-Color[class*=-C21] { + color: #0000FF +} + +div.highlight .-Color[class*=-BGC21] { + background-color: #0000FF +} + +div.highlight .-Color[class*=-C22] { + color: #005F00 +} + +div.highlight .-Color[class*=-BGC22] { + background-color: #005F00 +} + +div.highlight .-Color[class*=-C23] { + color: #005F5F +} + +div.highlight .-Color[class*=-BGC23] { + background-color: #005F5F +} + +div.highlight .-Color[class*=-C24] { + color: #005F87 +} + +div.highlight .-Color[class*=-BGC24] { + background-color: #005F87 +} + +div.highlight .-Color[class*=-C25] { + color: #005FAF +} + +div.highlight .-Color[class*=-BGC25] { + background-color: #005FAF +} + +div.highlight .-Color[class*=-C26] { + color: #005FD7 +} + +div.highlight .-Color[class*=-BGC26] { + background-color: #005FD7 +} + +div.highlight .-Color[class*=-C27] { + color: #005FFF +} + +div.highlight .-Color[class*=-BGC27] { + background-color: #005FFF +} + +div.highlight .-Color[class*=-C28] { + color: #008700 +} + +div.highlight .-Color[class*=-BGC28] { + background-color: #008700 +} + +div.highlight .-Color[class*=-C29] { + color: #00875F +} + +div.highlight .-Color[class*=-BGC29] { + background-color: #00875F +} + +div.highlight .-Color[class*=-C30] { + color: #008787 +} + +div.highlight .-Color[class*=-BGC30] { + background-color: #008787 +} + +div.highlight .-Color[class*=-C31] { + color: #0087AF +} + +div.highlight .-Color[class*=-BGC31] { + background-color: #0087AF +} + +div.highlight .-Color[class*=-C32] { + color: #0087D7 +} + +div.highlight .-Color[class*=-BGC32] { + background-color: #0087D7 +} + +div.highlight .-Color[class*=-C33] { + color: #0087FF +} + +div.highlight .-Color[class*=-BGC33] { + background-color: #0087FF +} + +div.highlight .-Color[class*=-C34] { + color: #00AF00 +} + +div.highlight .-Color[class*=-BGC34] { + background-color: #00AF00 +} + +div.highlight .-Color[class*=-C35] { + color: #00AF5F +} + +div.highlight .-Color[class*=-BGC35] { + background-color: #00AF5F +} + +div.highlight .-Color[class*=-C36] { + color: #00AF87 +} + +div.highlight .-Color[class*=-BGC36] { + background-color: #00AF87 +} + +div.highlight .-Color[class*=-C37] { + color: #00AFAF +} + +div.highlight .-Color[class*=-BGC37] { + background-color: #00AFAF +} + +div.highlight .-Color[class*=-C38] { + color: #00AFD7 +} + +div.highlight .-Color[class*=-BGC38] { + background-color: #00AFD7 +} + +div.highlight .-Color[class*=-C39] { + color: #00AFFF +} + +div.highlight .-Color[class*=-BGC39] { + background-color: #00AFFF +} + +div.highlight .-Color[class*=-C40] { + color: #00D700 +} + +div.highlight .-Color[class*=-BGC40] { + background-color: #00D700 +} + +div.highlight .-Color[class*=-C41] { + color: #00D75F +} + +div.highlight .-Color[class*=-BGC41] { + background-color: #00D75F +} + +div.highlight .-Color[class*=-C42] { + color: #00D787 +} + +div.highlight .-Color[class*=-BGC42] { + background-color: #00D787 +} + +div.highlight .-Color[class*=-C43] { + color: #00D7AF +} + +div.highlight .-Color[class*=-BGC43] { + background-color: #00D7AF +} + +div.highlight .-Color[class*=-C44] { + color: #00D7D7 +} + +div.highlight .-Color[class*=-BGC44] { + background-color: #00D7D7 +} + +div.highlight .-Color[class*=-C45] { + color: #00D7FF +} + +div.highlight .-Color[class*=-BGC45] { + background-color: #00D7FF +} + +div.highlight .-Color[class*=-C46] { + color: #00FF00 +} + +div.highlight .-Color[class*=-BGC46] { + background-color: #00FF00 +} + +div.highlight .-Color[class*=-C47] { + color: #00FF5F +} + +div.highlight .-Color[class*=-BGC47] { + background-color: #00FF5F +} + +div.highlight .-Color[class*=-C48] { + color: #00FF87 +} + +div.highlight .-Color[class*=-BGC48] { + background-color: #00FF87 +} + +div.highlight .-Color[class*=-C49] { + color: #00FFAF +} + +div.highlight .-Color[class*=-BGC49] { + background-color: #00FFAF +} + +div.highlight .-Color[class*=-C50] { + color: #00FFD7 +} + +div.highlight .-Color[class*=-BGC50] { + background-color: #00FFD7 +} + +div.highlight .-Color[class*=-C51] { + color: #00FFFF +} + +div.highlight .-Color[class*=-BGC51] { + background-color: #00FFFF +} + +div.highlight .-Color[class*=-C52] { + color: #5F0000 +} + +div.highlight .-Color[class*=-BGC52] { + background-color: #5F0000 +} + +div.highlight .-Color[class*=-C53] { + color: #5F005F +} + +div.highlight .-Color[class*=-BGC53] { + background-color: #5F005F +} + +div.highlight .-Color[class*=-C54] { + color: #5F0087 +} + +div.highlight .-Color[class*=-BGC54] { + background-color: #5F0087 +} + +div.highlight .-Color[class*=-C55] { + color: #5F00AF +} + +div.highlight .-Color[class*=-BGC55] { + background-color: #5F00AF +} + +div.highlight .-Color[class*=-C56] { + color: #5F00D7 +} + +div.highlight .-Color[class*=-BGC56] { + background-color: #5F00D7 +} + +div.highlight .-Color[class*=-C57] { + color: #5F00FF +} + +div.highlight .-Color[class*=-BGC57] { + background-color: #5F00FF +} + +div.highlight .-Color[class*=-C58] { + color: #5F5F00 +} + +div.highlight .-Color[class*=-BGC58] { + background-color: #5F5F00 +} + +div.highlight .-Color[class*=-C59] { + color: #5F5F5F +} + +div.highlight .-Color[class*=-BGC59] { + background-color: #5F5F5F +} + +div.highlight .-Color[class*=-C60] { + color: #5F5F87 +} + +div.highlight .-Color[class*=-BGC60] { + background-color: #5F5F87 +} + +div.highlight .-Color[class*=-C61] { + color: #5F5FAF +} + +div.highlight .-Color[class*=-BGC61] { + background-color: #5F5FAF +} + +div.highlight .-Color[class*=-C62] { + color: #5F5FD7 +} + +div.highlight .-Color[class*=-BGC62] { + background-color: #5F5FD7 +} + +div.highlight .-Color[class*=-C63] { + color: #5F5FFF +} + +div.highlight .-Color[class*=-BGC63] { + background-color: #5F5FFF +} + +div.highlight .-Color[class*=-C64] { + color: #5F8700 +} + +div.highlight .-Color[class*=-BGC64] { + background-color: #5F8700 +} + +div.highlight .-Color[class*=-C65] { + color: #5F875F +} + +div.highlight .-Color[class*=-BGC65] { + background-color: #5F875F +} + +div.highlight .-Color[class*=-C66] { + color: #5F8787 +} + +div.highlight .-Color[class*=-BGC66] { + background-color: #5F8787 +} + +div.highlight .-Color[class*=-C67] { + color: #5F87AF +} + +div.highlight .-Color[class*=-BGC67] { + background-color: #5F87AF +} + +div.highlight .-Color[class*=-C68] { + color: #5F87D7 +} + +div.highlight .-Color[class*=-BGC68] { + background-color: #5F87D7 +} + +div.highlight .-Color[class*=-C69] { + color: #5F87FF +} + +div.highlight .-Color[class*=-BGC69] { + background-color: #5F87FF +} + +div.highlight .-Color[class*=-C70] { + color: #5FAF00 +} + +div.highlight .-Color[class*=-BGC70] { + background-color: #5FAF00 +} + +div.highlight .-Color[class*=-C71] { + color: #5FAF5F +} + +div.highlight .-Color[class*=-BGC71] { + background-color: #5FAF5F +} + +div.highlight .-Color[class*=-C72] { + color: #5FAF87 +} + +div.highlight .-Color[class*=-BGC72] { + background-color: #5FAF87 +} + +div.highlight .-Color[class*=-C73] { + color: #5FAFAF +} + +div.highlight .-Color[class*=-BGC73] { + background-color: #5FAFAF +} + +div.highlight .-Color[class*=-C74] { + color: #5FAFD7 +} + +div.highlight .-Color[class*=-BGC74] { + background-color: #5FAFD7 +} + +div.highlight .-Color[class*=-C75] { + color: #5FAFFF +} + +div.highlight .-Color[class*=-BGC75] { + background-color: #5FAFFF +} + +div.highlight .-Color[class*=-C76] { + color: #5FD700 +} + +div.highlight .-Color[class*=-BGC76] { + background-color: #5FD700 +} + +div.highlight .-Color[class*=-C77] { + color: #5FD75F +} + +div.highlight .-Color[class*=-BGC77] { + background-color: #5FD75F +} + +div.highlight .-Color[class*=-C78] { + color: #5FD787 +} + +div.highlight .-Color[class*=-BGC78] { + background-color: #5FD787 +} + +div.highlight .-Color[class*=-C79] { + color: #5FD7AF +} + +div.highlight .-Color[class*=-BGC79] { + background-color: #5FD7AF +} + +div.highlight .-Color[class*=-C80] { + color: #5FD7D7 +} + +div.highlight .-Color[class*=-BGC80] { + background-color: #5FD7D7 +} + +div.highlight .-Color[class*=-C81] { + color: #5FD7FF +} + +div.highlight .-Color[class*=-BGC81] { + background-color: #5FD7FF +} + +div.highlight .-Color[class*=-C82] { + color: #5FFF00 +} + +div.highlight .-Color[class*=-BGC82] { + background-color: #5FFF00 +} + +div.highlight .-Color[class*=-C83] { + color: #5FFF5F +} + +div.highlight .-Color[class*=-BGC83] { + background-color: #5FFF5F +} + +div.highlight .-Color[class*=-C84] { + color: #5FFF87 +} + +div.highlight .-Color[class*=-BGC84] { + background-color: #5FFF87 +} + +div.highlight .-Color[class*=-C85] { + color: #5FFFAF +} + +div.highlight .-Color[class*=-BGC85] { + background-color: #5FFFAF +} + +div.highlight .-Color[class*=-C86] { + color: #5FFFD7 +} + +div.highlight .-Color[class*=-BGC86] { + background-color: #5FFFD7 +} + +div.highlight .-Color[class*=-C87] { + color: #5FFFFF +} + +div.highlight .-Color[class*=-BGC87] { + background-color: #5FFFFF +} + +div.highlight .-Color[class*=-C88] { + color: #870000 +} + +div.highlight .-Color[class*=-BGC88] { + background-color: #870000 +} + +div.highlight .-Color[class*=-C89] { + color: #87005F +} + +div.highlight .-Color[class*=-BGC89] { + background-color: #87005F +} + +div.highlight .-Color[class*=-C90] { + color: #870087 +} + +div.highlight .-Color[class*=-BGC90] { + background-color: #870087 +} + +div.highlight .-Color[class*=-C91] { + color: #8700AF +} + +div.highlight .-Color[class*=-BGC91] { + background-color: #8700AF +} + +div.highlight .-Color[class*=-C92] { + color: #8700D7 +} + +div.highlight .-Color[class*=-BGC92] { + background-color: #8700D7 +} + +div.highlight .-Color[class*=-C93] { + color: #8700FF +} + +div.highlight .-Color[class*=-BGC93] { + background-color: #8700FF +} + +div.highlight .-Color[class*=-C94] { + color: #875F00 +} + +div.highlight .-Color[class*=-BGC94] { + background-color: #875F00 +} + +div.highlight .-Color[class*=-C95] { + color: #875F5F +} + +div.highlight .-Color[class*=-BGC95] { + background-color: #875F5F +} + +div.highlight .-Color[class*=-C96] { + color: #875F87 +} + +div.highlight .-Color[class*=-BGC96] { + background-color: #875F87 +} + +div.highlight .-Color[class*=-C97] { + color: #875FAF +} + +div.highlight .-Color[class*=-BGC97] { + background-color: #875FAF +} + +div.highlight .-Color[class*=-C98] { + color: #875FD7 +} + +div.highlight .-Color[class*=-BGC98] { + background-color: #875FD7 +} + +div.highlight .-Color[class*=-C99] { + color: #875FFF +} + +div.highlight .-Color[class*=-BGC99] { + background-color: #875FFF +} + +div.highlight .-Color[class*=-C100] { + color: #878700 +} + +div.highlight .-Color[class*=-BGC100] { + background-color: #878700 +} + +div.highlight .-Color[class*=-C101] { + color: #87875F +} + +div.highlight .-Color[class*=-BGC101] { + background-color: #87875F +} + +div.highlight .-Color[class*=-C102] { + color: #878787 +} + +div.highlight .-Color[class*=-BGC102] { + background-color: #878787 +} + +div.highlight .-Color[class*=-C103] { + color: #8787AF +} + +div.highlight .-Color[class*=-BGC103] { + background-color: #8787AF +} + +div.highlight .-Color[class*=-C104] { + color: #8787D7 +} + +div.highlight .-Color[class*=-BGC104] { + background-color: #8787D7 +} + +div.highlight .-Color[class*=-C105] { + color: #8787FF +} + +div.highlight .-Color[class*=-BGC105] { + background-color: #8787FF +} + +div.highlight .-Color[class*=-C106] { + color: #87AF00 +} + +div.highlight .-Color[class*=-BGC106] { + background-color: #87AF00 +} + +div.highlight .-Color[class*=-C107] { + color: #87AF5F +} + +div.highlight .-Color[class*=-BGC107] { + background-color: #87AF5F +} + +div.highlight .-Color[class*=-C108] { + color: #87AF87 +} + +div.highlight .-Color[class*=-BGC108] { + background-color: #87AF87 +} + +div.highlight .-Color[class*=-C109] { + color: #87AFAF +} + +div.highlight .-Color[class*=-BGC109] { + background-color: #87AFAF +} + +div.highlight .-Color[class*=-C110] { + color: #87AFD7 +} + +div.highlight .-Color[class*=-BGC110] { + background-color: #87AFD7 +} + +div.highlight .-Color[class*=-C111] { + color: #87AFFF +} + +div.highlight .-Color[class*=-BGC111] { + background-color: #87AFFF +} + +div.highlight .-Color[class*=-C112] { + color: #87D700 +} + +div.highlight .-Color[class*=-BGC112] { + background-color: #87D700 +} + +div.highlight .-Color[class*=-C113] { + color: #87D75F +} + +div.highlight .-Color[class*=-BGC113] { + background-color: #87D75F +} + +div.highlight .-Color[class*=-C114] { + color: #87D787 +} + +div.highlight .-Color[class*=-BGC114] { + background-color: #87D787 +} + +div.highlight .-Color[class*=-C115] { + color: #87D7AF +} + +div.highlight .-Color[class*=-BGC115] { + background-color: #87D7AF +} + +div.highlight .-Color[class*=-C116] { + color: #87D7D7 +} + +div.highlight .-Color[class*=-BGC116] { + background-color: #87D7D7 +} + +div.highlight .-Color[class*=-C117] { + color: #87D7FF +} + +div.highlight .-Color[class*=-BGC117] { + background-color: #87D7FF +} + +div.highlight .-Color[class*=-C118] { + color: #87FF00 +} + +div.highlight .-Color[class*=-BGC118] { + background-color: #87FF00 +} + +div.highlight .-Color[class*=-C119] { + color: #87FF5F +} + +div.highlight .-Color[class*=-BGC119] { + background-color: #87FF5F +} + +div.highlight .-Color[class*=-C120] { + color: #87FF87 +} + +div.highlight .-Color[class*=-BGC120] { + background-color: #87FF87 +} + +div.highlight .-Color[class*=-C121] { + color: #87FFAF +} + +div.highlight .-Color[class*=-BGC121] { + background-color: #87FFAF +} + +div.highlight .-Color[class*=-C122] { + color: #87FFD7 +} + +div.highlight .-Color[class*=-BGC122] { + background-color: #87FFD7 +} + +div.highlight .-Color[class*=-C123] { + color: #87FFFF +} + +div.highlight .-Color[class*=-BGC123] { + background-color: #87FFFF +} + +div.highlight .-Color[class*=-C124] { + color: #AF0000 +} + +div.highlight .-Color[class*=-BGC124] { + background-color: #AF0000 +} + +div.highlight .-Color[class*=-C125] { + color: #AF005F +} + +div.highlight .-Color[class*=-BGC125] { + background-color: #AF005F +} + +div.highlight .-Color[class*=-C126] { + color: #AF0087 +} + +div.highlight .-Color[class*=-BGC126] { + background-color: #AF0087 +} + +div.highlight .-Color[class*=-C127] { + color: #AF00AF +} + +div.highlight .-Color[class*=-BGC127] { + background-color: #AF00AF +} + +div.highlight .-Color[class*=-C128] { + color: #AF00D7 +} + +div.highlight .-Color[class*=-BGC128] { + background-color: #AF00D7 +} + +div.highlight .-Color[class*=-C129] { + color: #AF00FF +} + +div.highlight .-Color[class*=-BGC129] { + background-color: #AF00FF +} + +div.highlight .-Color[class*=-C130] { + color: #AF5F00 +} + +div.highlight .-Color[class*=-BGC130] { + background-color: #AF5F00 +} + +div.highlight .-Color[class*=-C131] { + color: #AF5F5F +} + +div.highlight .-Color[class*=-BGC131] { + background-color: #AF5F5F +} + +div.highlight .-Color[class*=-C132] { + color: #AF5F87 +} + +div.highlight .-Color[class*=-BGC132] { + background-color: #AF5F87 +} + +div.highlight .-Color[class*=-C133] { + color: #AF5FAF +} + +div.highlight .-Color[class*=-BGC133] { + background-color: #AF5FAF +} + +div.highlight .-Color[class*=-C134] { + color: #AF5FD7 +} + +div.highlight .-Color[class*=-BGC134] { + background-color: #AF5FD7 +} + +div.highlight .-Color[class*=-C135] { + color: #AF5FFF +} + +div.highlight .-Color[class*=-BGC135] { + background-color: #AF5FFF +} + +div.highlight .-Color[class*=-C136] { + color: #AF8700 +} + +div.highlight .-Color[class*=-BGC136] { + background-color: #AF8700 +} + +div.highlight .-Color[class*=-C137] { + color: #AF875F +} + +div.highlight .-Color[class*=-BGC137] { + background-color: #AF875F +} + +div.highlight .-Color[class*=-C138] { + color: #AF8787 +} + +div.highlight .-Color[class*=-BGC138] { + background-color: #AF8787 +} + +div.highlight .-Color[class*=-C139] { + color: #AF87AF +} + +div.highlight .-Color[class*=-BGC139] { + background-color: #AF87AF +} + +div.highlight .-Color[class*=-C140] { + color: #AF87D7 +} + +div.highlight .-Color[class*=-BGC140] { + background-color: #AF87D7 +} + +div.highlight .-Color[class*=-C141] { + color: #AF87FF +} + +div.highlight .-Color[class*=-BGC141] { + background-color: #AF87FF +} + +div.highlight .-Color[class*=-C142] { + color: #AFAF00 +} + +div.highlight .-Color[class*=-BGC142] { + background-color: #AFAF00 +} + +div.highlight .-Color[class*=-C143] { + color: #AFAF5F +} + +div.highlight .-Color[class*=-BGC143] { + background-color: #AFAF5F +} + +div.highlight .-Color[class*=-C144] { + color: #AFAF87 +} + +div.highlight .-Color[class*=-BGC144] { + background-color: #AFAF87 +} + +div.highlight .-Color[class*=-C145] { + color: #AFAFAF +} + +div.highlight .-Color[class*=-BGC145] { + background-color: #AFAFAF +} + +div.highlight .-Color[class*=-C146] { + color: #AFAFD7 +} + +div.highlight .-Color[class*=-BGC146] { + background-color: #AFAFD7 +} + +div.highlight .-Color[class*=-C147] { + color: #AFAFFF +} + +div.highlight .-Color[class*=-BGC147] { + background-color: #AFAFFF +} + +div.highlight .-Color[class*=-C148] { + color: #AFD700 +} + +div.highlight .-Color[class*=-BGC148] { + background-color: #AFD700 +} + +div.highlight .-Color[class*=-C149] { + color: #AFD75F +} + +div.highlight .-Color[class*=-BGC149] { + background-color: #AFD75F +} + +div.highlight .-Color[class*=-C150] { + color: #AFD787 +} + +div.highlight .-Color[class*=-BGC150] { + background-color: #AFD787 +} + +div.highlight .-Color[class*=-C151] { + color: #AFD7AF +} + +div.highlight .-Color[class*=-BGC151] { + background-color: #AFD7AF +} + +div.highlight .-Color[class*=-C152] { + color: #AFD7D7 +} + +div.highlight .-Color[class*=-BGC152] { + background-color: #AFD7D7 +} + +div.highlight .-Color[class*=-C153] { + color: #AFD7FF +} + +div.highlight .-Color[class*=-BGC153] { + background-color: #AFD7FF +} + +div.highlight .-Color[class*=-C154] { + color: #AFFF00 +} + +div.highlight .-Color[class*=-BGC154] { + background-color: #AFFF00 +} + +div.highlight .-Color[class*=-C155] { + color: #AFFF5F +} + +div.highlight .-Color[class*=-BGC155] { + background-color: #AFFF5F +} + +div.highlight .-Color[class*=-C156] { + color: #AFFF87 +} + +div.highlight .-Color[class*=-BGC156] { + background-color: #AFFF87 +} + +div.highlight .-Color[class*=-C157] { + color: #AFFFAF +} + +div.highlight .-Color[class*=-BGC157] { + background-color: #AFFFAF +} + +div.highlight .-Color[class*=-C158] { + color: #AFFFD7 +} + +div.highlight .-Color[class*=-BGC158] { + background-color: #AFFFD7 +} + +div.highlight .-Color[class*=-C159] { + color: #AFFFFF +} + +div.highlight .-Color[class*=-BGC159] { + background-color: #AFFFFF +} + +div.highlight .-Color[class*=-C160] { + color: #D70000 +} + +div.highlight .-Color[class*=-BGC160] { + background-color: #D70000 +} + +div.highlight .-Color[class*=-C161] { + color: #D7005F +} + +div.highlight .-Color[class*=-BGC161] { + background-color: #D7005F +} + +div.highlight .-Color[class*=-C162] { + color: #D70087 +} + +div.highlight .-Color[class*=-BGC162] { + background-color: #D70087 +} + +div.highlight .-Color[class*=-C163] { + color: #D700AF +} + +div.highlight .-Color[class*=-BGC163] { + background-color: #D700AF +} + +div.highlight .-Color[class*=-C164] { + color: #D700D7 +} + +div.highlight .-Color[class*=-BGC164] { + background-color: #D700D7 +} + +div.highlight .-Color[class*=-C165] { + color: #D700FF +} + +div.highlight .-Color[class*=-BGC165] { + background-color: #D700FF +} + +div.highlight .-Color[class*=-C166] { + color: #D75F00 +} + +div.highlight .-Color[class*=-BGC166] { + background-color: #D75F00 +} + +div.highlight .-Color[class*=-C167] { + color: #D75F5F +} + +div.highlight .-Color[class*=-BGC167] { + background-color: #D75F5F +} + +div.highlight .-Color[class*=-C168] { + color: #D75F87 +} + +div.highlight .-Color[class*=-BGC168] { + background-color: #D75F87 +} + +div.highlight .-Color[class*=-C169] { + color: #D75FAF +} + +div.highlight .-Color[class*=-BGC169] { + background-color: #D75FAF +} + +div.highlight .-Color[class*=-C170] { + color: #D75FD7 +} + +div.highlight .-Color[class*=-BGC170] { + background-color: #D75FD7 +} + +div.highlight .-Color[class*=-C171] { + color: #D75FFF +} + +div.highlight .-Color[class*=-BGC171] { + background-color: #D75FFF +} + +div.highlight .-Color[class*=-C172] { + color: #D78700 +} + +div.highlight .-Color[class*=-BGC172] { + background-color: #D78700 +} + +div.highlight .-Color[class*=-C173] { + color: #D7875F +} + +div.highlight .-Color[class*=-BGC173] { + background-color: #D7875F +} + +div.highlight .-Color[class*=-C174] { + color: #D78787 +} + +div.highlight .-Color[class*=-BGC174] { + background-color: #D78787 +} + +div.highlight .-Color[class*=-C175] { + color: #D787AF +} + +div.highlight .-Color[class*=-BGC175] { + background-color: #D787AF +} + +div.highlight .-Color[class*=-C176] { + color: #D787D7 +} + +div.highlight .-Color[class*=-BGC176] { + background-color: #D787D7 +} + +div.highlight .-Color[class*=-C177] { + color: #D787FF +} + +div.highlight .-Color[class*=-BGC177] { + background-color: #D787FF +} + +div.highlight .-Color[class*=-C178] { + color: #D7AF00 +} + +div.highlight .-Color[class*=-BGC178] { + background-color: #D7AF00 +} + +div.highlight .-Color[class*=-C179] { + color: #D7AF5F +} + +div.highlight .-Color[class*=-BGC179] { + background-color: #D7AF5F +} + +div.highlight .-Color[class*=-C180] { + color: #D7AF87 +} + +div.highlight .-Color[class*=-BGC180] { + background-color: #D7AF87 +} + +div.highlight .-Color[class*=-C181] { + color: #D7AFAF +} + +div.highlight .-Color[class*=-BGC181] { + background-color: #D7AFAF +} + +div.highlight .-Color[class*=-C182] { + color: #D7AFD7 +} + +div.highlight .-Color[class*=-BGC182] { + background-color: #D7AFD7 +} + +div.highlight .-Color[class*=-C183] { + color: #D7AFFF +} + +div.highlight .-Color[class*=-BGC183] { + background-color: #D7AFFF +} + +div.highlight .-Color[class*=-C184] { + color: #D7D700 +} + +div.highlight .-Color[class*=-BGC184] { + background-color: #D7D700 +} + +div.highlight .-Color[class*=-C185] { + color: #D7D75F +} + +div.highlight .-Color[class*=-BGC185] { + background-color: #D7D75F +} + +div.highlight .-Color[class*=-C186] { + color: #D7D787 +} + +div.highlight .-Color[class*=-BGC186] { + background-color: #D7D787 +} + +div.highlight .-Color[class*=-C187] { + color: #D7D7AF +} + +div.highlight .-Color[class*=-BGC187] { + background-color: #D7D7AF +} + +div.highlight .-Color[class*=-C188] { + color: #D7D7D7 +} + +div.highlight .-Color[class*=-BGC188] { + background-color: #D7D7D7 +} + +div.highlight .-Color[class*=-C189] { + color: #D7D7FF +} + +div.highlight .-Color[class*=-BGC189] { + background-color: #D7D7FF +} + +div.highlight .-Color[class*=-C190] { + color: #D7FF00 +} + +div.highlight .-Color[class*=-BGC190] { + background-color: #D7FF00 +} + +div.highlight .-Color[class*=-C191] { + color: #D7FF5F +} + +div.highlight .-Color[class*=-BGC191] { + background-color: #D7FF5F +} + +div.highlight .-Color[class*=-C192] { + color: #D7FF87 +} + +div.highlight .-Color[class*=-BGC192] { + background-color: #D7FF87 +} + +div.highlight .-Color[class*=-C193] { + color: #D7FFAF +} + +div.highlight .-Color[class*=-BGC193] { + background-color: #D7FFAF +} + +div.highlight .-Color[class*=-C194] { + color: #D7FFD7 +} + +div.highlight .-Color[class*=-BGC194] { + background-color: #D7FFD7 +} + +div.highlight .-Color[class*=-C195] { + color: #D7FFFF +} + +div.highlight .-Color[class*=-BGC195] { + background-color: #D7FFFF +} + +div.highlight .-Color[class*=-C196] { + color: #FF0000 +} + +div.highlight .-Color[class*=-BGC196] { + background-color: #FF0000 +} + +div.highlight .-Color[class*=-C197] { + color: #FF005F +} + +div.highlight .-Color[class*=-BGC197] { + background-color: #FF005F +} + +div.highlight .-Color[class*=-C198] { + color: #FF0087 +} + +div.highlight .-Color[class*=-BGC198] { + background-color: #FF0087 +} + +div.highlight .-Color[class*=-C199] { + color: #FF00AF +} + +div.highlight .-Color[class*=-BGC199] { + background-color: #FF00AF +} + +div.highlight .-Color[class*=-C200] { + color: #FF00D7 +} + +div.highlight .-Color[class*=-BGC200] { + background-color: #FF00D7 +} + +div.highlight .-Color[class*=-C201] { + color: #FF00FF +} + +div.highlight .-Color[class*=-BGC201] { + background-color: #FF00FF +} + +div.highlight .-Color[class*=-C202] { + color: #FF5F00 +} + +div.highlight .-Color[class*=-BGC202] { + background-color: #FF5F00 +} + +div.highlight .-Color[class*=-C203] { + color: #FF5F5F +} + +div.highlight .-Color[class*=-BGC203] { + background-color: #FF5F5F +} + +div.highlight .-Color[class*=-C204] { + color: #FF5F87 +} + +div.highlight .-Color[class*=-BGC204] { + background-color: #FF5F87 +} + +div.highlight .-Color[class*=-C205] { + color: #FF5FAF +} + +div.highlight .-Color[class*=-BGC205] { + background-color: #FF5FAF +} + +div.highlight .-Color[class*=-C206] { + color: #FF5FD7 +} + +div.highlight .-Color[class*=-BGC206] { + background-color: #FF5FD7 +} + +div.highlight .-Color[class*=-C207] { + color: #FF5FFF +} + +div.highlight .-Color[class*=-BGC207] { + background-color: #FF5FFF +} + +div.highlight .-Color[class*=-C208] { + color: #FF8700 +} + +div.highlight .-Color[class*=-BGC208] { + background-color: #FF8700 +} + +div.highlight .-Color[class*=-C209] { + color: #FF875F +} + +div.highlight .-Color[class*=-BGC209] { + background-color: #FF875F +} + +div.highlight .-Color[class*=-C210] { + color: #FF8787 +} + +div.highlight .-Color[class*=-BGC210] { + background-color: #FF8787 +} + +div.highlight .-Color[class*=-C211] { + color: #FF87AF +} + +div.highlight .-Color[class*=-BGC211] { + background-color: #FF87AF +} + +div.highlight .-Color[class*=-C212] { + color: #FF87D7 +} + +div.highlight .-Color[class*=-BGC212] { + background-color: #FF87D7 +} + +div.highlight .-Color[class*=-C213] { + color: #FF87FF +} + +div.highlight .-Color[class*=-BGC213] { + background-color: #FF87FF +} + +div.highlight .-Color[class*=-C214] { + color: #FFAF00 +} + +div.highlight .-Color[class*=-BGC214] { + background-color: #FFAF00 +} + +div.highlight .-Color[class*=-C215] { + color: #FFAF5F +} + +div.highlight .-Color[class*=-BGC215] { + background-color: #FFAF5F +} + +div.highlight .-Color[class*=-C216] { + color: #FFAF87 +} + +div.highlight .-Color[class*=-BGC216] { + background-color: #FFAF87 +} + +div.highlight .-Color[class*=-C217] { + color: #FFAFAF +} + +div.highlight .-Color[class*=-BGC217] { + background-color: #FFAFAF +} + +div.highlight .-Color[class*=-C218] { + color: #FFAFD7 +} + +div.highlight .-Color[class*=-BGC218] { + background-color: #FFAFD7 +} + +div.highlight .-Color[class*=-C219] { + color: #FFAFFF +} + +div.highlight .-Color[class*=-BGC219] { + background-color: #FFAFFF +} + +div.highlight .-Color[class*=-C220] { + color: #FFD700 +} + +div.highlight .-Color[class*=-BGC220] { + background-color: #FFD700 +} + +div.highlight .-Color[class*=-C221] { + color: #FFD75F +} + +div.highlight .-Color[class*=-BGC221] { + background-color: #FFD75F +} + +div.highlight .-Color[class*=-C222] { + color: #FFD787 +} + +div.highlight .-Color[class*=-BGC222] { + background-color: #FFD787 +} + +div.highlight .-Color[class*=-C223] { + color: #FFD7AF +} + +div.highlight .-Color[class*=-BGC223] { + background-color: #FFD7AF +} + +div.highlight .-Color[class*=-C224] { + color: #FFD7D7 +} + +div.highlight .-Color[class*=-BGC224] { + background-color: #FFD7D7 +} + +div.highlight .-Color[class*=-C225] { + color: #FFD7FF +} + +div.highlight .-Color[class*=-BGC225] { + background-color: #FFD7FF +} + +div.highlight .-Color[class*=-C226] { + color: #FFFF00 +} + +div.highlight .-Color[class*=-BGC226] { + background-color: #FFFF00 +} + +div.highlight .-Color[class*=-C227] { + color: #FFFF5F +} + +div.highlight .-Color[class*=-BGC227] { + background-color: #FFFF5F +} + +div.highlight .-Color[class*=-C228] { + color: #FFFF87 +} + +div.highlight .-Color[class*=-BGC228] { + background-color: #FFFF87 +} + +div.highlight .-Color[class*=-C229] { + color: #FFFFAF +} + +div.highlight .-Color[class*=-BGC229] { + background-color: #FFFFAF +} + +div.highlight .-Color[class*=-C230] { + color: #FFFFD7 +} + +div.highlight .-Color[class*=-BGC230] { + background-color: #FFFFD7 +} + +div.highlight .-Color[class*=-C231] { + color: #FFFFFF +} + +div.highlight .-Color[class*=-BGC231] { + background-color: #FFFFFF +} + +div.highlight .-Color[class*=-C232] { + color: #080808 +} + +div.highlight .-Color[class*=-BGC232] { + background-color: #080808 +} + +div.highlight .-Color[class*=-C233] { + color: #121212 +} + +div.highlight .-Color[class*=-BGC233] { + background-color: #121212 +} + +div.highlight .-Color[class*=-C234] { + color: #1C1C1C +} + +div.highlight .-Color[class*=-BGC234] { + background-color: #1C1C1C +} + +div.highlight .-Color[class*=-C235] { + color: #262626 +} + +div.highlight .-Color[class*=-BGC235] { + background-color: #262626 +} + +div.highlight .-Color[class*=-C236] { + color: #303030 +} + +div.highlight .-Color[class*=-BGC236] { + background-color: #303030 +} + +div.highlight .-Color[class*=-C237] { + color: #3A3A3A +} + +div.highlight .-Color[class*=-BGC237] { + background-color: #3A3A3A +} + +div.highlight .-Color[class*=-C238] { + color: #444444 +} + +div.highlight .-Color[class*=-BGC238] { + background-color: #444444 +} + +div.highlight .-Color[class*=-C239] { + color: #4E4E4E +} + +div.highlight .-Color[class*=-BGC239] { + background-color: #4E4E4E +} + +div.highlight .-Color[class*=-C240] { + color: #585858 +} + +div.highlight .-Color[class*=-BGC240] { + background-color: #585858 +} + +div.highlight .-Color[class*=-C241] { + color: #626262 +} + +div.highlight .-Color[class*=-BGC241] { + background-color: #626262 +} + +div.highlight .-Color[class*=-C242] { + color: #6C6C6C +} + +div.highlight .-Color[class*=-BGC242] { + background-color: #6C6C6C +} + +div.highlight .-Color[class*=-C243] { + color: #767676 +} + +div.highlight .-Color[class*=-BGC243] { + background-color: #767676 +} + +div.highlight .-Color[class*=-C244] { + color: #808080 +} + +div.highlight .-Color[class*=-BGC244] { + background-color: #808080 +} + +div.highlight .-Color[class*=-C245] { + color: #8A8A8A +} + +div.highlight .-Color[class*=-BGC245] { + background-color: #8A8A8A +} + +div.highlight .-Color[class*=-C246] { + color: #949494 +} + +div.highlight .-Color[class*=-BGC246] { + background-color: #949494 +} + +div.highlight .-Color[class*=-C247] { + color: #9E9E9E +} + +div.highlight .-Color[class*=-BGC247] { + background-color: #9E9E9E +} + +div.highlight .-Color[class*=-C248] { + color: #A8A8A8 +} + +div.highlight .-Color[class*=-BGC248] { + background-color: #A8A8A8 +} + +div.highlight .-Color[class*=-C249] { + color: #B2B2B2 +} + +div.highlight .-Color[class*=-BGC249] { + background-color: #B2B2B2 +} + +div.highlight .-Color[class*=-C250] { + color: #BCBCBC +} + +div.highlight .-Color[class*=-BGC250] { + background-color: #BCBCBC +} + +div.highlight .-Color[class*=-C251] { + color: #C6C6C6 +} + +div.highlight .-Color[class*=-BGC251] { + background-color: #C6C6C6 +} + +div.highlight .-Color[class*=-C252] { + color: #D0D0D0 +} + +div.highlight .-Color[class*=-BGC252] { + background-color: #D0D0D0 +} + +div.highlight .-Color[class*=-C253] { + color: #DADADA +} + +div.highlight .-Color[class*=-BGC253] { + background-color: #DADADA +} + +div.highlight .-Color[class*=-C254] { + color: #E4E4E4 +} + +div.highlight .-Color[class*=-BGC254] { + background-color: #E4E4E4 +} + +div.highlight .-Color[class*=-C255] { + color: #EEEEEE +} + +div.highlight .-Color[class*=-BGC255] { + background-color: #EEEEEE +} diff --git a/tools/docs/_build/dirhtml/_static/plus.png b/tools/docs/_build/dirhtml/_static/plus.png new file mode 100644 index 0000000..7107cec Binary files /dev/null and b/tools/docs/_build/dirhtml/_static/plus.png differ diff --git a/tools/docs/_build/dirhtml/_static/print.css b/tools/docs/_build/dirhtml/_static/print.css new file mode 100644 index 0000000..bd88769 --- /dev/null +++ b/tools/docs/_build/dirhtml/_static/print.css @@ -0,0 +1,42 @@ +.sy-head { + position: static; + border-bottom: 1px solid var(--sy-c-divider); +} + +.sy-head-inner { + padding: 0; +} + +.sy-breadcrumbs { + display: none; +} + +h1, h2, h3, h4, h5, h6 { + page-break-inside: avoid; + page-break-after: avoid; +} + +.code-block-caption, +pre, code { + page-break-inside: avoid; + white-space: pre-wrap; + + -webkit-print-color-adjust: exact; +} + +.yue a.headerlink { + display: none; +} + +.highlight .linenos { + box-shadow: none; +} + +.admonition, +.sd-sphinx-override { + -webkit-print-color-adjust: exact; +} + +.sd-card { + page-break-inside: avoid; +} diff --git a/tools/docs/_build/dirhtml/_static/pygments.css b/tools/docs/_build/dirhtml/_static/pygments.css new file mode 100644 index 0000000..352b22c --- /dev/null +++ b/tools/docs/_build/dirhtml/_static/pygments.css @@ -0,0 +1,278 @@ +:root { +--syntax-light-background: #ffffff; +--syntax-light-highlight: #4ac26b40; +--syntax-light-text: #1F2328; +--syntax-light-escape: #0A3069; +--syntax-light-error: #82071E; +--syntax-light-keyword: #CF222E; +--syntax-light-keyword-constant: #0550AE; +--syntax-light-keyword-type: #953800; +--syntax-light-name-attribute: #0550AE; +--syntax-light-name-builtin: #0550AE; +--syntax-light-name-class: #953800; +--syntax-light-name-constant: #0550AE; +--syntax-light-name-decorator: #0550AE; +--syntax-light-name-entity: #0550AE; +--syntax-light-name-exception: #0550AE; +--syntax-light-name-function: #8250DF; +--syntax-light-name-function-magic: #0550AE; +--syntax-light-name-property: #0550AE; +--syntax-light-name-label: #0A3069; +--syntax-light-name-tag: #116329; +--syntax-light-name-variable-class: #953800; +--syntax-light-literal: #0A3069; +--syntax-light-literal-string: #0A3069; +--syntax-light-literal-string-affix: #CF222E; +--syntax-light-literal-string-backtick: #0550AE; +--syntax-light-literal-string-interpol: #CF222E; +--syntax-light-literal-string-regex: #0A3069; +--syntax-light-literal-string-symbol: #0550AE; +--syntax-light-literal-number: #0550AE; +--syntax-light-operator: #CF222E; +--syntax-light-operator-word: #0550AE; +--syntax-light-comment: #6E7781; +--syntax-light-comment-preproc: #CF222E; +--syntax-light-comment-preprocfile: #0A3069; +--syntax-light-generic-deleted: #82071E; +--syntax-light-generic-emph: #1F2328; +--syntax-light-generic-error: #82071E; +--syntax-light-generic-heading: #0550AE; +--syntax-light-generic-inserted: #116329; +--syntax-light-generic-output: #0A3069; +--syntax-light-generic-prompt: #CF222E; +--syntax-light-generic-strong: #1F2328; +--syntax-light-generic-subheading: #0550AE; +--syntax-light-generic-emphstrong: #1F2328; +--syntax-light-generic-traceback: #82071E; +--syntax-light-text-whitespace: #1F2328; +--syntax-light-keyword-declaration: #CF222E; +--syntax-light-keyword-namespace: #CF222E; +--syntax-light-keyword-pseudo: #CF222E; +--syntax-light-keyword-reserved: #CF222E; +--syntax-light-name-builtin-pseudo: #0550AE; +--syntax-light-literal-date: #0A3069; +--syntax-light-literal-string-char: #0A3069; +--syntax-light-literal-string-delimiter: #0A3069; +--syntax-light-literal-string-doc: #0A3069; +--syntax-light-literal-string-double: #0A3069; +--syntax-light-literal-string-escape: #0A3069; +--syntax-light-literal-string-heredoc: #0A3069; +--syntax-light-literal-string-other: #0A3069; +--syntax-light-literal-string-single: #0A3069; +--syntax-light-literal-number-bin: #0550AE; +--syntax-light-literal-number-float: #0550AE; +--syntax-light-literal-number-hex: #0550AE; +--syntax-light-literal-number-integer: #0550AE; +--syntax-light-literal-number-integer-long: #0550AE; +--syntax-light-literal-number-oct: #0550AE; +--syntax-light-comment-hashbang: #6E7781; +--syntax-light-comment-multiline: #6E7781; +--syntax-light-comment-single: #6E7781; +--syntax-light-comment-special: #6E7781; +} +html.light .highlight .c { color: var(--syntax-light-comment) } +html.light .highlight .err { color: var(--syntax-light-error) } +html.light .highlight .esc { color: var(--syntax-light-escape) } +html.light .highlight .k { color: var(--syntax-light-keyword) } +html.light .highlight .l { color: var(--syntax-light-literal) } +html.light .highlight .o { color: var(--syntax-light-operator) } +html.light .highlight .ch { color: var(--syntax-light-comment-hashbang) } +html.light .highlight .cm { color: var(--syntax-light-comment-multiline) } +html.light .highlight .cp { color: var(--syntax-light-comment-preproc) } +html.light .highlight .cpf { color: var(--syntax-light-comment-preprocfile) } +html.light .highlight .c1 { color: var(--syntax-light-comment-single) } +html.light .highlight .cs { color: var(--syntax-light-comment-special) } +html.light .highlight .gd { color: var(--syntax-light-generic-deleted) } +html.light .highlight .ge { color: var(--syntax-light-generic-emph); font-style: italic } +html.light .highlight .ges { color: var(--syntax-light-generic-emphstrong); font-weight: bold; font-style: italic } +html.light .highlight .gr { color: var(--syntax-light-generic-error) } +html.light .highlight .gh { color: var(--syntax-light-generic-heading); font-weight: bold } +html.light .highlight .gi { color: var(--syntax-light-generic-inserted) } +html.light .highlight .go { color: var(--syntax-light-generic-output) } +html.light .highlight .gp { color: var(--syntax-light-generic-prompt) } +html.light .highlight .gs { color: var(--syntax-light-generic-strong); font-weight: bold } +html.light .highlight .gu { color: var(--syntax-light-generic-subheading) } +html.light .highlight .gt { color: var(--syntax-light-generic-traceback) } +html.light .highlight .kc { color: var(--syntax-light-keyword-constant) } +html.light .highlight .kd { color: var(--syntax-light-keyword-declaration) } +html.light .highlight .kn { color: var(--syntax-light-keyword-namespace) } +html.light .highlight .kp { color: var(--syntax-light-keyword-pseudo) } +html.light .highlight .kr { color: var(--syntax-light-keyword-reserved) } +html.light .highlight .kt { color: var(--syntax-light-keyword-type) } +html.light .highlight .ld { color: var(--syntax-light-literal-date) } +html.light .highlight .m { color: var(--syntax-light-literal-number) } +html.light .highlight .s { color: var(--syntax-light-literal-string) } +html.light .highlight .na { color: var(--syntax-light-name-attribute) } +html.light .highlight .nb { color: var(--syntax-light-name-builtin) } +html.light .highlight .nc { color: var(--syntax-light-name-class) } +html.light .highlight .no { color: var(--syntax-light-name-constant) } +html.light .highlight .nd { color: var(--syntax-light-name-decorator) } +html.light .highlight .ni { color: var(--syntax-light-name-entity) } +html.light .highlight .ne { color: var(--syntax-light-name-exception) } +html.light .highlight .nf { color: var(--syntax-light-name-function) } +html.light .highlight .nl { color: var(--syntax-light-name-label) } +html.light .highlight .py { color: var(--syntax-light-name-property) } +html.light .highlight .nt { color: var(--syntax-light-name-tag) } +html.light .highlight .ow { color: var(--syntax-light-operator-word) } +html.light .highlight .w { color: var(--syntax-light-text-whitespace) } +html.light .highlight .mb { color: var(--syntax-light-literal-number-bin) } +html.light .highlight .mf { color: var(--syntax-light-literal-number-float) } +html.light .highlight .mh { color: var(--syntax-light-literal-number-hex) } +html.light .highlight .mi { color: var(--syntax-light-literal-number-integer) } +html.light .highlight .mo { color: var(--syntax-light-literal-number-oct) } +html.light .highlight .sa { color: var(--syntax-light-literal-string-affix) } +html.light .highlight .sb { color: var(--syntax-light-literal-string-backtick) } +html.light .highlight .sc { color: var(--syntax-light-literal-string-char) } +html.light .highlight .dl { color: var(--syntax-light-literal-string-delimiter) } +html.light .highlight .sd { color: var(--syntax-light-literal-string-doc) } +html.light .highlight .s2 { color: var(--syntax-light-literal-string-double) } +html.light .highlight .se { color: var(--syntax-light-literal-string-escape) } +html.light .highlight .sh { color: var(--syntax-light-literal-string-heredoc) } +html.light .highlight .si { color: var(--syntax-light-literal-string-interpol) } +html.light .highlight .sx { color: var(--syntax-light-literal-string-other) } +html.light .highlight .sr { color: var(--syntax-light-literal-string-regex) } +html.light .highlight .s1 { color: var(--syntax-light-literal-string-single) } +html.light .highlight .ss { color: var(--syntax-light-literal-string-symbol) } +html.light .highlight .bp { color: var(--syntax-light-name-builtin-pseudo) } +html.light .highlight .fm { color: var(--syntax-light-name-function-magic) } +html.light .highlight .vc { color: var(--syntax-light-name-variable-class) } +html.light .highlight .il { color: var(--syntax-light-literal-number-integer-long) } +:root { +--syntax-dark-background: #0d1117; +--syntax-dark-highlight: #3fb95040; +--syntax-dark-text: #E6EDF3; +--syntax-dark-escape: #A5D6FF; +--syntax-dark-error: #FFA198; +--syntax-dark-keyword: #FF7B72; +--syntax-dark-keyword-constant: #79C0FF; +--syntax-dark-keyword-type: #FFA657; +--syntax-dark-name-attribute: #79C0FF; +--syntax-dark-name-builtin: #79C0FF; +--syntax-dark-name-class: #FFA657; +--syntax-dark-name-constant: #79C0FF; +--syntax-dark-name-decorator: #79C0FF; +--syntax-dark-name-entity: #79C0FF; +--syntax-dark-name-exception: #79C0FF; +--syntax-dark-name-function: #D2A8FF; +--syntax-dark-name-function-magic: #79C0FF; +--syntax-dark-name-property: #79C0FF; +--syntax-dark-name-label: #A5D6FF; +--syntax-dark-name-tag: #7EE787; +--syntax-dark-name-variable-class: #FFA657; +--syntax-dark-literal: #A5D6FF; +--syntax-dark-literal-string: #A5D6FF; +--syntax-dark-literal-string-affix: #FF7B72; +--syntax-dark-literal-string-backtick: #79C0FF; +--syntax-dark-literal-string-interpol: #FF7B72; +--syntax-dark-literal-string-regex: #A5D6FF; +--syntax-dark-literal-string-symbol: #79C0FF; +--syntax-dark-literal-number: #79C0FF; +--syntax-dark-operator: #FF7B72; +--syntax-dark-operator-word: #79C0FF; +--syntax-dark-comment: #8B949E; +--syntax-dark-comment-preproc: #FF7B72; +--syntax-dark-comment-preprocfile: #A5D6FF; +--syntax-dark-generic-deleted: #FFA198; +--syntax-dark-generic-emph: #E6EDF3; +--syntax-dark-generic-error: #FFA198; +--syntax-dark-generic-heading: #79C0FF; +--syntax-dark-generic-inserted: #7EE787; +--syntax-dark-generic-output: #A5D6FF; +--syntax-dark-generic-prompt: #FF7B72; +--syntax-dark-generic-strong: #E6EDF3; +--syntax-dark-generic-subheading: #79C0FF; +--syntax-dark-generic-emphstrong: #E6EDF3; +--syntax-dark-generic-traceback: #FFA198; +--syntax-dark-text-whitespace: #E6EDF3; +--syntax-dark-keyword-declaration: #FF7B72; +--syntax-dark-keyword-namespace: #FF7B72; +--syntax-dark-keyword-pseudo: #FF7B72; +--syntax-dark-keyword-reserved: #FF7B72; +--syntax-dark-name-builtin-pseudo: #79C0FF; +--syntax-dark-literal-date: #A5D6FF; +--syntax-dark-literal-string-char: #A5D6FF; +--syntax-dark-literal-string-delimiter: #A5D6FF; +--syntax-dark-literal-string-doc: #A5D6FF; +--syntax-dark-literal-string-double: #A5D6FF; +--syntax-dark-literal-string-escape: #A5D6FF; +--syntax-dark-literal-string-heredoc: #A5D6FF; +--syntax-dark-literal-string-other: #A5D6FF; +--syntax-dark-literal-string-single: #A5D6FF; +--syntax-dark-literal-number-bin: #79C0FF; +--syntax-dark-literal-number-float: #79C0FF; +--syntax-dark-literal-number-hex: #79C0FF; +--syntax-dark-literal-number-integer: #79C0FF; +--syntax-dark-literal-number-integer-long: #79C0FF; +--syntax-dark-literal-number-oct: #79C0FF; +--syntax-dark-comment-hashbang: #8B949E; +--syntax-dark-comment-multiline: #8B949E; +--syntax-dark-comment-single: #8B949E; +--syntax-dark-comment-special: #8B949E; +} +html.dark .highlight .c, html.light .dark-code .highlight .c { color: var(--syntax-dark-comment) } +html.dark .highlight .err, html.light .dark-code .highlight .err { color: var(--syntax-dark-error) } +html.dark .highlight .esc, html.light .dark-code .highlight .esc { color: var(--syntax-dark-escape) } +html.dark .highlight .k, html.light .dark-code .highlight .k { color: var(--syntax-dark-keyword) } +html.dark .highlight .l, html.light .dark-code .highlight .l { color: var(--syntax-dark-literal) } +html.dark .highlight .o, html.light .dark-code .highlight .o { color: var(--syntax-dark-operator) } +html.dark .highlight .ch, html.light .dark-code .highlight .ch { color: var(--syntax-dark-comment-hashbang) } +html.dark .highlight .cm, html.light .dark-code .highlight .cm { color: var(--syntax-dark-comment-multiline) } +html.dark .highlight .cp, html.light .dark-code .highlight .cp { color: var(--syntax-dark-comment-preproc) } +html.dark .highlight .cpf, html.light .dark-code .highlight .cpf { color: var(--syntax-dark-comment-preprocfile) } +html.dark .highlight .c1, html.light .dark-code .highlight .c1 { color: var(--syntax-dark-comment-single) } +html.dark .highlight .cs, html.light .dark-code .highlight .cs { color: var(--syntax-dark-comment-special) } +html.dark .highlight .gd, html.light .dark-code .highlight .gd { color: var(--syntax-dark-generic-deleted) } +html.dark .highlight .ge, html.light .dark-code .highlight .ge { color: var(--syntax-dark-generic-emph); font-style: italic } +html.dark .highlight .ges, html.light .dark-code .highlight .ges { color: var(--syntax-dark-generic-emphstrong); font-weight: bold; font-style: italic } +html.dark .highlight .gr, html.light .dark-code .highlight .gr { color: var(--syntax-dark-generic-error) } +html.dark .highlight .gh, html.light .dark-code .highlight .gh { color: var(--syntax-dark-generic-heading); font-weight: bold } +html.dark .highlight .gi, html.light .dark-code .highlight .gi { color: var(--syntax-dark-generic-inserted) } +html.dark .highlight .go, html.light .dark-code .highlight .go { color: var(--syntax-dark-generic-output) } +html.dark .highlight .gp, html.light .dark-code .highlight .gp { color: var(--syntax-dark-generic-prompt) } +html.dark .highlight .gs, html.light .dark-code .highlight .gs { color: var(--syntax-dark-generic-strong); font-weight: bold } +html.dark .highlight .gu, html.light .dark-code .highlight .gu { color: var(--syntax-dark-generic-subheading) } +html.dark .highlight .gt, html.light .dark-code .highlight .gt { color: var(--syntax-dark-generic-traceback) } +html.dark .highlight .kc, html.light .dark-code .highlight .kc { color: var(--syntax-dark-keyword-constant) } +html.dark .highlight .kd, html.light .dark-code .highlight .kd { color: var(--syntax-dark-keyword-declaration) } +html.dark .highlight .kn, html.light .dark-code .highlight .kn { color: var(--syntax-dark-keyword-namespace) } +html.dark .highlight .kp, html.light .dark-code .highlight .kp { color: var(--syntax-dark-keyword-pseudo) } +html.dark .highlight .kr, html.light .dark-code .highlight .kr { color: var(--syntax-dark-keyword-reserved) } +html.dark .highlight .kt, html.light .dark-code .highlight .kt { color: var(--syntax-dark-keyword-type) } +html.dark .highlight .ld, html.light .dark-code .highlight .ld { color: var(--syntax-dark-literal-date) } +html.dark .highlight .m, html.light .dark-code .highlight .m { color: var(--syntax-dark-literal-number) } +html.dark .highlight .s, html.light .dark-code .highlight .s { color: var(--syntax-dark-literal-string) } +html.dark .highlight .na, html.light .dark-code .highlight .na { color: var(--syntax-dark-name-attribute) } +html.dark .highlight .nb, html.light .dark-code .highlight .nb { color: var(--syntax-dark-name-builtin) } +html.dark .highlight .nc, html.light .dark-code .highlight .nc { color: var(--syntax-dark-name-class) } +html.dark .highlight .no, html.light .dark-code .highlight .no { color: var(--syntax-dark-name-constant) } +html.dark .highlight .nd, html.light .dark-code .highlight .nd { color: var(--syntax-dark-name-decorator) } +html.dark .highlight .ni, html.light .dark-code .highlight .ni { color: var(--syntax-dark-name-entity) } +html.dark .highlight .ne, html.light .dark-code .highlight .ne { color: var(--syntax-dark-name-exception) } +html.dark .highlight .nf, html.light .dark-code .highlight .nf { color: var(--syntax-dark-name-function) } +html.dark .highlight .nl, html.light .dark-code .highlight .nl { color: var(--syntax-dark-name-label) } +html.dark .highlight .py, html.light .dark-code .highlight .py { color: var(--syntax-dark-name-property) } +html.dark .highlight .nt, html.light .dark-code .highlight .nt { color: var(--syntax-dark-name-tag) } +html.dark .highlight .ow, html.light .dark-code .highlight .ow { color: var(--syntax-dark-operator-word) } +html.dark .highlight .w, html.light .dark-code .highlight .w { color: var(--syntax-dark-text-whitespace) } +html.dark .highlight .mb, html.light .dark-code .highlight .mb { color: var(--syntax-dark-literal-number-bin) } +html.dark .highlight .mf, html.light .dark-code .highlight .mf { color: var(--syntax-dark-literal-number-float) } +html.dark .highlight .mh, html.light .dark-code .highlight .mh { color: var(--syntax-dark-literal-number-hex) } +html.dark .highlight .mi, html.light .dark-code .highlight .mi { color: var(--syntax-dark-literal-number-integer) } +html.dark .highlight .mo, html.light .dark-code .highlight .mo { color: var(--syntax-dark-literal-number-oct) } +html.dark .highlight .sa, html.light .dark-code .highlight .sa { color: var(--syntax-dark-literal-string-affix) } +html.dark .highlight .sb, html.light .dark-code .highlight .sb { color: var(--syntax-dark-literal-string-backtick) } +html.dark .highlight .sc, html.light .dark-code .highlight .sc { color: var(--syntax-dark-literal-string-char) } +html.dark .highlight .dl, html.light .dark-code .highlight .dl { color: var(--syntax-dark-literal-string-delimiter) } +html.dark .highlight .sd, html.light .dark-code .highlight .sd { color: var(--syntax-dark-literal-string-doc) } +html.dark .highlight .s2, html.light .dark-code .highlight .s2 { color: var(--syntax-dark-literal-string-double) } +html.dark .highlight .se, html.light .dark-code .highlight .se { color: var(--syntax-dark-literal-string-escape) } +html.dark .highlight .sh, html.light .dark-code .highlight .sh { color: var(--syntax-dark-literal-string-heredoc) } +html.dark .highlight .si, html.light .dark-code .highlight .si { color: var(--syntax-dark-literal-string-interpol) } +html.dark .highlight .sx, html.light .dark-code .highlight .sx { color: var(--syntax-dark-literal-string-other) } +html.dark .highlight .sr, html.light .dark-code .highlight .sr { color: var(--syntax-dark-literal-string-regex) } +html.dark .highlight .s1, html.light .dark-code .highlight .s1 { color: var(--syntax-dark-literal-string-single) } +html.dark .highlight .ss, html.light .dark-code .highlight .ss { color: var(--syntax-dark-literal-string-symbol) } +html.dark .highlight .bp, html.light .dark-code .highlight .bp { color: var(--syntax-dark-name-builtin-pseudo) } +html.dark .highlight .fm, html.light .dark-code .highlight .fm { color: var(--syntax-dark-name-function-magic) } +html.dark .highlight .vc, html.light .dark-code .highlight .vc { color: var(--syntax-dark-name-variable-class) } +html.dark .highlight .il, html.light .dark-code .highlight .il { color: var(--syntax-dark-literal-number-integer-long) } diff --git a/tools/docs/_build/dirhtml/_static/searchtools.js b/tools/docs/_build/dirhtml/_static/searchtools.js new file mode 100644 index 0000000..2c774d1 --- /dev/null +++ b/tools/docs/_build/dirhtml/_static/searchtools.js @@ -0,0 +1,632 @@ +/* + * Sphinx JavaScript utilities for the full-text search. + */ +"use strict"; + +/** + * Simple result scoring code. + */ +if (typeof Scorer === "undefined") { + var Scorer = { + // Implement the following function to further tweak the score for each result + // The function takes a result array [docname, title, anchor, descr, score, filename] + // and returns the new score. + /* + score: result => { + const [docname, title, anchor, descr, score, filename, kind] = result + return score + }, + */ + + // query matches the full name of an object + objNameMatch: 11, + // or matches in the last dotted part of the object name + objPartialMatch: 6, + // Additive scores depending on the priority of the object + objPrio: { + 0: 15, // used to be importantResults + 1: 5, // used to be objectResults + 2: -5, // used to be unimportantResults + }, + // Used when the priority is not in the mapping. + objPrioDefault: 0, + + // query found in title + title: 15, + partialTitle: 7, + // query found in terms + term: 5, + partialTerm: 2, + }; +} + +// Global search result kind enum, used by themes to style search results. +class SearchResultKind { + static get index() { return "index"; } + static get object() { return "object"; } + static get text() { return "text"; } + static get title() { return "title"; } +} + +const _removeChildren = (element) => { + while (element && element.lastChild) element.removeChild(element.lastChild); +}; + +/** + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping + */ +const _escapeRegExp = (string) => + string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string + +const _displayItem = (item, searchTerms, highlightTerms) => { + const docBuilder = DOCUMENTATION_OPTIONS.BUILDER; + const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX; + const docLinkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX; + const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY; + const contentRoot = document.documentElement.dataset.content_root; + + const [docName, title, anchor, descr, score, _filename, kind] = item; + + let listItem = document.createElement("li"); + // Add a class representing the item's type: + // can be used by a theme's CSS selector for styling + // See SearchResultKind for the class names. + listItem.classList.add(`kind-${kind}`); + let requestUrl; + let linkUrl; + if (docBuilder === "dirhtml") { + // dirhtml builder + let dirname = docName + "/"; + if (dirname.match(/\/index\/$/)) + dirname = dirname.substring(0, dirname.length - 6); + else if (dirname === "index/") dirname = ""; + requestUrl = contentRoot + dirname; + linkUrl = requestUrl; + } else { + // normal html builders + requestUrl = contentRoot + docName + docFileSuffix; + linkUrl = docName + docLinkSuffix; + } + let linkEl = listItem.appendChild(document.createElement("a")); + linkEl.href = linkUrl + anchor; + linkEl.dataset.score = score; + linkEl.innerHTML = title; + if (descr) { + listItem.appendChild(document.createElement("span")).innerHTML = + " (" + descr + ")"; + // highlight search terms in the description + if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js + highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); + } + else if (showSearchSummary) + fetch(requestUrl) + .then((responseData) => responseData.text()) + .then((data) => { + if (data) + listItem.appendChild( + Search.makeSearchSummary(data, searchTerms, anchor) + ); + // highlight search terms in the summary + if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js + highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); + }); + Search.output.appendChild(listItem); +}; +const _finishSearch = (resultCount) => { + Search.stopPulse(); + Search.title.innerText = _("Search Results"); + if (!resultCount) + Search.status.innerText = Documentation.gettext( + "Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories." + ); + else + Search.status.innerText = Documentation.ngettext( + "Search finished, found one page matching the search query.", + "Search finished, found ${resultCount} pages matching the search query.", + resultCount, + ).replace('${resultCount}', resultCount); +}; +const _displayNextItem = ( + results, + resultCount, + searchTerms, + highlightTerms, +) => { + // results left, load the summary and display it + // this is intended to be dynamic (don't sub resultsCount) + if (results.length) { + _displayItem(results.pop(), searchTerms, highlightTerms); + setTimeout( + () => _displayNextItem(results, resultCount, searchTerms, highlightTerms), + 5 + ); + } + // search finished, update title and status message + else _finishSearch(resultCount); +}; +// Helper function used by query() to order search results. +// Each input is an array of [docname, title, anchor, descr, score, filename, kind]. +// Order the results by score (in opposite order of appearance, since the +// `_displayNextItem` function uses pop() to retrieve items) and then alphabetically. +const _orderResultsByScoreThenName = (a, b) => { + const leftScore = a[4]; + const rightScore = b[4]; + if (leftScore === rightScore) { + // same score: sort alphabetically + const leftTitle = a[1].toLowerCase(); + const rightTitle = b[1].toLowerCase(); + if (leftTitle === rightTitle) return 0; + return leftTitle > rightTitle ? -1 : 1; // inverted is intentional + } + return leftScore > rightScore ? 1 : -1; +}; + +/** + * Default splitQuery function. Can be overridden in ``sphinx.search`` with a + * custom function per language. + * + * The regular expression works by splitting the string on consecutive characters + * that are not Unicode letters, numbers, underscores, or emoji characters. + * This is the same as ``\W+`` in Python, preserving the surrogate pair area. + */ +if (typeof splitQuery === "undefined") { + var splitQuery = (query) => query + .split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu) + .filter(term => term) // remove remaining empty strings +} + +/** + * Search Module + */ +const Search = { + _index: null, + _queued_query: null, + _pulse_status: -1, + + htmlToText: (htmlString, anchor) => { + const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html'); + for (const removalQuery of [".headerlink", "script", "style"]) { + htmlElement.querySelectorAll(removalQuery).forEach((el) => { el.remove() }); + } + if (anchor) { + const anchorContent = htmlElement.querySelector(`[role="main"] ${anchor}`); + if (anchorContent) return anchorContent.textContent; + + console.warn( + `Anchored content block not found. Sphinx search tries to obtain it via DOM query '[role=main] ${anchor}'. Check your theme or template.` + ); + } + + // if anchor not specified or not found, fall back to main content + const docContent = htmlElement.querySelector('[role="main"]'); + if (docContent) return docContent.textContent; + + console.warn( + "Content block not found. Sphinx search tries to obtain it via DOM query '[role=main]'. Check your theme or template." + ); + return ""; + }, + + init: () => { + const query = new URLSearchParams(window.location.search).get("q"); + document + .querySelectorAll('input[name="q"]') + .forEach((el) => (el.value = query)); + if (query) Search.performSearch(query); + }, + + loadIndex: (url) => + (document.body.appendChild(document.createElement("script")).src = url), + + setIndex: (index) => { + Search._index = index; + if (Search._queued_query !== null) { + const query = Search._queued_query; + Search._queued_query = null; + Search.query(query); + } + }, + + hasIndex: () => Search._index !== null, + + deferQuery: (query) => (Search._queued_query = query), + + stopPulse: () => (Search._pulse_status = -1), + + startPulse: () => { + if (Search._pulse_status >= 0) return; + + const pulse = () => { + Search._pulse_status = (Search._pulse_status + 1) % 4; + Search.dots.innerText = ".".repeat(Search._pulse_status); + if (Search._pulse_status >= 0) window.setTimeout(pulse, 500); + }; + pulse(); + }, + + /** + * perform a search for something (or wait until index is loaded) + */ + performSearch: (query) => { + // create the required interface elements + const searchText = document.createElement("h2"); + searchText.textContent = _("Searching"); + const searchSummary = document.createElement("p"); + searchSummary.classList.add("search-summary"); + searchSummary.innerText = ""; + const searchList = document.createElement("ul"); + searchList.setAttribute("role", "list"); + searchList.classList.add("search"); + + const out = document.getElementById("search-results"); + Search.title = out.appendChild(searchText); + Search.dots = Search.title.appendChild(document.createElement("span")); + Search.status = out.appendChild(searchSummary); + Search.output = out.appendChild(searchList); + + const searchProgress = document.getElementById("search-progress"); + // Some themes don't use the search progress node + if (searchProgress) { + searchProgress.innerText = _("Preparing search..."); + } + Search.startPulse(); + + // index already loaded, the browser was quick! + if (Search.hasIndex()) Search.query(query); + else Search.deferQuery(query); + }, + + _parseQuery: (query) => { + // stem the search terms and add them to the correct list + const stemmer = new Stemmer(); + const searchTerms = new Set(); + const excludedTerms = new Set(); + const highlightTerms = new Set(); + const objectTerms = new Set(splitQuery(query.toLowerCase().trim())); + splitQuery(query.trim()).forEach((queryTerm) => { + const queryTermLower = queryTerm.toLowerCase(); + + // maybe skip this "word" + // stopwords array is from language_data.js + if ( + stopwords.indexOf(queryTermLower) !== -1 || + queryTerm.match(/^\d+$/) + ) + return; + + // stem the word + let word = stemmer.stemWord(queryTermLower); + // select the correct list + if (word[0] === "-") excludedTerms.add(word.substr(1)); + else { + searchTerms.add(word); + highlightTerms.add(queryTermLower); + } + }); + + if (SPHINX_HIGHLIGHT_ENABLED) { // set in sphinx_highlight.js + localStorage.setItem("sphinx_highlight_terms", [...highlightTerms].join(" ")) + } + + // console.debug("SEARCH: searching for:"); + // console.info("required: ", [...searchTerms]); + // console.info("excluded: ", [...excludedTerms]); + + return [query, searchTerms, excludedTerms, highlightTerms, objectTerms]; + }, + + /** + * execute search (requires search index to be loaded) + */ + _performSearch: (query, searchTerms, excludedTerms, highlightTerms, objectTerms) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + const allTitles = Search._index.alltitles; + const indexEntries = Search._index.indexentries; + + // Collect multiple result groups to be sorted separately and then ordered. + // Each is an array of [docname, title, anchor, descr, score, filename, kind]. + const normalResults = []; + const nonMainIndexResults = []; + + _removeChildren(document.getElementById("search-progress")); + + const queryLower = query.toLowerCase().trim(); + for (const [title, foundTitles] of Object.entries(allTitles)) { + if (title.toLowerCase().trim().includes(queryLower) && (queryLower.length >= title.length/2)) { + for (const [file, id] of foundTitles) { + const score = Math.round(Scorer.title * queryLower.length / title.length); + const boost = titles[file] === title ? 1 : 0; // add a boost for document titles + normalResults.push([ + docNames[file], + titles[file] !== title ? `${titles[file]} > ${title}` : title, + id !== null ? "#" + id : "", + null, + score + boost, + filenames[file], + SearchResultKind.title, + ]); + } + } + } + + // search for explicit entries in index directives + for (const [entry, foundEntries] of Object.entries(indexEntries)) { + if (entry.includes(queryLower) && (queryLower.length >= entry.length/2)) { + for (const [file, id, isMain] of foundEntries) { + const score = Math.round(100 * queryLower.length / entry.length); + const result = [ + docNames[file], + titles[file], + id ? "#" + id : "", + null, + score, + filenames[file], + SearchResultKind.index, + ]; + if (isMain) { + normalResults.push(result); + } else { + nonMainIndexResults.push(result); + } + } + } + } + + // lookup as object + objectTerms.forEach((term) => + normalResults.push(...Search.performObjectSearch(term, objectTerms)) + ); + + // lookup as search terms in fulltext + normalResults.push(...Search.performTermsSearch(searchTerms, excludedTerms)); + + // let the scorer override scores with a custom scoring function + if (Scorer.score) { + normalResults.forEach((item) => (item[4] = Scorer.score(item))); + nonMainIndexResults.forEach((item) => (item[4] = Scorer.score(item))); + } + + // Sort each group of results by score and then alphabetically by name. + normalResults.sort(_orderResultsByScoreThenName); + nonMainIndexResults.sort(_orderResultsByScoreThenName); + + // Combine the result groups in (reverse) order. + // Non-main index entries are typically arbitrary cross-references, + // so display them after other results. + let results = [...nonMainIndexResults, ...normalResults]; + + // remove duplicate search results + // note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept + let seen = new Set(); + results = results.reverse().reduce((acc, result) => { + let resultStr = result.slice(0, 4).concat([result[5]]).map(v => String(v)).join(','); + if (!seen.has(resultStr)) { + acc.push(result); + seen.add(resultStr); + } + return acc; + }, []); + + return results.reverse(); + }, + + query: (query) => { + const [searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms] = Search._parseQuery(query); + const results = Search._performSearch(searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms); + + // for debugging + //Search.lastresults = results.slice(); // a copy + // console.info("search results:", Search.lastresults); + + // print the results + _displayNextItem(results, results.length, searchTerms, highlightTerms); + }, + + /** + * search for object names + */ + performObjectSearch: (object, objectTerms) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const objects = Search._index.objects; + const objNames = Search._index.objnames; + const titles = Search._index.titles; + + const results = []; + + const objectSearchCallback = (prefix, match) => { + const name = match[4] + const fullname = (prefix ? prefix + "." : "") + name; + const fullnameLower = fullname.toLowerCase(); + if (fullnameLower.indexOf(object) < 0) return; + + let score = 0; + const parts = fullnameLower.split("."); + + // check for different match types: exact matches of full name or + // "last name" (i.e. last dotted part) + if (fullnameLower === object || parts.slice(-1)[0] === object) + score += Scorer.objNameMatch; + else if (parts.slice(-1)[0].indexOf(object) > -1) + score += Scorer.objPartialMatch; // matches in last name + + const objName = objNames[match[1]][2]; + const title = titles[match[0]]; + + // If more than one term searched for, we require other words to be + // found in the name/title/description + const otherTerms = new Set(objectTerms); + otherTerms.delete(object); + if (otherTerms.size > 0) { + const haystack = `${prefix} ${name} ${objName} ${title}`.toLowerCase(); + if ( + [...otherTerms].some((otherTerm) => haystack.indexOf(otherTerm) < 0) + ) + return; + } + + let anchor = match[3]; + if (anchor === "") anchor = fullname; + else if (anchor === "-") anchor = objNames[match[1]][1] + "-" + fullname; + + const descr = objName + _(", in ") + title; + + // add custom score for some objects according to scorer + if (Scorer.objPrio.hasOwnProperty(match[2])) + score += Scorer.objPrio[match[2]]; + else score += Scorer.objPrioDefault; + + results.push([ + docNames[match[0]], + fullname, + "#" + anchor, + descr, + score, + filenames[match[0]], + SearchResultKind.object, + ]); + }; + Object.keys(objects).forEach((prefix) => + objects[prefix].forEach((array) => + objectSearchCallback(prefix, array) + ) + ); + return results; + }, + + /** + * search for full-text terms in the index + */ + performTermsSearch: (searchTerms, excludedTerms) => { + // prepare search + const terms = Search._index.terms; + const titleTerms = Search._index.titleterms; + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + + const scoreMap = new Map(); + const fileMap = new Map(); + + // perform the search on the required terms + searchTerms.forEach((word) => { + const files = []; + const arr = [ + { files: terms[word], score: Scorer.term }, + { files: titleTerms[word], score: Scorer.title }, + ]; + // add support for partial matches + if (word.length > 2) { + const escapedWord = _escapeRegExp(word); + if (!terms.hasOwnProperty(word)) { + Object.keys(terms).forEach((term) => { + if (term.match(escapedWord)) + arr.push({ files: terms[term], score: Scorer.partialTerm }); + }); + } + if (!titleTerms.hasOwnProperty(word)) { + Object.keys(titleTerms).forEach((term) => { + if (term.match(escapedWord)) + arr.push({ files: titleTerms[term], score: Scorer.partialTitle }); + }); + } + } + + // no match but word was a required one + if (arr.every((record) => record.files === undefined)) return; + + // found search word in contents + arr.forEach((record) => { + if (record.files === undefined) return; + + let recordFiles = record.files; + if (recordFiles.length === undefined) recordFiles = [recordFiles]; + files.push(...recordFiles); + + // set score for the word in each file + recordFiles.forEach((file) => { + if (!scoreMap.has(file)) scoreMap.set(file, {}); + scoreMap.get(file)[word] = record.score; + }); + }); + + // create the mapping + files.forEach((file) => { + if (!fileMap.has(file)) fileMap.set(file, [word]); + else if (fileMap.get(file).indexOf(word) === -1) fileMap.get(file).push(word); + }); + }); + + // now check if the files don't contain excluded terms + const results = []; + for (const [file, wordList] of fileMap) { + // check if all requirements are matched + + // as search terms with length < 3 are discarded + const filteredTermCount = [...searchTerms].filter( + (term) => term.length > 2 + ).length; + if ( + wordList.length !== searchTerms.size && + wordList.length !== filteredTermCount + ) + continue; + + // ensure that none of the excluded terms is in the search result + if ( + [...excludedTerms].some( + (term) => + terms[term] === file || + titleTerms[term] === file || + (terms[term] || []).includes(file) || + (titleTerms[term] || []).includes(file) + ) + ) + break; + + // select one (max) score for the file. + const score = Math.max(...wordList.map((w) => scoreMap.get(file)[w])); + // add result to the result list + results.push([ + docNames[file], + titles[file], + "", + null, + score, + filenames[file], + SearchResultKind.text, + ]); + } + return results; + }, + + /** + * helper function to return a node containing the + * search summary for a given text. keywords is a list + * of stemmed words. + */ + makeSearchSummary: (htmlText, keywords, anchor) => { + const text = Search.htmlToText(htmlText, anchor); + if (text === "") return null; + + const textLower = text.toLowerCase(); + const actualStartPosition = [...keywords] + .map((k) => textLower.indexOf(k.toLowerCase())) + .filter((i) => i > -1) + .slice(-1)[0]; + const startWithContext = Math.max(actualStartPosition - 120, 0); + + const top = startWithContext === 0 ? "" : "..."; + const tail = startWithContext + 240 < text.length ? "..." : ""; + + let summary = document.createElement("p"); + summary.classList.add("context"); + summary.textContent = top + text.substr(startWithContext, 240).trim() + tail; + + return summary; + }, +}; + +_ready(Search.init); diff --git a/tools/docs/_build/dirhtml/_static/shibuya.css b/tools/docs/_build/dirhtml/_static/shibuya.css new file mode 100644 index 0000000..859edaa --- /dev/null +++ b/tools/docs/_build/dirhtml/_static/shibuya.css @@ -0,0 +1,2 @@ +/*! tailwindcss v4.3.2 | MIT License | https://tailwindcss.com */ +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--spacing:.25rem;--container-6xl:72rem;--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.collapse{visibility:collapse}.relative{position:relative}.order-last{order:9999}.mx-auto{margin-inline:auto}.mr-3{margin-right:calc(var(--spacing) * 3)}.ml-1{margin-left:var(--spacing)}.block{display:block}.contents{display:contents}.flex{display:flex}.hidden{display:none}.inline-flex{display:inline-flex}.table{display:table}.w-8{width:calc(var(--spacing) * 8)}.w-64{width:calc(var(--spacing) * 64)}.w-full{width:100%}.max-w-6xl{max-width:var(--container-6xl)}.min-w-0{min-width:0}.flex-1{flex:1}.shrink-0{flex-shrink:0}.flex-grow{flex-grow:1}.resize{resize:both}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-around{justify-content:space-around}.justify-between{justify-content:space-between}.gap-1{gap:var(--spacing)}.p-6{padding:calc(var(--spacing) * 6)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-1{padding-block:var(--spacing)}.pt-6{padding-top:calc(var(--spacing) * 6)}.pt-8{padding-top:calc(var(--spacing) * 8)}.pt-12{padding-top:calc(var(--spacing) * 12)}.pr-3{padding-right:calc(var(--spacing) * 3)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.pb-6{padding-bottom:calc(var(--spacing) * 6)}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}@media not all and (min-width:40rem){.max-sm\:max-w-full{max-width:100%}}@media (min-width:48rem){.md\:sticky{position:sticky}.md\:flex{display:flex}.md\:hidden{display:none}.md\:inline{display:inline}.md\:w-72{width:calc(var(--spacing) * 72)}.md\:shrink-0{flex-shrink:0}}@media (min-width:64rem){.lg\:absolute{position:absolute}.lg\:top-8{top:calc(var(--spacing) * 8)}.lg\:right-6{right:calc(var(--spacing) * 6)}}@media (min-width:80rem){.xl\:sticky{position:sticky}.xl\:top-16{top:calc(var(--spacing) * 16)}.xl\:right-12{right:calc(var(--spacing) * 12)}.xl\:hidden{display:none}.xl\:px-12{padding-inline:calc(var(--spacing) * 12)}.xl\:pl-0{padding-left:0}}@media print{.print\:hidden{display:none}.print\:pt-6{padding-top:calc(var(--spacing) * 6)}}}@font-face{font-family:Twemoji Country Flags;unicode-range:U+1F1E6-1F1FF,U+1F3F4,U+E0062-E0063,U+E0065,U+E0067,U+E006C,U+E006E,U+E0073-E0074,U+E0077,U+E007F;src:url(https://cdn.jsdelivr.net/npm/country-flag-emoji-polyfill@0.1/dist/TwemojiCountryFlags.woff2)format("woff2")}::selection{color:var(--accent-contrast);background-color:var(--accent-a8)}html{scroll-behavior:smooth}body{font-family:var(--sy-f-text);color:var(--sy-c-text)}.win{font-family:"Twemoji Country Flags", var(--sy-f-text)}h1,h2,h3,h4,h5{color:var(--sy-c-heading);font-family:var(--sy-f-heading)}strong,em{color:var(--sy-c-bold)}.sy-container{max-width:90rem}.sy-scrollbar{scrollbar-gutter:stable;overflow-y:auto}.sy-scrollbar::-webkit-scrollbar{width:.75rem;height:.75rem}.sy-scrollbar::-webkit-scrollbar-thumb{border-radius:10px}.sy-scrollbar::-webkit-scrollbar-track{background-color:#0000}.sy-scrollbar:hover::-webkit-scrollbar-thumb{background-color:var(--gray-a3);background-clip:content-box;border:3px solid #0000}iconify-icon{vertical-align:middle}.i-lucide{-webkit-mask:var(--icon-url) no-repeat;-webkit-mask:var(--icon-url) no-repeat;mask:var(--icon-url) no-repeat;vertical-align:middle;background-color:currentColor;width:1em;height:1em;font-style:normal;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%}.theme-switch .theme-icon,[data-color-mode=auto] .theme-switch .theme-icon{--icon-url:var(--lucide-sun-moon-url)}[data-color-mode=light] .theme-switch .theme-icon{--icon-url:var(--lucide-sun-url)}[data-color-mode=dark] .theme-switch .theme-icon{--icon-url:var(--lucide-moon-url)}:root,.light,.light-theme{--tomato-1:#fffcfc;--tomato-2:#fff8f7;--tomato-3:#feebe7;--tomato-4:#ffdcd3;--tomato-5:#ffcdc2;--tomato-6:#fdbdaf;--tomato-7:#f5a898;--tomato-8:#ec8e7b;--tomato-9:#e54d2e;--tomato-10:#dd4425;--tomato-11:#d13415;--tomato-12:#5c271f}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--tomato-1:color(display-p3 .998 .989 .988);--tomato-2:color(display-p3 .994 .974 .969);--tomato-3:color(display-p3 .985 .924 .909);--tomato-4:color(display-p3 .996 .868 .835);--tomato-5:color(display-p3 .98 .812 .77);--tomato-6:color(display-p3 .953 .75 .698);--tomato-7:color(display-p3 .917 .673 .611);--tomato-8:color(display-p3 .875 .575 .502);--tomato-9:color(display-p3 .831 .345 .231);--tomato-10:color(display-p3 .802 .313 .2);--tomato-11:color(display-p3 .755 .259 .152);--tomato-12:color(display-p3 .335 .165 .132)}}}.dark,.dark-theme{--tomato-1:#181111;--tomato-2:#1f1513;--tomato-3:#391714;--tomato-4:#4e1511;--tomato-5:#5e1c16;--tomato-6:#6e2920;--tomato-7:#853a2d;--tomato-8:#ac4d39;--tomato-9:#e54d2e;--tomato-10:#ec6142;--tomato-11:#ff977d;--tomato-12:#fbd3cb}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--tomato-1:color(display-p3 .09 .068 .067);--tomato-2:color(display-p3 .115 .084 .076);--tomato-3:color(display-p3 .205 .097 .083);--tomato-4:color(display-p3 .282 .099 .077);--tomato-5:color(display-p3 .339 .129 .101);--tomato-6:color(display-p3 .398 .179 .141);--tomato-7:color(display-p3 .487 .245 .194);--tomato-8:color(display-p3 .629 .322 .248);--tomato-9:color(display-p3 .831 .345 .231);--tomato-10:color(display-p3 .862 .415 .298);--tomato-11:color(display-p3 1 .585 .455);--tomato-12:color(display-p3 .959 .833 .802)}}}:root,.light,.light-theme{--tomato-a1:#ff000003;--tomato-a2:#ff200008;--tomato-a3:#f52b0018;--tomato-a4:#ff35002c;--tomato-a5:#ff2e003d;--tomato-a6:#f92d0050;--tomato-a7:#e7280067;--tomato-a8:#db250084;--tomato-a9:#df2600d1;--tomato-a10:#d72400da;--tomato-a11:#cd2200ea;--tomato-a12:#460900e0}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--tomato-a1:color(display-p3 .675 .024 .024/.012);--tomato-a2:color(display-p3 .757 .145 .02/.032);--tomato-a3:color(display-p3 .831 .184 .012/.091);--tomato-a4:color(display-p3 .976 .192 .004/.165);--tomato-a5:color(display-p3 .918 .192 .004/.232);--tomato-a6:color(display-p3 .847 .173 .004/.302);--tomato-a7:color(display-p3 .788 .165 .004/.389);--tomato-a8:color(display-p3 .749 .153 .004/.499);--tomato-a9:color(display-p3 .78 .149 0/.769);--tomato-a10:color(display-p3 .757 .141 0/.8);--tomato-a11:color(display-p3 .755 .259 .152);--tomato-a12:color(display-p3 .335 .165 .132)}}}.dark,.dark-theme{--tomato-a1:#f1121208;--tomato-a2:#ff55330f;--tomato-a3:#ff35232b;--tomato-a4:#fd201142;--tomato-a5:#fe332153;--tomato-a6:#ff4f3864;--tomato-a7:#fd644a7d;--tomato-a8:#fe6d4ea7;--tomato-a9:#fe5431e4;--tomato-a10:#ff6847eb;--tomato-a11:#ff977d;--tomato-a12:#ffd6cefb}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--tomato-a1:color(display-p3 .973 .071 .071/.026);--tomato-a2:color(display-p3 .992 .376 .224/.051);--tomato-a3:color(display-p3 .996 .282 .176/.148);--tomato-a4:color(display-p3 1 .204 .118/.232);--tomato-a5:color(display-p3 1 .286 .192/.29);--tomato-a6:color(display-p3 1 .392 .278/.353);--tomato-a7:color(display-p3 1 .459 .349/.45);--tomato-a8:color(display-p3 1 .49 .369/.601);--tomato-a9:color(display-p3 1 .408 .267/.82);--tomato-a10:color(display-p3 1 .478 .341/.853);--tomato-a11:color(display-p3 1 .585 .455);--tomato-a12:color(display-p3 .959 .833 .802)}}}:root,.light,.light-theme{--red-1:#fffcfc;--red-2:#fff7f7;--red-3:#feebec;--red-4:#ffdbdc;--red-5:#ffcdce;--red-6:#fdbdbe;--red-7:#f4a9aa;--red-8:#eb8e90;--red-9:#e5484d;--red-10:#dc3e42;--red-11:#ce2c31;--red-12:#641723}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--red-1:color(display-p3 .998 .989 .988);--red-2:color(display-p3 .995 .971 .971);--red-3:color(display-p3 .985 .925 .925);--red-4:color(display-p3 .999 .866 .866);--red-5:color(display-p3 .984 .812 .811);--red-6:color(display-p3 .955 .751 .749);--red-7:color(display-p3 .915 .675 .672);--red-8:color(display-p3 .872 .575 .572);--red-9:color(display-p3 .83 .329 .324);--red-10:color(display-p3 .798 .294 .285);--red-11:color(display-p3 .744 .234 .222);--red-12:color(display-p3 .36 .115 .143)}}}.dark,.dark-theme{--red-1:#191111;--red-2:#201314;--red-3:#3b1219;--red-4:#500f1c;--red-5:#611623;--red-6:#72232d;--red-7:#8c333a;--red-8:#b54548;--red-9:#e5484d;--red-10:#ec5d5e;--red-11:#ff9592;--red-12:#ffd1d9}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--red-1:color(display-p3 .093 .068 .067);--red-2:color(display-p3 .118 .077 .079);--red-3:color(display-p3 .211 .081 .099);--red-4:color(display-p3 .287 .079 .113);--red-5:color(display-p3 .348 .11 .142);--red-6:color(display-p3 .414 .16 .183);--red-7:color(display-p3 .508 .224 .236);--red-8:color(display-p3 .659 .298 .297);--red-9:color(display-p3 .83 .329 .324);--red-10:color(display-p3 .861 .403 .387);--red-11:color(display-p3 1 .57 .55);--red-12:color(display-p3 .971 .826 .852)}}}:root,.light,.light-theme{--red-a1:#ff000003;--red-a2:#ff000008;--red-a3:#f3000d14;--red-a4:#ff000824;--red-a5:#ff000632;--red-a6:#f8000442;--red-a7:#df000356;--red-a8:#d2000571;--red-a9:#db0007b7;--red-a10:#d10005c1;--red-a11:#c40006d3;--red-a12:#55000de8}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--red-a1:color(display-p3 .675 .024 .024/.012);--red-a2:color(display-p3 .863 .024 .024/.028);--red-a3:color(display-p3 .792 .008 .008/.075);--red-a4:color(display-p3 1 .008 .008/.134);--red-a5:color(display-p3 .918 .008 .008/.189);--red-a6:color(display-p3 .831 .02 .004/.251);--red-a7:color(display-p3 .741 .016 .004/.33);--red-a8:color(display-p3 .698 .012 .004/.428);--red-a9:color(display-p3 .749 .008 0/.675);--red-a10:color(display-p3 .714 .012 0/.714);--red-a11:color(display-p3 .744 .234 .222);--red-a12:color(display-p3 .36 .115 .143)}}}.dark,.dark-theme{--red-a1:#f4121209;--red-a2:#f22f3e11;--red-a3:#ff173f2d;--red-a4:#fe0a3b44;--red-a5:#ff204756;--red-a6:#ff3e5668;--red-a7:#ff536184;--red-a8:#ff5d61b0;--red-a9:#fe4e54e4;--red-a10:#ff6465eb;--red-a11:#ff9592;--red-a12:#ffd1d9}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--red-a1:color(display-p3 .984 .071 .071/.03);--red-a2:color(display-p3 .996 .282 .282/.055);--red-a3:color(display-p3 1 .169 .271/.156);--red-a4:color(display-p3 1 .118 .267/.236);--red-a5:color(display-p3 1 .212 .314/.303);--red-a6:color(display-p3 1 .318 .38/.374);--red-a7:color(display-p3 1 .4 .424/.475);--red-a8:color(display-p3 1 .431 .431/.635);--red-a9:color(display-p3 1 .388 .384/.82);--red-a10:color(display-p3 1 .463 .447/.853);--red-a11:color(display-p3 1 .57 .55);--red-a12:color(display-p3 .971 .826 .852)}}}:root,.light,.light-theme{--ruby-1:#fffcfd;--ruby-2:#fff7f8;--ruby-3:#feeaed;--ruby-4:#ffdce1;--ruby-5:#ffced6;--ruby-6:#f8bfc8;--ruby-7:#efacb8;--ruby-8:#e592a3;--ruby-9:#e54666;--ruby-10:#dc3b5d;--ruby-11:#ca244d;--ruby-12:#64172b}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--ruby-1:color(display-p3 .998 .989 .992);--ruby-2:color(display-p3 .995 .971 .974);--ruby-3:color(display-p3 .983 .92 .928);--ruby-4:color(display-p3 .987 .869 .885);--ruby-5:color(display-p3 .968 .817 .839);--ruby-6:color(display-p3 .937 .758 .786);--ruby-7:color(display-p3 .897 .685 .721);--ruby-8:color(display-p3 .851 .588 .639);--ruby-9:color(display-p3 .83 .323 .408);--ruby-10:color(display-p3 .795 .286 .375);--ruby-11:color(display-p3 .728 .211 .311);--ruby-12:color(display-p3 .36 .115 .171)}}}.dark,.dark-theme{--ruby-1:#191113;--ruby-2:#1e1517;--ruby-3:#3a141e;--ruby-4:#4e1325;--ruby-5:#5e1a2e;--ruby-6:#6f2539;--ruby-7:#883447;--ruby-8:#b3445a;--ruby-9:#e54666;--ruby-10:#ec5a72;--ruby-11:#ff949d;--ruby-12:#fed2e1}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--ruby-1:color(display-p3 .093 .068 .074);--ruby-2:color(display-p3 .113 .083 .089);--ruby-3:color(display-p3 .208 .088 .117);--ruby-4:color(display-p3 .279 .092 .147);--ruby-5:color(display-p3 .337 .12 .18);--ruby-6:color(display-p3 .401 .166 .223);--ruby-7:color(display-p3 .495 .224 .281);--ruby-8:color(display-p3 .652 .295 .359);--ruby-9:color(display-p3 .83 .323 .408);--ruby-10:color(display-p3 .857 .392 .455);--ruby-11:color(display-p3 1 .57 .59);--ruby-12:color(display-p3 .968 .83 .88)}}}:root,.light,.light-theme{--ruby-a1:#ff005503;--ruby-a2:#ff002008;--ruby-a3:#f3002515;--ruby-a4:#ff002523;--ruby-a5:#ff002a31;--ruby-a6:#e4002440;--ruby-a7:#ce002553;--ruby-a8:#c300286d;--ruby-a9:#db002cb9;--ruby-a10:#d2002cc4;--ruby-a11:#c10030db;--ruby-a12:#550016e8}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--ruby-a1:color(display-p3 .675 .024 .349/.012);--ruby-a2:color(display-p3 .863 .024 .024/.028);--ruby-a3:color(display-p3 .804 .008 .11/.079);--ruby-a4:color(display-p3 .91 .008 .125/.13);--ruby-a5:color(display-p3 .831 .004 .133/.185);--ruby-a6:color(display-p3 .745 .004 .118/.244);--ruby-a7:color(display-p3 .678 .004 .114/.314);--ruby-a8:color(display-p3 .639 .004 .125/.412);--ruby-a9:color(display-p3 .753 0 .129/.679);--ruby-a10:color(display-p3 .714 0 .125/.714);--ruby-a11:color(display-p3 .728 .211 .311);--ruby-a12:color(display-p3 .36 .115 .171)}}}.dark,.dark-theme{--ruby-a1:#f4124a09;--ruby-a2:#fe5a7f0e;--ruby-a3:#ff235d2c;--ruby-a4:#fd195e42;--ruby-a5:#fe2d6b53;--ruby-a6:#ff447665;--ruby-a7:#ff577d80;--ruby-a8:#ff5c7cae;--ruby-a9:#fe4c70e4;--ruby-a10:#ff617beb;--ruby-a11:#ff949d;--ruby-a12:#ffd3e2fe}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--ruby-a1:color(display-p3 .984 .071 .329/.03);--ruby-a2:color(display-p3 .992 .376 .529/.051);--ruby-a3:color(display-p3 .996 .196 .404/.152);--ruby-a4:color(display-p3 1 .173 .416/.227);--ruby-a5:color(display-p3 1 .259 .459/.29);--ruby-a6:color(display-p3 1 .341 .506/.358);--ruby-a7:color(display-p3 1 .412 .541/.458);--ruby-a8:color(display-p3 1 .431 .537/.627);--ruby-a9:color(display-p3 1 .376 .482/.82);--ruby-a10:color(display-p3 1 .447 .522/.849);--ruby-a11:color(display-p3 1 .57 .59);--ruby-a12:color(display-p3 .968 .83 .88)}}}:root,.light,.light-theme{--crimson-1:#fffcfd;--crimson-2:#fef7f9;--crimson-3:#ffe9f0;--crimson-4:#fedce7;--crimson-5:#facedd;--crimson-6:#f3bed1;--crimson-7:#eaacc3;--crimson-8:#e093b2;--crimson-9:#e93d82;--crimson-10:#df3478;--crimson-11:#cb1d63;--crimson-12:#621639}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--crimson-1:color(display-p3 .998 .989 .992);--crimson-2:color(display-p3 .991 .969 .976);--crimson-3:color(display-p3 .987 .917 .941);--crimson-4:color(display-p3 .975 .866 .904);--crimson-5:color(display-p3 .953 .813 .864);--crimson-6:color(display-p3 .921 .755 .817);--crimson-7:color(display-p3 .88 .683 .761);--crimson-8:color(display-p3 .834 .592 .694);--crimson-9:color(display-p3 .843 .298 .507);--crimson-10:color(display-p3 .807 .266 .468);--crimson-11:color(display-p3 .731 .195 .388);--crimson-12:color(display-p3 .352 .111 .221)}}}.dark,.dark-theme{--crimson-1:#191114;--crimson-2:#201318;--crimson-3:#381525;--crimson-4:#4d122f;--crimson-5:#5c1839;--crimson-6:#6d2545;--crimson-7:#873356;--crimson-8:#b0436e;--crimson-9:#e93d82;--crimson-10:#ee518a;--crimson-11:#ff92ad;--crimson-12:#fdd3e8}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--crimson-1:color(display-p3 .093 .068 .078);--crimson-2:color(display-p3 .117 .078 .095);--crimson-3:color(display-p3 .203 .091 .143);--crimson-4:color(display-p3 .277 .087 .182);--crimson-5:color(display-p3 .332 .115 .22);--crimson-6:color(display-p3 .394 .162 .268);--crimson-7:color(display-p3 .489 .222 .336);--crimson-8:color(display-p3 .638 .289 .429);--crimson-9:color(display-p3 .843 .298 .507);--crimson-10:color(display-p3 .864 .364 .539);--crimson-11:color(display-p3 1 .56 .66);--crimson-12:color(display-p3 .966 .834 .906)}}}:root,.light,.light-theme{--crimson-a1:#ff005503;--crimson-a2:#e0004008;--crimson-a3:#ff005216;--crimson-a4:#f8005123;--crimson-a5:#e5004f31;--crimson-a6:#d0004b41;--crimson-a7:#bf004753;--crimson-a8:#b6004a6c;--crimson-a9:#e2005bc2;--crimson-a10:#d70056cb;--crimson-a11:#c4004fe2;--crimson-a12:#530026e9}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--crimson-a1:color(display-p3 .675 .024 .349/.012);--crimson-a2:color(display-p3 .757 .02 .267/.032);--crimson-a3:color(display-p3 .859 .008 .294/.083);--crimson-a4:color(display-p3 .827 .008 .298/.134);--crimson-a5:color(display-p3 .753 .008 .275/.189);--crimson-a6:color(display-p3 .682 .004 .247/.244);--crimson-a7:color(display-p3 .62 .004 .251/.318);--crimson-a8:color(display-p3 .6 .004 .251/.408);--crimson-a9:color(display-p3 .776 0 .298/.702);--crimson-a10:color(display-p3 .737 0 .275/.734);--crimson-a11:color(display-p3 .731 .195 .388);--crimson-a12:color(display-p3 .352 .111 .221)}}}.dark,.dark-theme{--crimson-a1:#f4126709;--crimson-a2:#f22f7a11;--crimson-a3:#fe2a8b2a;--crimson-a4:#fd158741;--crimson-a5:#fd278f51;--crimson-a6:#fe459763;--crimson-a7:#fd559b7f;--crimson-a8:#fe5b9bab;--crimson-a9:#fe418de8;--crimson-a10:#ff5693ed;--crimson-a11:#ff92ad;--crimson-a12:#ffd5eafd}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--crimson-a1:color(display-p3 .984 .071 .463/.03);--crimson-a2:color(display-p3 .996 .282 .569/.055);--crimson-a3:color(display-p3 .996 .227 .573/.148);--crimson-a4:color(display-p3 1 .157 .569/.227);--crimson-a5:color(display-p3 1 .231 .604/.286);--crimson-a6:color(display-p3 1 .337 .643/.349);--crimson-a7:color(display-p3 1 .416 .663/.454);--crimson-a8:color(display-p3 .996 .427 .651/.614);--crimson-a9:color(display-p3 1 .345 .596/.832);--crimson-a10:color(display-p3 1 .42 .62/.853);--crimson-a11:color(display-p3 1 .56 .66);--crimson-a12:color(display-p3 .966 .834 .906)}}}:root,.light,.light-theme{--pink-1:#fffcfe;--pink-2:#fef7fb;--pink-3:#fee9f5;--pink-4:#fbdcef;--pink-5:#f6cee7;--pink-6:#efbfdd;--pink-7:#e7acd0;--pink-8:#dd93c2;--pink-9:#d6409f;--pink-10:#cf3897;--pink-11:#c2298a;--pink-12:#651249}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--pink-1:color(display-p3 .998 .989 .996);--pink-2:color(display-p3 .992 .97 .985);--pink-3:color(display-p3 .981 .917 .96);--pink-4:color(display-p3 .963 .867 .932);--pink-5:color(display-p3 .939 .815 .899);--pink-6:color(display-p3 .907 .756 .859);--pink-7:color(display-p3 .869 .683 .81);--pink-8:color(display-p3 .825 .59 .751);--pink-9:color(display-p3 .775 .297 .61);--pink-10:color(display-p3 .748 .27 .581);--pink-11:color(display-p3 .698 .219 .528);--pink-12:color(display-p3 .363 .101 .279)}}}.dark,.dark-theme{--pink-1:#191117;--pink-2:#21121d;--pink-3:#37172f;--pink-4:#4b143d;--pink-5:#591c47;--pink-6:#692955;--pink-7:#833869;--pink-8:#a84885;--pink-9:#d6409f;--pink-10:#de51a8;--pink-11:#ff8dcc;--pink-12:#fdd1ea}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--pink-1:color(display-p3 .093 .068 .089);--pink-2:color(display-p3 .121 .073 .11);--pink-3:color(display-p3 .198 .098 .179);--pink-4:color(display-p3 .271 .095 .231);--pink-5:color(display-p3 .32 .127 .273);--pink-6:color(display-p3 .382 .177 .326);--pink-7:color(display-p3 .477 .238 .405);--pink-8:color(display-p3 .612 .304 .51);--pink-9:color(display-p3 .775 .297 .61);--pink-10:color(display-p3 .808 .356 .645);--pink-11:color(display-p3 1 .535 .78);--pink-12:color(display-p3 .964 .826 .912)}}}:root,.light,.light-theme{--pink-a1:#ff00aa03;--pink-a2:#e0008008;--pink-a3:#f4008c16;--pink-a4:#e2008b23;--pink-a5:#d1008331;--pink-a6:#c0007840;--pink-a7:#b6006f53;--pink-a8:#af006f6c;--pink-a9:#c8007fbf;--pink-a10:#c2007ac7;--pink-a11:#b60074d6;--pink-a12:#59003bed}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--pink-a1:color(display-p3 .675 .024 .675/.012);--pink-a2:color(display-p3 .757 .02 .51/.032);--pink-a3:color(display-p3 .765 .008 .529/.083);--pink-a4:color(display-p3 .737 .008 .506/.134);--pink-a5:color(display-p3 .663 .004 .451/.185);--pink-a6:color(display-p3 .616 .004 .424/.244);--pink-a7:color(display-p3 .596 .004 .412/.318);--pink-a8:color(display-p3 .573 .004 .404/.412);--pink-a9:color(display-p3 .682 0 .447/.702);--pink-a10:color(display-p3 .655 0 .424/.73);--pink-a11:color(display-p3 .698 .219 .528);--pink-a12:color(display-p3 .363 .101 .279)}}}.dark,.dark-theme{--pink-a1:#f412bc09;--pink-a2:#f420bb12;--pink-a3:#fe37cc29;--pink-a4:#fc1ec43f;--pink-a5:#fd35c24e;--pink-a6:#fd51c75f;--pink-a7:#fd62c87b;--pink-a8:#ff68c8a2;--pink-a9:#fe49bcd4;--pink-a10:#ff5cc0dc;--pink-a11:#ff8dcc;--pink-a12:#ffd3ecfd}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--pink-a1:color(display-p3 .984 .071 .855/.03);--pink-a2:color(display-p3 1 .2 .8/.059);--pink-a3:color(display-p3 1 .294 .886/.139);--pink-a4:color(display-p3 1 .192 .82/.219);--pink-a5:color(display-p3 1 .282 .827/.274);--pink-a6:color(display-p3 1 .396 .835/.337);--pink-a7:color(display-p3 1 .459 .831/.442);--pink-a8:color(display-p3 1 .478 .827/.585);--pink-a9:color(display-p3 1 .373 .784/.761);--pink-a10:color(display-p3 1 .435 .792/.795);--pink-a11:color(display-p3 1 .535 .78);--pink-a12:color(display-p3 .964 .826 .912)}}}:root,.light,.light-theme{--plum-1:#fefcff;--plum-2:#fdf7fd;--plum-3:#fbebfb;--plum-4:#f7def8;--plum-5:#f2d1f3;--plum-6:#e9c2ec;--plum-7:#deade3;--plum-8:#cf91d8;--plum-9:#ab4aba;--plum-10:#a144af;--plum-11:#953ea3;--plum-12:#53195d}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--plum-1:color(display-p3 .995 .988 .999);--plum-2:color(display-p3 .988 .971 .99);--plum-3:color(display-p3 .973 .923 .98);--plum-4:color(display-p3 .953 .875 .966);--plum-5:color(display-p3 .926 .825 .945);--plum-6:color(display-p3 .89 .765 .916);--plum-7:color(display-p3 .84 .686 .877);--plum-8:color(display-p3 .775 .58 .832);--plum-9:color(display-p3 .624 .313 .708);--plum-10:color(display-p3 .587 .29 .667);--plum-11:color(display-p3 .543 .263 .619);--plum-12:color(display-p3 .299 .114 .352)}}}.dark,.dark-theme{--plum-1:#181118;--plum-2:#201320;--plum-3:#351a35;--plum-4:#451d47;--plum-5:#512454;--plum-6:#5e3061;--plum-7:#734079;--plum-8:#92549c;--plum-9:#ab4aba;--plum-10:#b658c4;--plum-11:#e796f3;--plum-12:#f4d4f4}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--plum-1:color(display-p3 .09 .068 .092);--plum-2:color(display-p3 .118 .077 .121);--plum-3:color(display-p3 .192 .105 .202);--plum-4:color(display-p3 .25 .121 .271);--plum-5:color(display-p3 .293 .152 .319);--plum-6:color(display-p3 .343 .198 .372);--plum-7:color(display-p3 .424 .262 .461);--plum-8:color(display-p3 .54 .341 .595);--plum-9:color(display-p3 .624 .313 .708);--plum-10:color(display-p3 .666 .365 .748);--plum-11:color(display-p3 .86 .602 .933);--plum-12:color(display-p3 .936 .836 .949)}}}:root,.light,.light-theme{--plum-a1:#aa00ff03;--plum-a2:#c000c008;--plum-a3:#cc00cc14;--plum-a4:#c200c921;--plum-a5:#b700bd2e;--plum-a6:#a400b03d;--plum-a7:#9900a852;--plum-a8:#9000a56e;--plum-a9:#89009eb5;--plum-a10:#7f0092bb;--plum-a11:#730086c1;--plum-a12:#40004be6}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--plum-a1:color(display-p3 .675 .024 1/.012);--plum-a2:color(display-p3 .58 .024 .58/.028);--plum-a3:color(display-p3 .655 .008 .753/.079);--plum-a4:color(display-p3 .627 .008 .722/.126);--plum-a5:color(display-p3 .58 .004 .69/.177);--plum-a6:color(display-p3 .537 .004 .655/.236);--plum-a7:color(display-p3 .49 .004 .616/.314);--plum-a8:color(display-p3 .471 .004 .6/.42);--plum-a9:color(display-p3 .451 0 .576/.687);--plum-a10:color(display-p3 .42 0 .529/.71);--plum-a11:color(display-p3 .543 .263 .619);--plum-a12:color(display-p3 .299 .114 .352)}}}.dark,.dark-theme{--plum-a1:#f112f108;--plum-a2:#f22ff211;--plum-a3:#fd4cfd27;--plum-a4:#f646ff3a;--plum-a5:#f455ff48;--plum-a6:#f66dff56;--plum-a7:#f07cfd70;--plum-a8:#ee84ff95;--plum-a9:#e961feb6;--plum-a10:#ed70ffc0;--plum-a11:#f19cfef3;--plum-a12:#feddfef4}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--plum-a1:color(display-p3 .973 .071 .973/.026);--plum-a2:color(display-p3 .933 .267 1/.059);--plum-a3:color(display-p3 .918 .333 .996/.148);--plum-a4:color(display-p3 .91 .318 1/.219);--plum-a5:color(display-p3 .914 .388 1/.269);--plum-a6:color(display-p3 .906 .463 1/.328);--plum-a7:color(display-p3 .906 .529 1/.425);--plum-a8:color(display-p3 .906 .553 1/.568);--plum-a9:color(display-p3 .875 .427 1/.69);--plum-a10:color(display-p3 .886 .471 .996/.732);--plum-a11:color(display-p3 .86 .602 .933);--plum-a12:color(display-p3 .936 .836 .949)}}}:root,.light,.light-theme{--purple-1:#fefcfe;--purple-2:#fbf7fe;--purple-3:#f7edfe;--purple-4:#f2e2fc;--purple-5:#ead5f9;--purple-6:#e0c4f4;--purple-7:#d1afec;--purple-8:#be93e4;--purple-9:#8e4ec6;--purple-10:#8347b9;--purple-11:#8145b5;--purple-12:#402060}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--purple-1:color(display-p3 .995 .988 .996);--purple-2:color(display-p3 .983 .971 .993);--purple-3:color(display-p3 .963 .931 .989);--purple-4:color(display-p3 .937 .888 .981);--purple-5:color(display-p3 .904 .837 .966);--purple-6:color(display-p3 .86 .774 .942);--purple-7:color(display-p3 .799 .69 .91);--purple-8:color(display-p3 .719 .583 .874);--purple-9:color(display-p3 .523 .318 .751);--purple-10:color(display-p3 .483 .289 .7);--purple-11:color(display-p3 .473 .281 .687);--purple-12:color(display-p3 .234 .132 .363)}}}.dark,.dark-theme{--purple-1:#18111b;--purple-2:#1e1523;--purple-3:#301c3b;--purple-4:#3d224e;--purple-5:#48295c;--purple-6:#54346b;--purple-7:#664282;--purple-8:#8457aa;--purple-9:#8e4ec6;--purple-10:#9a5cd0;--purple-11:#d19dff;--purple-12:#ecd9fa}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--purple-1:color(display-p3 .09 .068 .103);--purple-2:color(display-p3 .113 .082 .134);--purple-3:color(display-p3 .175 .112 .224);--purple-4:color(display-p3 .224 .137 .297);--purple-5:color(display-p3 .264 .167 .349);--purple-6:color(display-p3 .311 .208 .406);--purple-7:color(display-p3 .381 .266 .496);--purple-8:color(display-p3 .49 .349 .649);--purple-9:color(display-p3 .523 .318 .751);--purple-10:color(display-p3 .57 .373 .791);--purple-11:color(display-p3 .8 .62 1);--purple-12:color(display-p3 .913 .854 .971)}}}:root,.light,.light-theme{--purple-a1:#aa00aa03;--purple-a2:#8000e008;--purple-a3:#8e00f112;--purple-a4:#8d00e51d;--purple-a5:#8000db2a;--purple-a6:#7a01d03b;--purple-a7:#6d00c350;--purple-a8:#6600c06c;--purple-a9:#5c00adb1;--purple-a10:#53009eb8;--purple-a11:#52009aba;--purple-a12:#250049df}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--purple-a1:color(display-p3 .675 .024 .675/.012);--purple-a2:color(display-p3 .443 .024 .722/.028);--purple-a3:color(display-p3 .506 .008 .835/.071);--purple-a4:color(display-p3 .451 .004 .831/.114);--purple-a5:color(display-p3 .431 .004 .788/.165);--purple-a6:color(display-p3 .384 .004 .745/.228);--purple-a7:color(display-p3 .357 .004 .71/.31);--purple-a8:color(display-p3 .322 .004 .702/.416);--purple-a9:color(display-p3 .298 0 .639/.683);--purple-a10:color(display-p3 .271 0 .58/.71);--purple-a11:color(display-p3 .473 .281 .687);--purple-a12:color(display-p3 .234 .132 .363)}}}.dark,.dark-theme{--purple-a1:#b412f90b;--purple-a2:#b744f714;--purple-a3:#c150ff2d;--purple-a4:#bb53fd42;--purple-a5:#be5cfd51;--purple-a6:#c16dfd61;--purple-a7:#c378fd7a;--purple-a8:#c47effa4;--purple-a9:#b661ffc2;--purple-a10:#bc6fffcd;--purple-a11:#d19dff;--purple-a12:#f1ddfffa}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--purple-a1:color(display-p3 .686 .071 .996/.038);--purple-a2:color(display-p3 .722 .286 .996/.072);--purple-a3:color(display-p3 .718 .349 .996/.169);--purple-a4:color(display-p3 .702 .353 1/.248);--purple-a5:color(display-p3 .718 .404 1/.303);--purple-a6:color(display-p3 .733 .455 1/.366);--purple-a7:color(display-p3 .753 .506 1/.458);--purple-a8:color(display-p3 .749 .522 1/.622);--purple-a9:color(display-p3 .686 .408 1/.736);--purple-a10:color(display-p3 .71 .459 1/.778);--purple-a11:color(display-p3 .8 .62 1);--purple-a12:color(display-p3 .913 .854 .971)}}}:root,.light,.light-theme{--violet-1:#fdfcfe;--violet-2:#faf8ff;--violet-3:#f4f0fe;--violet-4:#ebe4ff;--violet-5:#e1d9ff;--violet-6:#d4cafe;--violet-7:#c2b5f5;--violet-8:#aa99ec;--violet-9:#6e56cf;--violet-10:#654dc4;--violet-11:#6550b9;--violet-12:#2f265f}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--violet-1:color(display-p3 .991 .988 .995);--violet-2:color(display-p3 .978 .974 .998);--violet-3:color(display-p3 .953 .943 .993);--violet-4:color(display-p3 .916 .897 1);--violet-5:color(display-p3 .876 .851 1);--violet-6:color(display-p3 .825 .793 .981);--violet-7:color(display-p3 .752 .712 .943);--violet-8:color(display-p3 .654 .602 .902);--violet-9:color(display-p3 .417 .341 .784);--violet-10:color(display-p3 .381 .306 .741);--violet-11:color(display-p3 .383 .317 .702);--violet-12:color(display-p3 .179 .15 .359)}}}.dark,.dark-theme{--violet-1:#14121f;--violet-2:#1b1525;--violet-3:#291f43;--violet-4:#33255b;--violet-5:#3c2e69;--violet-6:#473876;--violet-7:#56468b;--violet-8:#6958ad;--violet-9:#6e56cf;--violet-10:#7d66d9;--violet-11:#baa7ff;--violet-12:#e2ddfe}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--violet-1:color(display-p3 .077 .071 .118);--violet-2:color(display-p3 .101 .084 .141);--violet-3:color(display-p3 .154 .123 .256);--violet-4:color(display-p3 .191 .148 .345);--violet-5:color(display-p3 .226 .182 .396);--violet-6:color(display-p3 .269 .223 .449);--violet-7:color(display-p3 .326 .277 .53);--violet-8:color(display-p3 .399 .346 .656);--violet-9:color(display-p3 .417 .341 .784);--violet-10:color(display-p3 .477 .402 .823);--violet-11:color(display-p3 .72 .65 1);--violet-12:color(display-p3 .883 .867 .986)}}}:root,.light,.light-theme{--violet-a1:#5500aa03;--violet-a2:#4900ff07;--violet-a3:#4400ee0f;--violet-a4:#4300ff1b;--violet-a5:#3600ff26;--violet-a6:#3100fb35;--violet-a7:#2d01dd4a;--violet-a8:#2b00d066;--violet-a9:#2400b7a9;--violet-a10:#2300abb2;--violet-a11:#1f0099af;--violet-a12:#0b0043d9}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--violet-a1:color(display-p3 .349 .024 .675/.012);--violet-a2:color(display-p3 .161 .024 .863/.028);--violet-a3:color(display-p3 .204 .004 .871/.059);--violet-a4:color(display-p3 .196 .004 1/.102);--violet-a5:color(display-p3 .165 .008 1/.15);--violet-a6:color(display-p3 .153 .004 .906/.208);--violet-a7:color(display-p3 .141 .004 .796/.287);--violet-a8:color(display-p3 .133 .004 .753/.397);--violet-a9:color(display-p3 .114 0 .675/.659);--violet-a10:color(display-p3 .11 0 .627/.695);--violet-a11:color(display-p3 .383 .317 .702);--violet-a12:color(display-p3 .179 .15 .359)}}}.dark,.dark-theme{--violet-a1:#4422ff0f;--violet-a2:#853ff916;--violet-a3:#8354fe36;--violet-a4:#7d51fd50;--violet-a5:#845ffd5f;--violet-a6:#8f6cfd6d;--violet-a7:#9879ff83;--violet-a8:#977dfea8;--violet-a9:#8668ffcc;--violet-a10:#9176fed7;--violet-a11:#baa7ff;--violet-a12:#e3defffe}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--violet-a1:color(display-p3 .282 .141 .996/.055);--violet-a2:color(display-p3 .51 .263 1/.08);--violet-a3:color(display-p3 .494 .337 .996/.202);--violet-a4:color(display-p3 .49 .345 1/.299);--violet-a5:color(display-p3 .525 .392 1/.353);--violet-a6:color(display-p3 .569 .455 1/.408);--violet-a7:color(display-p3 .588 .494 1/.496);--violet-a8:color(display-p3 .596 .51 1/.631);--violet-a9:color(display-p3 .522 .424 1/.769);--violet-a10:color(display-p3 .576 .482 1/.811);--violet-a11:color(display-p3 .72 .65 1);--violet-a12:color(display-p3 .883 .867 .986)}}}:root,.light,.light-theme{--iris-1:#fdfdff;--iris-2:#f8f8ff;--iris-3:#f0f1fe;--iris-4:#e6e7ff;--iris-5:#dadcff;--iris-6:#cbcdff;--iris-7:#b8baf8;--iris-8:#9b9ef0;--iris-9:#5b5bd6;--iris-10:#5151cd;--iris-11:#5753c6;--iris-12:#272962}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--iris-1:color(display-p3 .992 .992 .999);--iris-2:color(display-p3 .972 .973 .998);--iris-3:color(display-p3 .943 .945 .992);--iris-4:color(display-p3 .902 .906 1);--iris-5:color(display-p3 .857 .861 1);--iris-6:color(display-p3 .799 .805 .987);--iris-7:color(display-p3 .721 .727 .955);--iris-8:color(display-p3 .61 .619 .918);--iris-9:color(display-p3 .357 .357 .81);--iris-10:color(display-p3 .318 .318 .774);--iris-11:color(display-p3 .337 .326 .748);--iris-12:color(display-p3 .154 .161 .371)}}}.dark,.dark-theme{--iris-1:#13131e;--iris-2:#171625;--iris-3:#202248;--iris-4:#262a65;--iris-5:#303374;--iris-6:#3d3e82;--iris-7:#4a4a95;--iris-8:#5958b1;--iris-9:#5b5bd6;--iris-10:#6e6ade;--iris-11:#b1a9ff;--iris-12:#e0dffe}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--iris-1:color(display-p3 .075 .075 .114);--iris-2:color(display-p3 .089 .086 .14);--iris-3:color(display-p3 .128 .134 .272);--iris-4:color(display-p3 .153 .165 .382);--iris-5:color(display-p3 .192 .201 .44);--iris-6:color(display-p3 .239 .241 .491);--iris-7:color(display-p3 .291 .289 .565);--iris-8:color(display-p3 .35 .345 .673);--iris-9:color(display-p3 .357 .357 .81);--iris-10:color(display-p3 .428 .416 .843);--iris-11:color(display-p3 .685 .662 1);--iris-12:color(display-p3 .878 .875 .986)}}}:root,.light,.light-theme{--iris-a1:#0000ff02;--iris-a2:#0000ff07;--iris-a3:#0011ee0f;--iris-a4:#000bff19;--iris-a5:#000eff25;--iris-a6:#000aff34;--iris-a7:#0008e647;--iris-a8:#0008d964;--iris-a9:#0000c0a4;--iris-a10:#0000b6ae;--iris-a11:#0600abac;--iris-a12:#000246d8}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--iris-a1:color(display-p3 .02 .02 1/.008);--iris-a2:color(display-p3 .024 .024 .863/.028);--iris-a3:color(display-p3 .004 .071 .871/.059);--iris-a4:color(display-p3 .012 .051 1/.099);--iris-a5:color(display-p3 .008 .035 1/.142);--iris-a6:color(display-p3 0 .02 .941/.2);--iris-a7:color(display-p3 .004 .02 .847/.279);--iris-a8:color(display-p3 .004 .024 .788/.389);--iris-a9:color(display-p3 0 0 .706/.644);--iris-a10:color(display-p3 0 0 .667/.683);--iris-a11:color(display-p3 .337 .326 .748);--iris-a12:color(display-p3 .154 .161 .371)}}}.dark,.dark-theme{--iris-a1:#3636fe0e;--iris-a2:#564bf916;--iris-a3:#525bff3b;--iris-a4:#4d58ff5a;--iris-a5:#5b62fd6b;--iris-a6:#6d6ffd7a;--iris-a7:#7777fe8e;--iris-a8:#7b7afeac;--iris-a9:#6a6afed4;--iris-a10:#7d79ffdc;--iris-a11:#b1a9ff;--iris-a12:#e1e0fffe}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--iris-a1:color(display-p3 .224 .224 .992/.051);--iris-a2:color(display-p3 .361 .314 1/.08);--iris-a3:color(display-p3 .357 .373 1/.219);--iris-a4:color(display-p3 .325 .361 1/.337);--iris-a5:color(display-p3 .38 .4 1/.4);--iris-a6:color(display-p3 .447 .447 1/.454);--iris-a7:color(display-p3 .486 .486 1/.534);--iris-a8:color(display-p3 .502 .494 1/.652);--iris-a9:color(display-p3 .431 .431 1/.799);--iris-a10:color(display-p3 .502 .486 1/.832);--iris-a11:color(display-p3 .685 .662 1);--iris-a12:color(display-p3 .878 .875 .986)}}}:root,.light,.light-theme{--indigo-1:#fdfdfe;--indigo-2:#f7f9ff;--indigo-3:#edf2fe;--indigo-4:#e1e9ff;--indigo-5:#d2deff;--indigo-6:#c1d0ff;--indigo-7:#abbdf9;--indigo-8:#8da4ef;--indigo-9:#3e63dd;--indigo-10:#3358d4;--indigo-11:#3a5bc7;--indigo-12:#1f2d5c}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--indigo-1:color(display-p3 .992 .992 .996);--indigo-2:color(display-p3 .971 .977 .998);--indigo-3:color(display-p3 .933 .948 .992);--indigo-4:color(display-p3 .885 .914 1);--indigo-5:color(display-p3 .831 .87 1);--indigo-6:color(display-p3 .767 .814 .995);--indigo-7:color(display-p3 .685 .74 .957);--indigo-8:color(display-p3 .569 .639 .916);--indigo-9:color(display-p3 .276 .384 .837);--indigo-10:color(display-p3 .234 .343 .801);--indigo-11:color(display-p3 .256 .354 .755);--indigo-12:color(display-p3 .133 .175 .348)}}}.dark,.dark-theme{--indigo-1:#11131f;--indigo-2:#141726;--indigo-3:#182449;--indigo-4:#1d2e62;--indigo-5:#253974;--indigo-6:#304384;--indigo-7:#3a4f97;--indigo-8:#435db1;--indigo-9:#3e63dd;--indigo-10:#5472e4;--indigo-11:#9eb1ff;--indigo-12:#d6e1ff}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--indigo-1:color(display-p3 .068 .074 .118);--indigo-2:color(display-p3 .081 .089 .144);--indigo-3:color(display-p3 .105 .141 .275);--indigo-4:color(display-p3 .129 .18 .369);--indigo-5:color(display-p3 .163 .22 .439);--indigo-6:color(display-p3 .203 .262 .5);--indigo-7:color(display-p3 .245 .309 .575);--indigo-8:color(display-p3 .285 .362 .674);--indigo-9:color(display-p3 .276 .384 .837);--indigo-10:color(display-p3 .354 .445 .866);--indigo-11:color(display-p3 .63 .69 1);--indigo-12:color(display-p3 .848 .881 .99)}}}:root,.light,.light-theme{--indigo-a1:#00008002;--indigo-a2:#0040ff08;--indigo-a3:#0047f112;--indigo-a4:#0044ff1e;--indigo-a5:#0044ff2d;--indigo-a6:#003eff3e;--indigo-a7:#0037ed54;--indigo-a8:#0034dc72;--indigo-a9:#0031d2c1;--indigo-a10:#002ec9cc;--indigo-a11:#002bb7c5;--indigo-a12:#001046e0}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--indigo-a1:color(display-p3 .02 .02 .51/.008);--indigo-a2:color(display-p3 .024 .161 .863/.028);--indigo-a3:color(display-p3 .008 .239 .886/.067);--indigo-a4:color(display-p3 .004 .247 1/.114);--indigo-a5:color(display-p3 .004 .235 1/.169);--indigo-a6:color(display-p3 .004 .208 .984/.232);--indigo-a7:color(display-p3 .004 .176 .863/.314);--indigo-a8:color(display-p3 .004 .165 .812/.432);--indigo-a9:color(display-p3 0 .153 .773/.726);--indigo-a10:color(display-p3 0 .137 .737/.765);--indigo-a11:color(display-p3 .256 .354 .755);--indigo-a12:color(display-p3 .133 .175 .348)}}}.dark,.dark-theme{--indigo-a1:#1133ff0f;--indigo-a2:#3354fa17;--indigo-a3:#2f62ff3c;--indigo-a4:#3566ff57;--indigo-a5:#4171fd6b;--indigo-a6:#5178fd7c;--indigo-a7:#5a7fff90;--indigo-a8:#5b81feac;--indigo-a9:#4671ffdb;--indigo-a10:#5c7efee3;--indigo-a11:#9eb1ff;--indigo-a12:#d6e1ff}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--indigo-a1:color(display-p3 .071 .212 .996/.055);--indigo-a2:color(display-p3 .251 .345 .988/.085);--indigo-a3:color(display-p3 .243 .404 1/.223);--indigo-a4:color(display-p3 .263 .42 1/.324);--indigo-a5:color(display-p3 .314 .451 1/.4);--indigo-a6:color(display-p3 .361 .49 1/.467);--indigo-a7:color(display-p3 .388 .51 1/.547);--indigo-a8:color(display-p3 .404 .518 1/.652);--indigo-a9:color(display-p3 .318 .451 1/.824);--indigo-a10:color(display-p3 .404 .506 1/.858);--indigo-a11:color(display-p3 .63 .69 1);--indigo-a12:color(display-p3 .848 .881 .99)}}}:root,.light,.light-theme{--blue-1:#fbfdff;--blue-2:#f4faff;--blue-3:#e6f4fe;--blue-4:#d5efff;--blue-5:#c2e5ff;--blue-6:#acd8fc;--blue-7:#8ec8f6;--blue-8:#5eb1ef;--blue-9:#0090ff;--blue-10:#0588f0;--blue-11:#0d74ce;--blue-12:#113264}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--blue-1:color(display-p3 .986 .992 .999);--blue-2:color(display-p3 .96 .979 .998);--blue-3:color(display-p3 .912 .956 .991);--blue-4:color(display-p3 .853 .932 1);--blue-5:color(display-p3 .788 .894 .998);--blue-6:color(display-p3 .709 .843 .976);--blue-7:color(display-p3 .606 .777 .947);--blue-8:color(display-p3 .451 .688 .917);--blue-9:color(display-p3 .247 .556 .969);--blue-10:color(display-p3 .234 .523 .912);--blue-11:color(display-p3 .15 .44 .84);--blue-12:color(display-p3 .102 .193 .379)}}}.dark,.dark-theme{--blue-1:#0d1520;--blue-2:#111927;--blue-3:#0d2847;--blue-4:#003362;--blue-5:#004074;--blue-6:#104d87;--blue-7:#205d9e;--blue-8:#2870bd;--blue-9:#0090ff;--blue-10:#3b9eff;--blue-11:#70b8ff;--blue-12:#c2e6ff}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--blue-1:color(display-p3 .057 .081 .122);--blue-2:color(display-p3 .072 .098 .147);--blue-3:color(display-p3 .078 .154 .27);--blue-4:color(display-p3 .033 .197 .37);--blue-5:color(display-p3 .08 .245 .441);--blue-6:color(display-p3 .14 .298 .511);--blue-7:color(display-p3 .195 .361 .6);--blue-8:color(display-p3 .239 .434 .72);--blue-9:color(display-p3 .247 .556 .969);--blue-10:color(display-p3 .344 .612 .973);--blue-11:color(display-p3 .49 .72 1);--blue-12:color(display-p3 .788 .898 .99)}}}:root,.light,.light-theme{--blue-a1:#0080ff04;--blue-a2:#008cff0b;--blue-a3:#008ff519;--blue-a4:#009eff2a;--blue-a5:#0093ff3d;--blue-a6:#0088f653;--blue-a7:#0083eb71;--blue-a8:#0084e6a1;--blue-a9:#0090ff;--blue-a10:#0086f0fa;--blue-a11:#006dcbf2;--blue-a12:#002359ee}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--blue-a1:color(display-p3 .024 .514 1/.016);--blue-a2:color(display-p3 .024 .514 .906/.04);--blue-a3:color(display-p3 .012 .506 .914/.087);--blue-a4:color(display-p3 .008 .545 1/.146);--blue-a5:color(display-p3 .004 .502 .984/.212);--blue-a6:color(display-p3 .004 .463 .922/.291);--blue-a7:color(display-p3 .004 .431 .863/.393);--blue-a8:color(display-p3 0 .427 .851/.55);--blue-a9:color(display-p3 0 .412 .961/.753);--blue-a10:color(display-p3 0 .376 .886/.765);--blue-a11:color(display-p3 .15 .44 .84);--blue-a12:color(display-p3 .102 .193 .379)}}}.dark,.dark-theme{--blue-a1:#004df211;--blue-a2:#1166fb18;--blue-a3:#0077ff3a;--blue-a4:#0075ff57;--blue-a5:#0081fd6b;--blue-a6:#0f89fd7f;--blue-a7:#2a91fe98;--blue-a8:#3094feb9;--blue-a9:#0090ff;--blue-a10:#3b9eff;--blue-a11:#70b8ff;--blue-a12:#c2e6ff}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--blue-a1:color(display-p3 0 .333 1/.059);--blue-a2:color(display-p3 .114 .435 .988/.085);--blue-a3:color(display-p3 .122 .463 1/.219);--blue-a4:color(display-p3 0 .467 1/.324);--blue-a5:color(display-p3 .098 .51 1/.4);--blue-a6:color(display-p3 .224 .557 1/.475);--blue-a7:color(display-p3 .294 .584 1/.572);--blue-a8:color(display-p3 .314 .592 1/.702);--blue-a9:color(display-p3 .251 .573 .996/.967);--blue-a10:color(display-p3 .357 .631 1/.971);--blue-a11:color(display-p3 .49 .72 1);--blue-a12:color(display-p3 .788 .898 .99)}}}:root,.light,.light-theme{--cyan-1:#fafdfe;--cyan-2:#f2fafb;--cyan-3:#def7f9;--cyan-4:#caf1f6;--cyan-5:#b5e9f0;--cyan-6:#9ddde7;--cyan-7:#7dcedc;--cyan-8:#3db9cf;--cyan-9:#00a2c7;--cyan-10:#0797b9;--cyan-11:#107d98;--cyan-12:#0d3c48}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--cyan-1:color(display-p3 .982 .992 .996);--cyan-2:color(display-p3 .955 .981 .984);--cyan-3:color(display-p3 .888 .965 .975);--cyan-4:color(display-p3 .821 .941 .959);--cyan-5:color(display-p3 .751 .907 .935);--cyan-6:color(display-p3 .671 .862 .9);--cyan-7:color(display-p3 .564 .8 .854);--cyan-8:color(display-p3 .388 .715 .798);--cyan-9:color(display-p3 .282 .627 .765);--cyan-10:color(display-p3 .264 .583 .71);--cyan-11:color(display-p3 .08 .48 .63);--cyan-12:color(display-p3 .108 .232 .277)}}}.dark,.dark-theme{--cyan-1:#0b161a;--cyan-2:#101b20;--cyan-3:#082c36;--cyan-4:#003848;--cyan-5:#004558;--cyan-6:#045468;--cyan-7:#12677e;--cyan-8:#11809c;--cyan-9:#00a2c7;--cyan-10:#23afd0;--cyan-11:#4ccce6;--cyan-12:#b6ecf7}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--cyan-1:color(display-p3 .053 .085 .098);--cyan-2:color(display-p3 .072 .105 .122);--cyan-3:color(display-p3 .073 .168 .209);--cyan-4:color(display-p3 .063 .216 .277);--cyan-5:color(display-p3 .091 .267 .336);--cyan-6:color(display-p3 .137 .324 .4);--cyan-7:color(display-p3 .186 .398 .484);--cyan-8:color(display-p3 .23 .496 .6);--cyan-9:color(display-p3 .282 .627 .765);--cyan-10:color(display-p3 .331 .675 .801);--cyan-11:color(display-p3 .446 .79 .887);--cyan-12:color(display-p3 .757 .919 .962)}}}:root,.light,.light-theme{--cyan-a1:#0099cc05;--cyan-a2:#009db10d;--cyan-a3:#00c2d121;--cyan-a4:#00bcd435;--cyan-a5:#01b4cc4a;--cyan-a6:#00a7c162;--cyan-a7:#009fbb82;--cyan-a8:#00a3c0c2;--cyan-a9:#00a2c7;--cyan-a10:#0094b7f8;--cyan-a11:#007491ef;--cyan-a12:#00323ef2}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--cyan-a1:color(display-p3 .02 .608 .804/.02);--cyan-a2:color(display-p3 .02 .557 .647/.044);--cyan-a3:color(display-p3 .004 .694 .796/.114);--cyan-a4:color(display-p3 .004 .678 .784/.181);--cyan-a5:color(display-p3 .004 .624 .733/.248);--cyan-a6:color(display-p3 .004 .584 .706/.33);--cyan-a7:color(display-p3 .004 .541 .667/.436);--cyan-a8:color(display-p3 0 .533 .667/.612);--cyan-a9:color(display-p3 0 .482 .675/.718);--cyan-a10:color(display-p3 0 .435 .608/.738);--cyan-a11:color(display-p3 .08 .48 .63);--cyan-a12:color(display-p3 .108 .232 .277)}}}.dark,.dark-theme{--cyan-a1:#0091f70a;--cyan-a2:#02a7f211;--cyan-a3:#00befd28;--cyan-a4:#00baff3b;--cyan-a5:#00befd4d;--cyan-a6:#00c7fd5e;--cyan-a7:#14cdff75;--cyan-a8:#11cfff95;--cyan-a9:#00cfffc3;--cyan-a10:#28d6ffcd;--cyan-a11:#52e1fee5;--cyan-a12:#bbf3fef7}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--cyan-a1:color(display-p3 0 .647 .992/.034);--cyan-a2:color(display-p3 .133 .733 1/.059);--cyan-a3:color(display-p3 .122 .741 .996/.152);--cyan-a4:color(display-p3 .051 .725 1/.227);--cyan-a5:color(display-p3 .149 .757 1/.29);--cyan-a6:color(display-p3 .267 .792 1/.358);--cyan-a7:color(display-p3 .333 .808 1/.446);--cyan-a8:color(display-p3 .357 .816 1/.572);--cyan-a9:color(display-p3 .357 .82 1/.748);--cyan-a10:color(display-p3 .4 .839 1/.786);--cyan-a11:color(display-p3 .446 .79 .887);--cyan-a12:color(display-p3 .757 .919 .962)}}}:root,.light,.light-theme{--teal-1:#fafefd;--teal-2:#f3fbf9;--teal-3:#e0f8f3;--teal-4:#ccf3ea;--teal-5:#b8eae0;--teal-6:#a1ded2;--teal-7:#83cdc1;--teal-8:#53b9ab;--teal-9:#12a594;--teal-10:#0d9b8a;--teal-11:#008573;--teal-12:#0d3d38}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--teal-1:color(display-p3 .983 .996 .992);--teal-2:color(display-p3 .958 .983 .976);--teal-3:color(display-p3 .895 .971 .952);--teal-4:color(display-p3 .831 .949 .92);--teal-5:color(display-p3 .761 .914 .878);--teal-6:color(display-p3 .682 .864 .825);--teal-7:color(display-p3 .581 .798 .756);--teal-8:color(display-p3 .433 .716 .671);--teal-9:color(display-p3 .297 .637 .581);--teal-10:color(display-p3 .275 .599 .542);--teal-11:color(display-p3 .08 .5 .43);--teal-12:color(display-p3 .11 .235 .219)}}}.dark,.dark-theme{--teal-1:#0d1514;--teal-2:#111c1b;--teal-3:#0d2d2a;--teal-4:#023b37;--teal-5:#084843;--teal-6:#145750;--teal-7:#1c6961;--teal-8:#207e73;--teal-9:#12a594;--teal-10:#0eb39e;--teal-11:#0bd8b6;--teal-12:#adf0dd}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--teal-1:color(display-p3 .059 .083 .079);--teal-2:color(display-p3 .075 .11 .107);--teal-3:color(display-p3 .087 .175 .165);--teal-4:color(display-p3 .087 .227 .214);--teal-5:color(display-p3 .12 .277 .261);--teal-6:color(display-p3 .162 .335 .314);--teal-7:color(display-p3 .205 .406 .379);--teal-8:color(display-p3 .245 .489 .453);--teal-9:color(display-p3 .297 .637 .581);--teal-10:color(display-p3 .319 .69 .62);--teal-11:color(display-p3 .388 .835 .719);--teal-12:color(display-p3 .734 .934 .87)}}}:root,.light,.light-theme{--teal-a1:#00cc9905;--teal-a2:#00aa800c;--teal-a3:#00c69d1f;--teal-a4:#00c39633;--teal-a5:#00b49047;--teal-a6:#00a6855e;--teal-a7:#0099807c;--teal-a8:#009783ac;--teal-a9:#009e8ced;--teal-a10:#009684f2;--teal-a11:#008573;--teal-a12:#00332df2}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--teal-a1:color(display-p3 .024 .757 .514/.016);--teal-a2:color(display-p3 .02 .647 .467/.044);--teal-a3:color(display-p3 .004 .741 .557/.106);--teal-a4:color(display-p3 .004 .702 .537/.169);--teal-a5:color(display-p3 .004 .643 .494/.24);--teal-a6:color(display-p3 .004 .569 .447/.318);--teal-a7:color(display-p3 .004 .518 .424/.42);--teal-a8:color(display-p3 0 .506 .424/.569);--teal-a9:color(display-p3 0 .482 .404/.702);--teal-a10:color(display-p3 0 .451 .369/.726);--teal-a11:color(display-p3 .08 .5 .43);--teal-a12:color(display-p3 .11 .235 .219)}}}.dark,.dark-theme{--teal-a1:#00deab05;--teal-a2:#12fbe60c;--teal-a3:#00ffe61e;--teal-a4:#00ffe92d;--teal-a5:#00ffea3b;--teal-a6:#1cffe84b;--teal-a7:#2efde85f;--teal-a8:#32ffe775;--teal-a9:#13ffe49f;--teal-a10:#0dffe0ae;--teal-a11:#0afed5d6;--teal-a12:#b8ffebef}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--teal-a1:color(display-p3 0 .992 .761/.017);--teal-a2:color(display-p3 .235 .988 .902/.047);--teal-a3:color(display-p3 .235 1 .898/.118);--teal-a4:color(display-p3 .18 .996 .929/.173);--teal-a5:color(display-p3 .31 1 .933/.227);--teal-a6:color(display-p3 .396 1 .933/.286);--teal-a7:color(display-p3 .443 1 .925/.366);--teal-a8:color(display-p3 .459 1 .925/.454);--teal-a9:color(display-p3 .443 .996 .906/.61);--teal-a10:color(display-p3 .439 .996 .89/.669);--teal-a11:color(display-p3 .388 .835 .719);--teal-a12:color(display-p3 .734 .934 .87)}}}:root,.light,.light-theme{--jade-1:#fbfefd;--jade-2:#f4fbf7;--jade-3:#e6f7ed;--jade-4:#d6f1e3;--jade-5:#c3e9d7;--jade-6:#acdec8;--jade-7:#8bceb6;--jade-8:#56ba9f;--jade-9:#29a383;--jade-10:#26997b;--jade-11:#208368;--jade-12:#1d3b31}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--jade-1:color(display-p3 .986 .996 .992);--jade-2:color(display-p3 .962 .983 .969);--jade-3:color(display-p3 .912 .965 .932);--jade-4:color(display-p3 .858 .941 .893);--jade-5:color(display-p3 .795 .909 .847);--jade-6:color(display-p3 .715 .864 .791);--jade-7:color(display-p3 .603 .802 .718);--jade-8:color(display-p3 .44 .72 .629);--jade-9:color(display-p3 .319 .63 .521);--jade-10:color(display-p3 .299 .592 .488);--jade-11:color(display-p3 .15 .5 .37);--jade-12:color(display-p3 .142 .229 .194)}}}.dark,.dark-theme{--jade-1:#0d1512;--jade-2:#121c18;--jade-3:#0f2e22;--jade-4:#0b3b2c;--jade-5:#114837;--jade-6:#1b5745;--jade-7:#246854;--jade-8:#2a7e68;--jade-9:#29a383;--jade-10:#27b08b;--jade-11:#1fd8a4;--jade-12:#adf0d4}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--jade-1:color(display-p3 .059 .083 .071);--jade-2:color(display-p3 .078 .11 .094);--jade-3:color(display-p3 .091 .176 .138);--jade-4:color(display-p3 .102 .228 .177);--jade-5:color(display-p3 .133 .279 .221);--jade-6:color(display-p3 .174 .334 .273);--jade-7:color(display-p3 .219 .402 .335);--jade-8:color(display-p3 .263 .488 .411);--jade-9:color(display-p3 .319 .63 .521);--jade-10:color(display-p3 .338 .68 .555);--jade-11:color(display-p3 .4 .835 .656);--jade-12:color(display-p3 .734 .934 .838)}}}:root,.light,.light-theme{--jade-a1:#00c08004;--jade-a2:#00a3460b;--jade-a3:#00ae4819;--jade-a4:#00a85129;--jade-a5:#00a2553c;--jade-a6:#009a5753;--jade-a7:#00945f74;--jade-a8:#00976ea9;--jade-a9:#00916bd6;--jade-a10:#008764d9;--jade-a11:#007152df;--jade-a12:#002217e2}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--jade-a1:color(display-p3 .024 .757 .514/.016);--jade-a2:color(display-p3 .024 .612 .22/.04);--jade-a3:color(display-p3 .012 .596 .235/.087);--jade-a4:color(display-p3 .008 .588 .255/.142);--jade-a5:color(display-p3 .004 .561 .251/.204);--jade-a6:color(display-p3 .004 .525 .278/.287);--jade-a7:color(display-p3 .004 .506 .29/.397);--jade-a8:color(display-p3 0 .506 .337/.561);--jade-a9:color(display-p3 0 .459 .298/.683);--jade-a10:color(display-p3 0 .42 .271/.702);--jade-a11:color(display-p3 .15 .5 .37);--jade-a12:color(display-p3 .142 .229 .194)}}}.dark,.dark-theme{--jade-a1:#00de4505;--jade-a2:#27fba60c;--jade-a3:#02f99920;--jade-a4:#00ffaa2d;--jade-a5:#11ffb63b;--jade-a6:#34ffc24b;--jade-a7:#45fdc75e;--jade-a8:#48ffcf75;--jade-a9:#38feca9d;--jade-a10:#31fec7ab;--jade-a11:#21fec0d6;--jade-a12:#b8ffe1ef}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--jade-a1:color(display-p3 0 .992 .298/.017);--jade-a2:color(display-p3 .318 .988 .651/.047);--jade-a3:color(display-p3 .267 1 .667/.118);--jade-a4:color(display-p3 .275 .996 .702/.173);--jade-a5:color(display-p3 .361 1 .741/.227);--jade-a6:color(display-p3 .439 1 .796/.286);--jade-a7:color(display-p3 .49 1 .804/.362);--jade-a8:color(display-p3 .506 1 .835/.45);--jade-a9:color(display-p3 .478 .996 .816/.606);--jade-a10:color(display-p3 .478 1 .816/.656);--jade-a11:color(display-p3 .4 .835 .656);--jade-a12:color(display-p3 .734 .934 .838)}}}:root,.light,.light-theme{--green-1:#fbfefc;--green-2:#f4fbf6;--green-3:#e6f6eb;--green-4:#d6f1df;--green-5:#c4e8d1;--green-6:#adddc0;--green-7:#8eceaa;--green-8:#5bb98b;--green-9:#30a46c;--green-10:#2b9a66;--green-11:#218358;--green-12:#193b2d}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--green-1:color(display-p3 .986 .996 .989);--green-2:color(display-p3 .963 .983 .967);--green-3:color(display-p3 .913 .964 .925);--green-4:color(display-p3 .859 .94 .879);--green-5:color(display-p3 .796 .907 .826);--green-6:color(display-p3 .718 .863 .761);--green-7:color(display-p3 .61 .801 .675);--green-8:color(display-p3 .451 .715 .559);--green-9:color(display-p3 .332 .634 .442);--green-10:color(display-p3 .308 .595 .417);--green-11:color(display-p3 .19 .5 .32);--green-12:color(display-p3 .132 .228 .18)}}}.dark,.dark-theme{--green-1:#0e1512;--green-2:#121b17;--green-3:#132d21;--green-4:#113b29;--green-5:#174933;--green-6:#20573e;--green-7:#28684a;--green-8:#2f7c57;--green-9:#30a46c;--green-10:#33b074;--green-11:#3dd68c;--green-12:#b1f1cb}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--green-1:color(display-p3 .062 .083 .071);--green-2:color(display-p3 .079 .106 .09);--green-3:color(display-p3 .1 .173 .133);--green-4:color(display-p3 .115 .229 .166);--green-5:color(display-p3 .147 .282 .206);--green-6:color(display-p3 .185 .338 .25);--green-7:color(display-p3 .227 .403 .298);--green-8:color(display-p3 .27 .479 .351);--green-9:color(display-p3 .332 .634 .442);--green-10:color(display-p3 .357 .682 .474);--green-11:color(display-p3 .434 .828 .573);--green-12:color(display-p3 .747 .938 .807)}}}:root,.light,.light-theme{--green-a1:#00c04004;--green-a2:#00a32f0b;--green-a3:#00a43319;--green-a4:#00a83829;--green-a5:#019c393b;--green-a6:#00963c52;--green-a7:#00914071;--green-a8:#00924ba4;--green-a9:#008f4acf;--green-a10:#008647d4;--green-a11:#00713fde;--green-a12:#002616e6}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--green-a1:color(display-p3 .024 .757 .267/.016);--green-a2:color(display-p3 .024 .565 .129/.036);--green-a3:color(display-p3 .012 .596 .145/.087);--green-a4:color(display-p3 .008 .588 .145/.142);--green-a5:color(display-p3 .004 .541 .157/.204);--green-a6:color(display-p3 .004 .518 .157/.283);--green-a7:color(display-p3 .004 .486 .165/.389);--green-a8:color(display-p3 0 .478 .2/.55);--green-a9:color(display-p3 0 .455 .165/.667);--green-a10:color(display-p3 0 .416 .153/.691);--green-a11:color(display-p3 .19 .5 .32);--green-a12:color(display-p3 .132 .228 .18)}}}.dark,.dark-theme{--green-a1:#00de4505;--green-a2:#29f99d0b;--green-a3:#22ff991e;--green-a4:#11ff992d;--green-a5:#2bffa23c;--green-a6:#44ffaa4b;--green-a7:#50fdac5e;--green-a8:#54ffad73;--green-a9:#44ffa49e;--green-a10:#43fea4ab;--green-a11:#46fea5d4;--green-a12:#bbffd7f0}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--green-a1:color(display-p3 0 .992 .298/.017);--green-a2:color(display-p3 .341 .98 .616/.043);--green-a3:color(display-p3 .376 .996 .655/.114);--green-a4:color(display-p3 .341 .996 .635/.173);--green-a5:color(display-p3 .408 1 .678/.232);--green-a6:color(display-p3 .475 1 .706/.29);--green-a7:color(display-p3 .514 1 .706/.362);--green-a8:color(display-p3 .529 1 .718/.442);--green-a9:color(display-p3 .502 .996 .682/.61);--green-a10:color(display-p3 .506 1 .682/.66);--green-a11:color(display-p3 .434 .828 .573);--green-a12:color(display-p3 .747 .938 .807)}}}:root,.light,.light-theme{--grass-1:#fbfefb;--grass-2:#f5fbf5;--grass-3:#e9f6e9;--grass-4:#daf1db;--grass-5:#c9e8ca;--grass-6:#b2ddb5;--grass-7:#94ce9a;--grass-8:#65ba74;--grass-9:#46a758;--grass-10:#3e9b4f;--grass-11:#2a7e3b;--grass-12:#203c25}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--grass-1:color(display-p3 .986 .996 .985);--grass-2:color(display-p3 .966 .983 .964);--grass-3:color(display-p3 .923 .965 .917);--grass-4:color(display-p3 .872 .94 .865);--grass-5:color(display-p3 .811 .908 .802);--grass-6:color(display-p3 .733 .864 .724);--grass-7:color(display-p3 .628 .803 .622);--grass-8:color(display-p3 .477 .72 .482);--grass-9:color(display-p3 .38 .647 .378);--grass-10:color(display-p3 .344 .598 .342);--grass-11:color(display-p3 .263 .488 .261);--grass-12:color(display-p3 .151 .233 .153)}}}.dark,.dark-theme{--grass-1:#0e1511;--grass-2:#141a15;--grass-3:#1b2a1e;--grass-4:#1d3a24;--grass-5:#25482d;--grass-6:#2d5736;--grass-7:#366740;--grass-8:#3e7949;--grass-9:#46a758;--grass-10:#53b365;--grass-11:#71d083;--grass-12:#c2f0c2}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--grass-1:color(display-p3 .062 .083 .067);--grass-2:color(display-p3 .083 .103 .085);--grass-3:color(display-p3 .118 .163 .122);--grass-4:color(display-p3 .142 .225 .15);--grass-5:color(display-p3 .178 .279 .186);--grass-6:color(display-p3 .217 .337 .224);--grass-7:color(display-p3 .258 .4 .264);--grass-8:color(display-p3 .302 .47 .305);--grass-9:color(display-p3 .38 .647 .378);--grass-10:color(display-p3 .426 .694 .426);--grass-11:color(display-p3 .535 .807 .542);--grass-12:color(display-p3 .797 .936 .776)}}}:root,.light,.light-theme{--grass-a1:#00c00004;--grass-a2:#0099000a;--grass-a3:#00970016;--grass-a4:#009f0725;--grass-a5:#00930536;--grass-a6:#008f0a4d;--grass-a7:#018b0f6b;--grass-a8:#008d199a;--grass-a9:#008619b9;--grass-a10:#007b17c1;--grass-a11:#006514d5;--grass-a12:#002006df}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--grass-a1:color(display-p3 .024 .757 .024/.016);--grass-a2:color(display-p3 .024 .565 .024/.036);--grass-a3:color(display-p3 .059 .576 .008/.083);--grass-a4:color(display-p3 .035 .565 .008/.134);--grass-a5:color(display-p3 .047 .545 .008/.197);--grass-a6:color(display-p3 .031 .502 .004/.275);--grass-a7:color(display-p3 .012 .482 .004/.377);--grass-a8:color(display-p3 0 .467 .008/.522);--grass-a9:color(display-p3 .008 .435 0/.624);--grass-a10:color(display-p3 .008 .388 0/.659);--grass-a11:color(display-p3 .263 .488 .261);--grass-a12:color(display-p3 .151 .233 .153)}}}.dark,.dark-theme{--grass-a1:#00de1205;--grass-a2:#5ef7780a;--grass-a3:#70fe8c1b;--grass-a4:#57ff802c;--grass-a5:#68ff8b3b;--grass-a6:#71ff8f4b;--grass-a7:#77fd925d;--grass-a8:#77fd9070;--grass-a9:#65ff82a1;--grass-a10:#72ff8dae;--grass-a11:#89ff9fcd;--grass-a12:#ceffceef}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--grass-a1:color(display-p3 0 .992 .071/.017);--grass-a2:color(display-p3 .482 .996 .584/.038);--grass-a3:color(display-p3 .549 .992 .588/.106);--grass-a4:color(display-p3 .51 .996 .557/.169);--grass-a5:color(display-p3 .553 1 .588/.227);--grass-a6:color(display-p3 .584 1 .608/.29);--grass-a7:color(display-p3 .604 1 .616/.358);--grass-a8:color(display-p3 .608 1 .62/.433);--grass-a9:color(display-p3 .573 1 .569/.622);--grass-a10:color(display-p3 .6 .996 .6/.673);--grass-a11:color(display-p3 .535 .807 .542);--grass-a12:color(display-p3 .797 .936 .776)}}}:root,.light,.light-theme{--orange-1:#fefcfb;--orange-2:#fff7ed;--orange-3:#ffefd6;--orange-4:#ffdfb5;--orange-5:#ffd19a;--orange-6:#ffc182;--orange-7:#f5ae73;--orange-8:#ec9455;--orange-9:#f76b15;--orange-10:#ef5f00;--orange-11:#cc4e00;--orange-12:#582d1d}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--orange-1:color(display-p3 .995 .988 .985);--orange-2:color(display-p3 .994 .968 .934);--orange-3:color(display-p3 .989 .938 .85);--orange-4:color(display-p3 1 .874 .687);--orange-5:color(display-p3 1 .821 .583);--orange-6:color(display-p3 .975 .767 .545);--orange-7:color(display-p3 .919 .693 .486);--orange-8:color(display-p3 .877 .597 .379);--orange-9:color(display-p3 .9 .45 .2);--orange-10:color(display-p3 .87 .409 .164);--orange-11:color(display-p3 .76 .34 0);--orange-12:color(display-p3 .323 .185 .127)}}}.dark,.dark-theme{--orange-1:#17120e;--orange-2:#1e160f;--orange-3:#331e0b;--orange-4:#462100;--orange-5:#562800;--orange-6:#66350c;--orange-7:#7e451d;--orange-8:#a35829;--orange-9:#f76b15;--orange-10:#ff801f;--orange-11:#ffa057;--orange-12:#ffe0c2}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--orange-1:color(display-p3 .088 .07 .057);--orange-2:color(display-p3 .113 .089 .061);--orange-3:color(display-p3 .189 .12 .056);--orange-4:color(display-p3 .262 .132 0);--orange-5:color(display-p3 .315 .168 .016);--orange-6:color(display-p3 .376 .219 .088);--orange-7:color(display-p3 .465 .283 .147);--orange-8:color(display-p3 .601 .359 .201);--orange-9:color(display-p3 .9 .45 .2);--orange-10:color(display-p3 .98 .51 .23);--orange-11:color(display-p3 1 .63 .38);--orange-12:color(display-p3 .98 .883 .775)}}}:root,.light,.light-theme{--orange-a1:#c0400004;--orange-a2:#ff8e0012;--orange-a3:#ff9c0029;--orange-a4:#ff91014a;--orange-a5:#ff8b0065;--orange-a6:#ff81007d;--orange-a7:#ed6c008c;--orange-a8:#e35f00aa;--orange-a9:#f65e00ea;--orange-a10:#ef5f00;--orange-a11:#cc4e00;--orange-a12:#431200e2}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--orange-a1:color(display-p3 .757 .267 .024/.016);--orange-a2:color(display-p3 .886 .533 .008/.067);--orange-a3:color(display-p3 .922 .584 .008/.15);--orange-a4:color(display-p3 1 .604 .004/.314);--orange-a5:color(display-p3 1 .569 .004/.416);--orange-a6:color(display-p3 .949 .494 .004/.455);--orange-a7:color(display-p3 .839 .408 0/.514);--orange-a8:color(display-p3 .804 .349 0/.62);--orange-a9:color(display-p3 .878 .314 0/.8);--orange-a10:color(display-p3 .843 .29 0/.836);--orange-a11:color(display-p3 .76 .34 0);--orange-a12:color(display-p3 .323 .185 .127)}}}.dark,.dark-theme{--orange-a1:#ec360007;--orange-a2:#fe6d000e;--orange-a3:#fb6a0025;--orange-a4:#ff590039;--orange-a5:#ff61004a;--orange-a6:#fd75045c;--orange-a7:#ff832c75;--orange-a8:#fe84389d;--orange-a9:#fe6d15f7;--orange-a10:#ff801f;--orange-a11:#ffa057;--orange-a12:#ffe0c2}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--orange-a1:color(display-p3 .961 .247 0/.022);--orange-a2:color(display-p3 .992 .529 0/.051);--orange-a3:color(display-p3 .996 .486 0/.131);--orange-a4:color(display-p3 .996 .384 0/.211);--orange-a5:color(display-p3 1 .455 0/.265);--orange-a6:color(display-p3 1 .529 .129/.332);--orange-a7:color(display-p3 1 .569 .251/.429);--orange-a8:color(display-p3 1 .584 .302/.572);--orange-a9:color(display-p3 1 .494 .216/.895);--orange-a10:color(display-p3 1 .522 .235/.979);--orange-a11:color(display-p3 1 .63 .38);--orange-a12:color(display-p3 .98 .883 .775)}}}:root,.light,.light-theme{--brown-1:#fefdfc;--brown-2:#fcf9f6;--brown-3:#f6eee7;--brown-4:#f0e4d9;--brown-5:#ebdaca;--brown-6:#e4cdb7;--brown-7:#dcbc9f;--brown-8:#cea37e;--brown-9:#ad7f58;--brown-10:#a07553;--brown-11:#815e46;--brown-12:#3e332e}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--brown-1:color(display-p3 .995 .992 .989);--brown-2:color(display-p3 .987 .976 .964);--brown-3:color(display-p3 .959 .936 .909);--brown-4:color(display-p3 .934 .897 .855);--brown-5:color(display-p3 .909 .856 .798);--brown-6:color(display-p3 .88 .808 .73);--brown-7:color(display-p3 .841 .742 .639);--brown-8:color(display-p3 .782 .647 .514);--brown-9:color(display-p3 .651 .505 .368);--brown-10:color(display-p3 .601 .465 .344);--brown-11:color(display-p3 .485 .374 .288);--brown-12:color(display-p3 .236 .202 .183)}}}.dark,.dark-theme{--brown-1:#12110f;--brown-2:#1c1816;--brown-3:#28211d;--brown-4:#322922;--brown-5:#3e3128;--brown-6:#4d3c2f;--brown-7:#614a39;--brown-8:#7c5f46;--brown-9:#ad7f58;--brown-10:#b88c67;--brown-11:#dbb594;--brown-12:#f2e1ca}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--brown-1:color(display-p3 .071 .067 .059);--brown-2:color(display-p3 .107 .095 .087);--brown-3:color(display-p3 .151 .13 .115);--brown-4:color(display-p3 .191 .161 .138);--brown-5:color(display-p3 .235 .194 .162);--brown-6:color(display-p3 .291 .237 .192);--brown-7:color(display-p3 .365 .295 .232);--brown-8:color(display-p3 .469 .377 .287);--brown-9:color(display-p3 .651 .505 .368);--brown-10:color(display-p3 .697 .557 .423);--brown-11:color(display-p3 .835 .715 .597);--brown-12:color(display-p3 .938 .885 .802)}}}:root,.light,.light-theme{--brown-a1:#aa550003;--brown-a2:#aa550009;--brown-a3:#a04b0018;--brown-a4:#9b4a0026;--brown-a5:#9f4d0035;--brown-a6:#a04e0048;--brown-a7:#a34e0060;--brown-a8:#9f4a0081;--brown-a9:#823c00a7;--brown-a10:#723300ac;--brown-a11:#522100b9;--brown-a12:#140600d1}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--brown-a1:color(display-p3 .675 .349 .024/.012);--brown-a2:color(display-p3 .675 .349 .024/.036);--brown-a3:color(display-p3 .573 .314 .012/.091);--brown-a4:color(display-p3 .545 .302 .008/.146);--brown-a5:color(display-p3 .561 .29 .004/.204);--brown-a6:color(display-p3 .553 .294 .004/.271);--brown-a7:color(display-p3 .557 .286 .004/.361);--brown-a8:color(display-p3 .549 .275 .004/.487);--brown-a9:color(display-p3 .447 .22 0/.632);--brown-a10:color(display-p3 .388 .188 0/.655);--brown-a11:color(display-p3 .485 .374 .288);--brown-a12:color(display-p3 .236 .202 .183)}}}.dark,.dark-theme{--brown-a1:#91110002;--brown-a2:#fba67c0c;--brown-a3:#fcb58c19;--brown-a4:#fbbb8a24;--brown-a5:#fcb88931;--brown-a6:#fdba8741;--brown-a7:#ffbb8856;--brown-a8:#ffbe8773;--brown-a9:#feb87da8;--brown-a10:#ffc18cb3;--brown-a11:#fed1aad9;--brown-a12:#feecd4f2}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--brown-a1:color(display-p3 .855 .071 0/.005);--brown-a2:color(display-p3 .98 .706 .525/.043);--brown-a3:color(display-p3 .996 .745 .576/.093);--brown-a4:color(display-p3 1 .765 .592/.135);--brown-a5:color(display-p3 1 .761 .588/.181);--brown-a6:color(display-p3 1 .773 .592/.24);--brown-a7:color(display-p3 .996 .776 .58/.32);--brown-a8:color(display-p3 1 .78 .573/.433);--brown-a9:color(display-p3 1 .769 .549/.627);--brown-a10:color(display-p3 1 .792 .596/.677);--brown-a11:color(display-p3 .835 .715 .597);--brown-a12:color(display-p3 .938 .885 .802)}}}:root,.light,.light-theme{--sky-1:#f9feff;--sky-2:#f1fafd;--sky-3:#e1f6fd;--sky-4:#d1f0fa;--sky-5:#bee7f5;--sky-6:#a9daed;--sky-7:#8dcae3;--sky-8:#60b3d7;--sky-9:#7ce2fe;--sky-10:#74daf8;--sky-11:#00749e;--sky-12:#1d3e56}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--sky-1:color(display-p3 .98 .995 .999);--sky-2:color(display-p3 .953 .98 .99);--sky-3:color(display-p3 .899 .963 .989);--sky-4:color(display-p3 .842 .937 .977);--sky-5:color(display-p3 .777 .9 .954);--sky-6:color(display-p3 .701 .851 .921);--sky-7:color(display-p3 .604 .785 .879);--sky-8:color(display-p3 .457 .696 .829);--sky-9:color(display-p3 .585 .877 .983);--sky-10:color(display-p3 .555 .845 .959);--sky-11:color(display-p3 .193 .448 .605);--sky-12:color(display-p3 .145 .241 .329)}}}.dark,.dark-theme{--sky-1:#0d141f;--sky-2:#111a27;--sky-3:#112840;--sky-4:#113555;--sky-5:#154467;--sky-6:#1b537b;--sky-7:#1f6692;--sky-8:#197cae;--sky-9:#7ce2fe;--sky-10:#a8eeff;--sky-11:#75c7f0;--sky-12:#c2f3ff}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--sky-1:color(display-p3 .056 .078 .116);--sky-2:color(display-p3 .075 .101 .149);--sky-3:color(display-p3 .089 .154 .244);--sky-4:color(display-p3 .106 .207 .323);--sky-5:color(display-p3 .135 .261 .394);--sky-6:color(display-p3 .17 .322 .469);--sky-7:color(display-p3 .205 .394 .557);--sky-8:color(display-p3 .232 .48 .665);--sky-9:color(display-p3 .585 .877 .983);--sky-10:color(display-p3 .718 .925 .991);--sky-11:color(display-p3 .536 .772 .924);--sky-12:color(display-p3 .799 .947 .993)}}}:root,.light,.light-theme{--sky-a1:#00d5ff06;--sky-a2:#00a4db0e;--sky-a3:#00b3ee1e;--sky-a4:#00ace42e;--sky-a5:#00a1d841;--sky-a6:#0092ca56;--sky-a7:#0089c172;--sky-a8:#0085bf9f;--sky-a9:#00c7fe83;--sky-a10:#00bcf38b;--sky-a11:#00749e;--sky-a12:#002540e2}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--sky-a1:color(display-p3 .02 .804 1/.02);--sky-a2:color(display-p3 .024 .592 .757/.048);--sky-a3:color(display-p3 .004 .655 .886/.102);--sky-a4:color(display-p3 .004 .604 .851/.157);--sky-a5:color(display-p3 .004 .565 .792/.224);--sky-a6:color(display-p3 .004 .502 .737/.299);--sky-a7:color(display-p3 .004 .459 .694/.397);--sky-a8:color(display-p3 0 .435 .682/.542);--sky-a9:color(display-p3 .004 .71 .965/.416);--sky-a10:color(display-p3 .004 .647 .914/.444);--sky-a11:color(display-p3 .193 .448 .605);--sky-a12:color(display-p3 .145 .241 .329)}}}.dark,.dark-theme{--sky-a1:#0044ff0f;--sky-a2:#1171fb18;--sky-a3:#1184fc33;--sky-a4:#128fff49;--sky-a5:#1c9dfd5d;--sky-a6:#28a5ff72;--sky-a7:#2badfe8b;--sky-a8:#1db2fea9;--sky-a9:#7ce3fffe;--sky-a10:#a8eeff;--sky-a11:#7cd3ffef;--sky-a12:#c2f3ff}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--sky-a1:color(display-p3 0 .282 .996/.055);--sky-a2:color(display-p3 .157 .467 .992/.089);--sky-a3:color(display-p3 .192 .522 .996/.19);--sky-a4:color(display-p3 .212 .584 1/.274);--sky-a5:color(display-p3 .259 .631 1/.349);--sky-a6:color(display-p3 .302 .655 1/.433);--sky-a7:color(display-p3 .329 .686 1/.526);--sky-a8:color(display-p3 .325 .71 1/.643);--sky-a9:color(display-p3 .592 .894 1/.984);--sky-a10:color(display-p3 .722 .933 1/.992);--sky-a11:color(display-p3 .536 .772 .924);--sky-a12:color(display-p3 .799 .947 .993)}}}:root,.light,.light-theme{--mint-1:#f9fefd;--mint-2:#f2fbf9;--mint-3:#ddf9f2;--mint-4:#c8f4e9;--mint-5:#b3ecde;--mint-6:#9ce0d0;--mint-7:#7ecfbd;--mint-8:#4cbba5;--mint-9:#86ead4;--mint-10:#7de0cb;--mint-11:#027864;--mint-12:#16433c}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--mint-1:color(display-p3 .98 .995 .992);--mint-2:color(display-p3 .957 .985 .977);--mint-3:color(display-p3 .888 .972 .95);--mint-4:color(display-p3 .819 .951 .916);--mint-5:color(display-p3 .747 .918 .873);--mint-6:color(display-p3 .668 .87 .818);--mint-7:color(display-p3 .567 .805 .744);--mint-8:color(display-p3 .42 .724 .649);--mint-9:color(display-p3 .62 .908 .834);--mint-10:color(display-p3 .585 .871 .797);--mint-11:color(display-p3 .203 .463 .397);--mint-12:color(display-p3 .136 .259 .236)}}}.dark,.dark-theme{--mint-1:#0e1515;--mint-2:#0f1b1b;--mint-3:#092c2b;--mint-4:#003a38;--mint-5:#004744;--mint-6:#105650;--mint-7:#1e685f;--mint-8:#277f70;--mint-9:#86ead4;--mint-10:#a8f5e5;--mint-11:#58d5ba;--mint-12:#c4f5e1}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--mint-1:color(display-p3 .059 .082 .081);--mint-2:color(display-p3 .068 .104 .105);--mint-3:color(display-p3 .077 .17 .168);--mint-4:color(display-p3 .068 .224 .22);--mint-5:color(display-p3 .104 .275 .264);--mint-6:color(display-p3 .154 .332 .313);--mint-7:color(display-p3 .207 .403 .373);--mint-8:color(display-p3 .258 .49 .441);--mint-9:color(display-p3 .62 .908 .834);--mint-10:color(display-p3 .725 .954 .898);--mint-11:color(display-p3 .482 .825 .733);--mint-12:color(display-p3 .807 .955 .887)}}}:root,.light,.light-theme{--mint-a1:#00d5aa06;--mint-a2:#00b18a0d;--mint-a3:#00d29e22;--mint-a4:#00cc9937;--mint-a5:#00c0914c;--mint-a6:#00b08663;--mint-a7:#00a17d81;--mint-a8:#009e7fb3;--mint-a9:#00d3a579;--mint-a10:#00c39982;--mint-a11:#007763fd;--mint-a12:#00312ae9}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--mint-a1:color(display-p3 .02 .804 .608/.02);--mint-a2:color(display-p3 .02 .647 .467/.044);--mint-a3:color(display-p3 .004 .761 .553/.114);--mint-a4:color(display-p3 .004 .741 .545/.181);--mint-a5:color(display-p3 .004 .678 .51/.255);--mint-a6:color(display-p3 .004 .616 .463/.334);--mint-a7:color(display-p3 .004 .549 .412/.432);--mint-a8:color(display-p3 0 .529 .392/.581);--mint-a9:color(display-p3 .004 .765 .569/.381);--mint-a10:color(display-p3 .004 .69 .51/.416);--mint-a11:color(display-p3 .203 .463 .397);--mint-a12:color(display-p3 .136 .259 .236)}}}.dark,.dark-theme{--mint-a1:#00dede05;--mint-a2:#00f9f90b;--mint-a3:#00fff61d;--mint-a4:#00fff42c;--mint-a5:#00fff23a;--mint-a6:#0effeb4a;--mint-a7:#34fde55e;--mint-a8:#41ffdf76;--mint-a9:#92ffe7e9;--mint-a10:#aefeedf5;--mint-a11:#67ffded2;--mint-a12:#cbfee9f5}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--mint-a1:color(display-p3 0 .992 .992/.017);--mint-a2:color(display-p3 .071 .98 .98/.043);--mint-a3:color(display-p3 .176 .996 .996/.11);--mint-a4:color(display-p3 .071 .996 .973/.169);--mint-a5:color(display-p3 .243 1 .949/.223);--mint-a6:color(display-p3 .369 1 .933/.286);--mint-a7:color(display-p3 .459 1 .914/.362);--mint-a8:color(display-p3 .49 1 .89/.454);--mint-a9:color(display-p3 .678 .996 .914/.904);--mint-a10:color(display-p3 .761 1 .941/.95);--mint-a11:color(display-p3 .482 .825 .733);--mint-a12:color(display-p3 .807 .955 .887)}}}:root,.light,.light-theme{--lime-1:#fcfdfa;--lime-2:#f8faf3;--lime-3:#eef6d6;--lime-4:#e2f0bd;--lime-5:#d3e7a6;--lime-6:#c2da91;--lime-7:#abc978;--lime-8:#8db654;--lime-9:#bdee63;--lime-10:#b0e64c;--lime-11:#5c7c2f;--lime-12:#37401c}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--lime-1:color(display-p3 .989 .992 .981);--lime-2:color(display-p3 .975 .98 .954);--lime-3:color(display-p3 .939 .965 .851);--lime-4:color(display-p3 .896 .94 .76);--lime-5:color(display-p3 .843 .903 .678);--lime-6:color(display-p3 .778 .852 .599);--lime-7:color(display-p3 .694 .784 .508);--lime-8:color(display-p3 .585 .707 .378);--lime-9:color(display-p3 .78 .928 .466);--lime-10:color(display-p3 .734 .896 .397);--lime-11:color(display-p3 .386 .482 .227);--lime-12:color(display-p3 .222 .25 .128)}}}.dark,.dark-theme{--lime-1:#11130c;--lime-2:#151a10;--lime-3:#1f2917;--lime-4:#29371d;--lime-5:#334423;--lime-6:#3d522a;--lime-7:#496231;--lime-8:#577538;--lime-9:#bdee63;--lime-10:#d4ff70;--lime-11:#bde56c;--lime-12:#e3f7ba}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--lime-1:color(display-p3 .067 .073 .048);--lime-2:color(display-p3 .086 .1 .067);--lime-3:color(display-p3 .13 .16 .099);--lime-4:color(display-p3 .172 .214 .126);--lime-5:color(display-p3 .213 .266 .153);--lime-6:color(display-p3 .257 .321 .182);--lime-7:color(display-p3 .307 .383 .215);--lime-8:color(display-p3 .365 .456 .25);--lime-9:color(display-p3 .78 .928 .466);--lime-10:color(display-p3 .865 .995 .519);--lime-11:color(display-p3 .771 .893 .485);--lime-12:color(display-p3 .905 .966 .753)}}}:root,.light,.light-theme{--lime-a1:#66990005;--lime-a2:#6b95000c;--lime-a3:#96c80029;--lime-a4:#8fc60042;--lime-a5:#81bb0059;--lime-a6:#72aa006e;--lime-a7:#61990087;--lime-a8:#559200ab;--lime-a9:#93e4009c;--lime-a10:#8fdc00b3;--lime-a11:#375f00d0;--lime-a12:#1e2900e3}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--lime-a1:color(display-p3 .412 .608 .02/.02);--lime-a2:color(display-p3 .514 .592 .024/.048);--lime-a3:color(display-p3 .584 .765 .008/.15);--lime-a4:color(display-p3 .561 .757 .004/.24);--lime-a5:color(display-p3 .514 .698 .004/.322);--lime-a6:color(display-p3 .443 .627 0/.4);--lime-a7:color(display-p3 .376 .561 .004/.491);--lime-a8:color(display-p3 .333 .529 0/.624);--lime-a9:color(display-p3 .588 .867 0/.534);--lime-a10:color(display-p3 .561 .827 0/.604);--lime-a11:color(display-p3 .386 .482 .227);--lime-a12:color(display-p3 .222 .25 .128)}}}.dark,.dark-theme{--lime-a1:#11bb0003;--lime-a2:#78f7000a;--lime-a3:#9bfd4c1a;--lime-a4:#a7fe5c29;--lime-a5:#affe6537;--lime-a6:#b2fe6d46;--lime-a7:#b6ff6f57;--lime-a8:#b6fd6d6c;--lime-a9:#caff69ed;--lime-a10:#d4ff70;--lime-a11:#d1fe77e4;--lime-a12:#e9febff7}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--lime-a1:color(display-p3 .067 .941 0/.009);--lime-a2:color(display-p3 .584 .996 .071/.038);--lime-a3:color(display-p3 .69 1 .38/.101);--lime-a4:color(display-p3 .729 1 .435/.16);--lime-a5:color(display-p3 .745 1 .471/.215);--lime-a6:color(display-p3 .769 1 .482/.274);--lime-a7:color(display-p3 .769 1 .506/.341);--lime-a8:color(display-p3 .784 1 .51/.416);--lime-a9:color(display-p3 .839 1 .502/.925);--lime-a10:color(display-p3 .871 1 .522/.996);--lime-a11:color(display-p3 .771 .893 .485);--lime-a12:color(display-p3 .905 .966 .753)}}}:root,.light,.light-theme{--yellow-1:#fdfdf9;--yellow-2:#fefce9;--yellow-3:#fffab8;--yellow-4:#fff394;--yellow-5:#ffe770;--yellow-6:#f3d768;--yellow-7:#e4c767;--yellow-8:#d5ae39;--yellow-9:#ffe629;--yellow-10:#ffdc00;--yellow-11:#9e6c00;--yellow-12:#473b1f}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--yellow-1:color(display-p3 .992 .992 .978);--yellow-2:color(display-p3 .995 .99 .922);--yellow-3:color(display-p3 .997 .982 .749);--yellow-4:color(display-p3 .992 .953 .627);--yellow-5:color(display-p3 .984 .91 .51);--yellow-6:color(display-p3 .934 .847 .474);--yellow-7:color(display-p3 .876 .785 .46);--yellow-8:color(display-p3 .811 .689 .313);--yellow-9:color(display-p3 1 .92 .22);--yellow-10:color(display-p3 .977 .868 .291);--yellow-11:color(display-p3 .6 .44 0);--yellow-12:color(display-p3 .271 .233 .137)}}}.dark,.dark-theme{--yellow-1:#14120b;--yellow-2:#1b180f;--yellow-3:#2d2305;--yellow-4:#362b00;--yellow-5:#433500;--yellow-6:#524202;--yellow-7:#665417;--yellow-8:#836a21;--yellow-9:#ffe629;--yellow-10:#ffff57;--yellow-11:#f5e147;--yellow-12:#f6eeb4}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--yellow-1:color(display-p3 .078 .069 .047);--yellow-2:color(display-p3 .103 .094 .063);--yellow-3:color(display-p3 .168 .137 .039);--yellow-4:color(display-p3 .209 .169 0);--yellow-5:color(display-p3 .255 .209 0);--yellow-6:color(display-p3 .31 .261 .07);--yellow-7:color(display-p3 .389 .331 .135);--yellow-8:color(display-p3 .497 .42 .182);--yellow-9:color(display-p3 1 .92 .22);--yellow-10:color(display-p3 1 1 .456);--yellow-11:color(display-p3 .948 .885 .392);--yellow-12:color(display-p3 .959 .934 .731)}}}:root,.light,.light-theme{--yellow-a1:#aaaa0006;--yellow-a2:#f4dd0016;--yellow-a3:#ffee0047;--yellow-a4:#ffe3016b;--yellow-a5:#ffd5008f;--yellow-a6:#ebbc0097;--yellow-a7:#d2a10098;--yellow-a8:#c99700c6;--yellow-a9:#ffe100d6;--yellow-a10:#ffdc00;--yellow-a11:#9e6c00;--yellow-a12:#2e2000e0}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--yellow-a1:color(display-p3 .675 .675 .024/.024);--yellow-a2:color(display-p3 .953 .855 .008/.079);--yellow-a3:color(display-p3 .988 .925 .004/.251);--yellow-a4:color(display-p3 .98 .875 .004/.373);--yellow-a5:color(display-p3 .969 .816 .004/.491);--yellow-a6:color(display-p3 .875 .71 0/.526);--yellow-a7:color(display-p3 .769 .604 0/.542);--yellow-a8:color(display-p3 .725 .549 0/.687);--yellow-a9:color(display-p3 1 .898 0/.781);--yellow-a10:color(display-p3 .969 .812 0/.71);--yellow-a11:color(display-p3 .6 .44 0);--yellow-a12:color(display-p3 .271 .233 .137)}}}.dark,.dark-theme{--yellow-a1:#d1510004;--yellow-a2:#f9b4000b;--yellow-a3:#ffaa001e;--yellow-a4:#fdb70028;--yellow-a5:#febb0036;--yellow-a6:#fec40046;--yellow-a7:#fdcb225c;--yellow-a8:#fdca327b;--yellow-a9:#ffe629;--yellow-a10:#ffff57;--yellow-a11:#fee949f5;--yellow-a12:#fef6baf6}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--yellow-a1:color(display-p3 .973 .369 0/.013);--yellow-a2:color(display-p3 .996 .792 0/.038);--yellow-a3:color(display-p3 .996 .71 0/.11);--yellow-a4:color(display-p3 .996 .741 0/.152);--yellow-a5:color(display-p3 .996 .765 0/.202);--yellow-a6:color(display-p3 .996 .816 .082/.261);--yellow-a7:color(display-p3 1 .831 .263/.345);--yellow-a8:color(display-p3 1 .831 .314/.463);--yellow-a9:color(display-p3 1 .922 .22);--yellow-a10:color(display-p3 1 1 .455);--yellow-a11:color(display-p3 .948 .885 .392);--yellow-a12:color(display-p3 .959 .934 .731)}}}:root,.light,.light-theme{--amber-1:#fefdfb;--amber-2:#fefbe9;--amber-3:#fff7c2;--amber-4:#ffee9c;--amber-5:#fbe577;--amber-6:#f3d673;--amber-7:#e9c162;--amber-8:#e2a336;--amber-9:#ffc53d;--amber-10:#ffba18;--amber-11:#ab6400;--amber-12:#4f3422}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--amber-1:color(display-p3 .995 .992 .985);--amber-2:color(display-p3 .994 .986 .921);--amber-3:color(display-p3 .994 .969 .782);--amber-4:color(display-p3 .989 .937 .65);--amber-5:color(display-p3 .97 .902 .527);--amber-6:color(display-p3 .936 .844 .506);--amber-7:color(display-p3 .89 .762 .443);--amber-8:color(display-p3 .85 .65 .3);--amber-9:color(display-p3 1 .77 .26);--amber-10:color(display-p3 .959 .741 .274);--amber-11:color(display-p3 .64 .4 0);--amber-12:color(display-p3 .294 .208 .145)}}}.dark,.dark-theme{--amber-1:#16120c;--amber-2:#1d180f;--amber-3:#302008;--amber-4:#3f2700;--amber-5:#4d3000;--amber-6:#5c3d05;--amber-7:#714f19;--amber-8:#8f6424;--amber-9:#ffc53d;--amber-10:#ffd60a;--amber-11:#ffca16;--amber-12:#ffe7b3}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--amber-1:color(display-p3 .082 .07 .05);--amber-2:color(display-p3 .111 .094 .064);--amber-3:color(display-p3 .178 .128 .049);--amber-4:color(display-p3 .239 .156 0);--amber-5:color(display-p3 .29 .193 0);--amber-6:color(display-p3 .344 .245 .076);--amber-7:color(display-p3 .422 .314 .141);--amber-8:color(display-p3 .535 .399 .189);--amber-9:color(display-p3 1 .77 .26);--amber-10:color(display-p3 1 .87 .15);--amber-11:color(display-p3 1 .8 .29);--amber-12:color(display-p3 .984 .909 .726)}}}:root,.light,.light-theme{--amber-a1:#c0800004;--amber-a2:#f4d10016;--amber-a3:#ffde003d;--amber-a4:#ffd40063;--amber-a5:#f8cf0088;--amber-a6:#eab5008c;--amber-a7:#dc9b009d;--amber-a8:#da8a00c9;--amber-a9:#ffb300c2;--amber-a10:#ffb300e7;--amber-a11:#ab6400;--amber-a12:#341500dd}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--amber-a1:color(display-p3 .757 .514 .024/.016);--amber-a2:color(display-p3 .902 .804 .008/.079);--amber-a3:color(display-p3 .965 .859 .004/.22);--amber-a4:color(display-p3 .969 .82 .004/.35);--amber-a5:color(display-p3 .933 .796 .004/.475);--amber-a6:color(display-p3 .875 .682 .004/.495);--amber-a7:color(display-p3 .804 .573 0/.557);--amber-a8:color(display-p3 .788 .502 0/.699);--amber-a9:color(display-p3 1 .686 0/.742);--amber-a10:color(display-p3 .945 .643 0/.726);--amber-a11:color(display-p3 .64 .4 0);--amber-a12:color(display-p3 .294 .208 .145)}}}.dark,.dark-theme{--amber-a1:#e63c0006;--amber-a2:#fd9b000d;--amber-a3:#fa820022;--amber-a4:#fc820032;--amber-a5:#fd8b0041;--amber-a6:#fd9b0051;--amber-a7:#ffab2567;--amber-a8:#ffae3587;--amber-a9:#ffc53d;--amber-a10:#ffd60a;--amber-a11:#ffca16;--amber-a12:#ffe7b3}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--amber-a1:color(display-p3 .992 .298 0/.017);--amber-a2:color(display-p3 .988 .651 0/.047);--amber-a3:color(display-p3 1 .6 0/.118);--amber-a4:color(display-p3 1 .557 0/.185);--amber-a5:color(display-p3 1 .592 0/.24);--amber-a6:color(display-p3 1 .659 .094/.299);--amber-a7:color(display-p3 1 .714 .263/.383);--amber-a8:color(display-p3 .996 .729 .306/.5);--amber-a9:color(display-p3 1 .769 .259);--amber-a10:color(display-p3 1 .871 .149);--amber-a11:color(display-p3 1 .8 .29);--amber-a12:color(display-p3 .984 .909 .726)}}}:root,.light,.light-theme{--gold-1:#fdfdfc;--gold-2:#faf9f2;--gold-3:#f2f0e7;--gold-4:#eae6db;--gold-5:#e1dccf;--gold-6:#d8d0bf;--gold-7:#cbc0aa;--gold-8:#b9a88d;--gold-9:#978365;--gold-10:#8c7a5e;--gold-11:#71624b;--gold-12:#3b352b}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--gold-1:color(display-p3 .992 .992 .989);--gold-2:color(display-p3 .98 .976 .953);--gold-3:color(display-p3 .947 .94 .909);--gold-4:color(display-p3 .914 .904 .865);--gold-5:color(display-p3 .88 .865 .816);--gold-6:color(display-p3 .84 .818 .756);--gold-7:color(display-p3 .788 .753 .677);--gold-8:color(display-p3 .715 .66 .565);--gold-9:color(display-p3 .579 .517 .41);--gold-10:color(display-p3 .538 .479 .38);--gold-11:color(display-p3 .433 .386 .305);--gold-12:color(display-p3 .227 .209 .173)}}}.dark,.dark-theme{--gold-1:#121211;--gold-2:#1b1a17;--gold-3:#24231f;--gold-4:#2d2b26;--gold-5:#38352e;--gold-6:#444039;--gold-7:#544f46;--gold-8:#696256;--gold-9:#978365;--gold-10:#a39073;--gold-11:#cbb99f;--gold-12:#e8e2d9}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--gold-1:color(display-p3 .071 .071 .067);--gold-2:color(display-p3 .104 .101 .09);--gold-3:color(display-p3 .141 .136 .122);--gold-4:color(display-p3 .177 .17 .152);--gold-5:color(display-p3 .217 .207 .185);--gold-6:color(display-p3 .265 .252 .225);--gold-7:color(display-p3 .327 .31 .277);--gold-8:color(display-p3 .407 .384 .342);--gold-9:color(display-p3 .579 .517 .41);--gold-10:color(display-p3 .628 .566 .463);--gold-11:color(display-p3 .784 .728 .635);--gold-12:color(display-p3 .906 .887 .855)}}}:root,.light,.light-theme{--gold-a1:#55550003;--gold-a2:#9d8a000d;--gold-a3:#75600018;--gold-a4:#6b4e0024;--gold-a5:#60460030;--gold-a6:#64440040;--gold-a7:#63420055;--gold-a8:#633d0072;--gold-a9:#5332009a;--gold-a10:#492d00a1;--gold-a11:#362100b4;--gold-a12:#130c00d4}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--gold-a1:color(display-p3 .349 .349 .024/.012);--gold-a2:color(display-p3 .592 .514 .024/.048);--gold-a3:color(display-p3 .4 .357 .012/.091);--gold-a4:color(display-p3 .357 .298 .008/.134);--gold-a5:color(display-p3 .345 .282 .004/.185);--gold-a6:color(display-p3 .341 .263 .004/.244);--gold-a7:color(display-p3 .345 .235 .004/.322);--gold-a8:color(display-p3 .345 .22 .004/.436);--gold-a9:color(display-p3 .286 .18 0/.589);--gold-a10:color(display-p3 .255 .161 0/.62);--gold-a11:color(display-p3 .433 .386 .305);--gold-a12:color(display-p3 .227 .209 .173)}}}.dark,.dark-theme{--gold-a1:#91911102;--gold-a2:#f9e29d0b;--gold-a3:#f8ecbb15;--gold-a4:#ffeec41e;--gold-a5:#feecc22a;--gold-a6:#feebcb37;--gold-a7:#ffedcd48;--gold-a8:#fdeaca5f;--gold-a9:#ffdba690;--gold-a10:#fedfb09d;--gold-a11:#fee7c6c8;--gold-a12:#fef7ede7}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--gold-a1:color(display-p3 .855 .855 .071/.005);--gold-a2:color(display-p3 .98 .89 .616/.043);--gold-a3:color(display-p3 1 .949 .753/.08);--gold-a4:color(display-p3 1 .933 .8/.118);--gold-a5:color(display-p3 1 .949 .804/.16);--gold-a6:color(display-p3 1 .925 .8/.215);--gold-a7:color(display-p3 1 .945 .831/.278);--gold-a8:color(display-p3 1 .937 .82/.366);--gold-a9:color(display-p3 .996 .882 .69/.551);--gold-a10:color(display-p3 1 .894 .725/.601);--gold-a11:color(display-p3 .784 .728 .635);--gold-a12:color(display-p3 .906 .887 .855)}}}:root,.light,.light-theme{--bronze-1:#fdfcfc;--bronze-2:#fdf7f5;--bronze-3:#f6edea;--bronze-4:#efe4df;--bronze-5:#e7d9d3;--bronze-6:#dfcdc5;--bronze-7:#d3bcb3;--bronze-8:#c2a499;--bronze-9:#a18072;--bronze-10:#957468;--bronze-11:#7d5e54;--bronze-12:#43302b}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--bronze-1:color(display-p3 .991 .988 .988);--bronze-2:color(display-p3 .989 .97 .961);--bronze-3:color(display-p3 .958 .932 .919);--bronze-4:color(display-p3 .929 .894 .877);--bronze-5:color(display-p3 .898 .853 .832);--bronze-6:color(display-p3 .861 .805 .778);--bronze-7:color(display-p3 .812 .739 .706);--bronze-8:color(display-p3 .741 .647 .606);--bronze-9:color(display-p3 .611 .507 .455);--bronze-10:color(display-p3 .563 .461 .414);--bronze-11:color(display-p3 .471 .373 .336);--bronze-12:color(display-p3 .251 .191 .172)}}}.dark,.dark-theme{--bronze-1:#141110;--bronze-2:#1c1917;--bronze-3:#262220;--bronze-4:#302a27;--bronze-5:#3b3330;--bronze-6:#493e3a;--bronze-7:#5a4c47;--bronze-8:#6f5f58;--bronze-9:#a18072;--bronze-10:#ae8c7e;--bronze-11:#d4b3a5;--bronze-12:#ede0d9}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--bronze-1:color(display-p3 .076 .067 .063);--bronze-2:color(display-p3 .106 .097 .093);--bronze-3:color(display-p3 .147 .132 .125);--bronze-4:color(display-p3 .185 .166 .156);--bronze-5:color(display-p3 .227 .202 .19);--bronze-6:color(display-p3 .278 .246 .23);--bronze-7:color(display-p3 .343 .302 .281);--bronze-8:color(display-p3 .426 .374 .347);--bronze-9:color(display-p3 .611 .507 .455);--bronze-10:color(display-p3 .66 .556 .504);--bronze-11:color(display-p3 .81 .707 .655);--bronze-12:color(display-p3 .921 .88 .854)}}}:root,.light,.light-theme{--bronze-a1:#55000003;--bronze-a2:#cc33000a;--bronze-a3:#92250015;--bronze-a4:#80280020;--bronze-a5:#7423002c;--bronze-a6:#7324003a;--bronze-a7:#6c1f004c;--bronze-a8:#671c0066;--bronze-a9:#551a008d;--bronze-a10:#4c150097;--bronze-a11:#3d0f00ab;--bronze-a12:#1d0600d4}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--bronze-a1:color(display-p3 .349 .024 .024/.012);--bronze-a2:color(display-p3 .71 .22 .024/.04);--bronze-a3:color(display-p3 .482 .2 .008/.083);--bronze-a4:color(display-p3 .424 .133 .004/.122);--bronze-a5:color(display-p3 .4 .145 .004/.169);--bronze-a6:color(display-p3 .388 .125 .004/.224);--bronze-a7:color(display-p3 .365 .11 .004/.295);--bronze-a8:color(display-p3 .341 .102 .004/.393);--bronze-a9:color(display-p3 .29 .094 0/.546);--bronze-a10:color(display-p3 .255 .082 0/.585);--bronze-a11:color(display-p3 .471 .373 .336);--bronze-a12:color(display-p3 .251 .191 .172)}}}.dark,.dark-theme{--bronze-a1:#d1110004;--bronze-a2:#fbbc910c;--bronze-a3:#faceb817;--bronze-a4:#facdb622;--bronze-a5:#ffd2c12d;--bronze-a6:#ffd1c03c;--bronze-a7:#fdd0c04f;--bronze-a8:#ffd6c565;--bronze-a9:#fec7b09b;--bronze-a10:#fecab5a9;--bronze-a11:#ffd7c6d1;--bronze-a12:#fff1e9ec}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--bronze-a1:color(display-p3 .941 .067 0/.009);--bronze-a2:color(display-p3 .98 .8 .706/.043);--bronze-a3:color(display-p3 .988 .851 .761/.085);--bronze-a4:color(display-p3 .996 .839 .78/.127);--bronze-a5:color(display-p3 .996 .863 .773/.173);--bronze-a6:color(display-p3 1 .863 .796/.227);--bronze-a7:color(display-p3 1 .867 .8/.295);--bronze-a8:color(display-p3 1 .859 .788/.387);--bronze-a9:color(display-p3 1 .82 .733/.585);--bronze-a10:color(display-p3 1 .839 .761/.635);--bronze-a11:color(display-p3 .81 .707 .655);--bronze-a12:color(display-p3 .921 .88 .854)}}}:root,.light,.light-theme{--gray-1:#fcfcfc;--gray-2:#f9f9f9;--gray-3:#f0f0f0;--gray-4:#e8e8e8;--gray-5:#e0e0e0;--gray-6:#d9d9d9;--gray-7:#cecece;--gray-8:#bbb;--gray-9:#8d8d8d;--gray-10:#838383;--gray-11:#646464;--gray-12:#202020}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--gray-1:color(display-p3 .988 .988 .988);--gray-2:color(display-p3 .975 .975 .975);--gray-3:color(display-p3 .939 .939 .939);--gray-4:color(display-p3 .908 .908 .908);--gray-5:color(display-p3 .88 .88 .88);--gray-6:color(display-p3 .849 .849 .849);--gray-7:color(display-p3 .807 .807 .807);--gray-8:color(display-p3 .732 .732 .732);--gray-9:color(display-p3 .553 .553 .553);--gray-10:color(display-p3 .512 .512 .512);--gray-11:color(display-p3 .392 .392 .392);--gray-12:color(display-p3 .125 .125 .125)}}}.dark,.dark-theme{--gray-1:#111;--gray-2:#191919;--gray-3:#222;--gray-4:#2a2a2a;--gray-5:#313131;--gray-6:#3a3a3a;--gray-7:#484848;--gray-8:#606060;--gray-9:#6e6e6e;--gray-10:#7b7b7b;--gray-11:#b4b4b4;--gray-12:#eee}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--gray-1:color(display-p3 .067 .067 .067);--gray-2:color(display-p3 .098 .098 .098);--gray-3:color(display-p3 .135 .135 .135);--gray-4:color(display-p3 .163 .163 .163);--gray-5:color(display-p3 .192 .192 .192);--gray-6:color(display-p3 .228 .228 .228);--gray-7:color(display-p3 .283 .283 .283);--gray-8:color(display-p3 .375 .375 .375);--gray-9:color(display-p3 .431 .431 .431);--gray-10:color(display-p3 .484 .484 .484);--gray-11:color(display-p3 .706 .706 .706);--gray-12:color(display-p3 .933 .933 .933)}}}:root,.light,.light-theme{--gray-a1:#00000003;--gray-a2:#00000006;--gray-a3:#0000000f;--gray-a4:#00000017;--gray-a5:#0000001f;--gray-a6:#00000026;--gray-a7:#00000031;--gray-a8:#0004;--gray-a9:#00000072;--gray-a10:#0000007c;--gray-a11:#0000009b;--gray-a12:#000000df}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--gray-a1:color(display-p3 0 0 0/.012);--gray-a2:color(display-p3 0 0 0/.024);--gray-a3:color(display-p3 0 0 0/.063);--gray-a4:color(display-p3 0 0 0/.09);--gray-a5:color(display-p3 0 0 0/.122);--gray-a6:color(display-p3 0 0 0/.153);--gray-a7:color(display-p3 0 0 0/.192);--gray-a8:color(display-p3 0 0 0/.267);--gray-a9:color(display-p3 0 0 0/.447);--gray-a10:color(display-p3 0 0 0/.486);--gray-a11:color(display-p3 0 0 0/.608);--gray-a12:color(display-p3 0 0 0/.875)}}}.dark,.dark-theme{--gray-a1:#0000;--gray-a2:#ffffff09;--gray-a3:#ffffff12;--gray-a4:#ffffff1b;--gray-a5:#fff2;--gray-a6:#ffffff2c;--gray-a7:#ffffff3b;--gray-a8:#fff5;--gray-a9:#ffffff64;--gray-a10:#ffffff72;--gray-a11:#ffffffaf;--gray-a12:#ffffffed}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--gray-a1:color(display-p3 0 0 0/0);--gray-a2:color(display-p3 1 1 1/.034);--gray-a3:color(display-p3 1 1 1/.071);--gray-a4:color(display-p3 1 1 1/.105);--gray-a5:color(display-p3 1 1 1/.134);--gray-a6:color(display-p3 1 1 1/.172);--gray-a7:color(display-p3 1 1 1/.231);--gray-a8:color(display-p3 1 1 1/.332);--gray-a9:color(display-p3 1 1 1/.391);--gray-a10:color(display-p3 1 1 1/.445);--gray-a11:color(display-p3 1 1 1/.685);--gray-a12:color(display-p3 1 1 1/.929)}}}:root,.light,.light-theme{--mauve-1:#fdfcfd;--mauve-2:#faf9fb;--mauve-3:#f2eff3;--mauve-4:#eae7ec;--mauve-5:#e3dfe6;--mauve-6:#dbd8e0;--mauve-7:#d0cdd7;--mauve-8:#bcbac7;--mauve-9:#8e8c99;--mauve-10:#84828e;--mauve-11:#65636d;--mauve-12:#211f26}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--mauve-1:color(display-p3 .991 .988 .992);--mauve-2:color(display-p3 .98 .976 .984);--mauve-3:color(display-p3 .946 .938 .952);--mauve-4:color(display-p3 .915 .906 .925);--mauve-5:color(display-p3 .886 .876 .901);--mauve-6:color(display-p3 .856 .846 .875);--mauve-7:color(display-p3 .814 .804 .84);--mauve-8:color(display-p3 .735 .728 .777);--mauve-9:color(display-p3 .555 .549 .596);--mauve-10:color(display-p3 .514 .508 .552);--mauve-11:color(display-p3 .395 .388 .424);--mauve-12:color(display-p3 .128 .122 .147)}}}.dark,.dark-theme{--mauve-1:#121113;--mauve-2:#1a191b;--mauve-3:#232225;--mauve-4:#2b292d;--mauve-5:#323035;--mauve-6:#3c393f;--mauve-7:#49474e;--mauve-8:#625f69;--mauve-9:#6f6d78;--mauve-10:#7c7a85;--mauve-11:#b5b2bc;--mauve-12:#eeeef0}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--mauve-1:color(display-p3 .07 .067 .074);--mauve-2:color(display-p3 .101 .098 .105);--mauve-3:color(display-p3 .138 .134 .144);--mauve-4:color(display-p3 .167 .161 .175);--mauve-5:color(display-p3 .196 .189 .206);--mauve-6:color(display-p3 .232 .225 .245);--mauve-7:color(display-p3 .286 .277 .302);--mauve-8:color(display-p3 .383 .373 .408);--mauve-9:color(display-p3 .434 .428 .467);--mauve-10:color(display-p3 .487 .48 .519);--mauve-11:color(display-p3 .707 .7 .735);--mauve-12:color(display-p3 .933 .933 .94)}}}:root,.light,.light-theme{--mauve-a1:#55005503;--mauve-a2:#2b005506;--mauve-a3:#30004010;--mauve-a4:#20003618;--mauve-a5:#20003820;--mauve-a6:#14003527;--mauve-a7:#10003332;--mauve-a8:#08003145;--mauve-a9:#05001d73;--mauve-a10:#0500197d;--mauve-a11:#0400119c;--mauve-a12:#020008e0}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--mauve-a1:color(display-p3 .349 .024 .349/.012);--mauve-a2:color(display-p3 .184 .024 .349/.024);--mauve-a3:color(display-p3 .129 .008 .255/.063);--mauve-a4:color(display-p3 .094 .012 .216/.095);--mauve-a5:color(display-p3 .098 .008 .224/.126);--mauve-a6:color(display-p3 .055 .004 .18/.153);--mauve-a7:color(display-p3 .067 .008 .184/.197);--mauve-a8:color(display-p3 .02 .004 .176/.271);--mauve-a9:color(display-p3 .02 .004 .106/.451);--mauve-a10:color(display-p3 .012 .004 .09/.491);--mauve-a11:color(display-p3 .016 0 .059/.612);--mauve-a12:color(display-p3 .008 0 .027/.879)}}}.dark,.dark-theme{--mauve-a1:#0000;--mauve-a2:#f5f4f609;--mauve-a3:#ebeaf814;--mauve-a4:#eee5f81d;--mauve-a5:#efe6fe25;--mauve-a6:#f1e6fd30;--mauve-a7:#eee9ff40;--mauve-a8:#eee7ff5d;--mauve-a9:#eae6fd6e;--mauve-a10:#ece9fd7c;--mauve-a11:#f5f1ffb7;--mauve-a12:#fdfdffef}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--mauve-a1:color(display-p3 0 0 0/0);--mauve-a2:color(display-p3 .996 .992 1/.034);--mauve-a3:color(display-p3 .937 .933 .992/.077);--mauve-a4:color(display-p3 .957 .918 .996/.111);--mauve-a5:color(display-p3 .937 .906 .996/.145);--mauve-a6:color(display-p3 .953 .925 .996/.183);--mauve-a7:color(display-p3 .945 .929 1/.246);--mauve-a8:color(display-p3 .937 .918 1/.361);--mauve-a9:color(display-p3 .933 .918 1/.424);--mauve-a10:color(display-p3 .941 .925 1/.479);--mauve-a11:color(display-p3 .965 .961 1/.712);--mauve-a12:color(display-p3 .992 .992 1/.937)}}}:root,.light,.light-theme{--slate-1:#fcfcfd;--slate-2:#f9f9fb;--slate-3:#f0f0f3;--slate-4:#e8e8ec;--slate-5:#e0e1e6;--slate-6:#d9d9e0;--slate-7:#cdced6;--slate-8:#b9bbc6;--slate-9:#8b8d98;--slate-10:#80838d;--slate-11:#60646c;--slate-12:#1c2024}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--slate-1:color(display-p3 .988 .988 .992);--slate-2:color(display-p3 .976 .976 .984);--slate-3:color(display-p3 .94 .941 .953);--slate-4:color(display-p3 .908 .909 .925);--slate-5:color(display-p3 .88 .881 .901);--slate-6:color(display-p3 .85 .852 .876);--slate-7:color(display-p3 .805 .808 .838);--slate-8:color(display-p3 .727 .733 .773);--slate-9:color(display-p3 .547 .553 .592);--slate-10:color(display-p3 .503 .512 .549);--slate-11:color(display-p3 .379 .392 .421);--slate-12:color(display-p3 .113 .125 .14)}}}.dark,.dark-theme{--slate-1:#111113;--slate-2:#18191b;--slate-3:#212225;--slate-4:#272a2d;--slate-5:#2e3135;--slate-6:#363a3f;--slate-7:#43484e;--slate-8:#5a6169;--slate-9:#696e77;--slate-10:#777b84;--slate-11:#b0b4ba;--slate-12:#edeef0}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--slate-1:color(display-p3 .067 .067 .074);--slate-2:color(display-p3 .095 .098 .105);--slate-3:color(display-p3 .13 .135 .145);--slate-4:color(display-p3 .156 .163 .176);--slate-5:color(display-p3 .183 .191 .206);--slate-6:color(display-p3 .215 .226 .244);--slate-7:color(display-p3 .265 .28 .302);--slate-8:color(display-p3 .357 .381 .409);--slate-9:color(display-p3 .415 .431 .463);--slate-10:color(display-p3 .469 .483 .514);--slate-11:color(display-p3 .692 .704 .728);--slate-12:color(display-p3 .93 .933 .94)}}}:root,.light,.light-theme{--slate-a1:#00005503;--slate-a2:#00005506;--slate-a3:#0000330f;--slate-a4:#00002d17;--slate-a5:#0009321f;--slate-a6:#00002f26;--slate-a7:#00062e32;--slate-a8:#00083046;--slate-a9:#00051d74;--slate-a10:#00071b7f;--slate-a11:#0007149f;--slate-a12:#000509e3}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--slate-a1:color(display-p3 .024 .024 .349/.012);--slate-a2:color(display-p3 .024 .024 .349/.024);--slate-a3:color(display-p3 .004 .004 .204/.059);--slate-a4:color(display-p3 .012 .012 .184/.091);--slate-a5:color(display-p3 .004 .039 .2/.122);--slate-a6:color(display-p3 .008 .008 .165/.15);--slate-a7:color(display-p3 .008 .027 .184/.197);--slate-a8:color(display-p3 .004 .031 .176/.275);--slate-a9:color(display-p3 .004 .02 .106/.455);--slate-a10:color(display-p3 .004 .027 .098/.499);--slate-a11:color(display-p3 0 .02 .063/.62);--slate-a12:color(display-p3 0 .012 .031/.887)}}}.dark,.dark-theme{--slate-a1:#0000;--slate-a2:#d8f4f609;--slate-a3:#ddeaf814;--slate-a4:#d3edf81d;--slate-a5:#d9edfe25;--slate-a6:#d6ebfd30;--slate-a7:#d9edff40;--slate-a8:#d9edff5d;--slate-a9:#dfebfd6d;--slate-a10:#e5edfd7b;--slate-a11:#f1f7feb5;--slate-a12:#fcfdffef}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--slate-a1:color(display-p3 0 0 0/0);--slate-a2:color(display-p3 .875 .992 1/.034);--slate-a3:color(display-p3 .882 .933 .992/.077);--slate-a4:color(display-p3 .882 .953 .996/.111);--slate-a5:color(display-p3 .878 .929 .996/.145);--slate-a6:color(display-p3 .882 .949 .996/.183);--slate-a7:color(display-p3 .882 .929 1/.246);--slate-a8:color(display-p3 .871 .937 1/.361);--slate-a9:color(display-p3 .898 .937 1/.42);--slate-a10:color(display-p3 .918 .945 1/.475);--slate-a11:color(display-p3 .949 .969 .996/.708);--slate-a12:color(display-p3 .988 .992 1/.937)}}}:root,.light,.light-theme{--sage-1:#fbfdfc;--sage-2:#f7f9f8;--sage-3:#eef1f0;--sage-4:#e6e9e8;--sage-5:#dfe2e0;--sage-6:#d7dad9;--sage-7:#cbcfcd;--sage-8:#b8bcba;--sage-9:#868e8b;--sage-10:#7c8481;--sage-11:#5f6563;--sage-12:#1a211e}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--sage-1:color(display-p3 .986 .992 .988);--sage-2:color(display-p3 .97 .977 .974);--sage-3:color(display-p3 .935 .944 .94);--sage-4:color(display-p3 .904 .913 .909);--sage-5:color(display-p3 .875 .885 .88);--sage-6:color(display-p3 .844 .854 .849);--sage-7:color(display-p3 .8 .811 .806);--sage-8:color(display-p3 .725 .738 .732);--sage-9:color(display-p3 .531 .556 .546);--sage-10:color(display-p3 .492 .515 .506);--sage-11:color(display-p3 .377 .395 .389);--sage-12:color(display-p3 .107 .129 .118)}}}.dark,.dark-theme{--sage-1:#101211;--sage-2:#171918;--sage-3:#202221;--sage-4:#272a29;--sage-5:#2e3130;--sage-6:#373b39;--sage-7:#444947;--sage-8:#5b625f;--sage-9:#63706b;--sage-10:#717d79;--sage-11:#adb5b2;--sage-12:#eceeed}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--sage-1:color(display-p3 .064 .07 .067);--sage-2:color(display-p3 .092 .098 .094);--sage-3:color(display-p3 .128 .135 .131);--sage-4:color(display-p3 .155 .164 .159);--sage-5:color(display-p3 .183 .193 .188);--sage-6:color(display-p3 .218 .23 .224);--sage-7:color(display-p3 .269 .285 .277);--sage-8:color(display-p3 .362 .382 .373);--sage-9:color(display-p3 .398 .438 .421);--sage-10:color(display-p3 .453 .49 .474);--sage-11:color(display-p3 .685 .709 .697);--sage-12:color(display-p3 .927 .933 .93)}}}:root,.light,.light-theme{--sage-a1:#00804004;--sage-a2:#00402008;--sage-a3:#002d1e11;--sage-a4:#001f1519;--sage-a5:#00180820;--sage-a6:#00140d28;--sage-a7:#00140a34;--sage-a8:#000f0847;--sage-a9:#00110b79;--sage-a10:#00100a83;--sage-a11:#000a07a0;--sage-a12:#000805e5}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--sage-a1:color(display-p3 .024 .514 .267/.016);--sage-a2:color(display-p3 .02 .267 .145/.032);--sage-a3:color(display-p3 .008 .184 .125/.067);--sage-a4:color(display-p3 .012 .094 .051/.095);--sage-a5:color(display-p3 .008 .098 .035/.126);--sage-a6:color(display-p3 .004 .078 .027/.157);--sage-a7:color(display-p3 0 .059 .039/.2);--sage-a8:color(display-p3 .004 .047 .031/.275);--sage-a9:color(display-p3 .004 .059 .035/.471);--sage-a10:color(display-p3 0 .047 .031/.51);--sage-a11:color(display-p3 0 .031 .02/.624);--sage-a12:color(display-p3 0 .027 .012/.895)}}}.dark,.dark-theme{--sage-a1:#0000;--sage-a2:#f0f2f108;--sage-a3:#f3f5f412;--sage-a4:#f2fefd1a;--sage-a5:#f1fbfa22;--sage-a6:#edfbf42d;--sage-a7:#edfcf73c;--sage-a8:#ebfdf657;--sage-a9:#dffdf266;--sage-a10:#e5fdf674;--sage-a11:#f4fefbb0;--sage-a12:#fdfffeed}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--sage-a1:color(display-p3 0 0 0/0);--sage-a2:color(display-p3 .976 .988 .984/.03);--sage-a3:color(display-p3 .992 .945 .941/.072);--sage-a4:color(display-p3 .988 .996 .992/.102);--sage-a5:color(display-p3 .992 1 .996/.131);--sage-a6:color(display-p3 .973 1 .976/.173);--sage-a7:color(display-p3 .957 1 .976/.233);--sage-a8:color(display-p3 .957 1 .984/.334);--sage-a9:color(display-p3 .902 1 .957/.397);--sage-a10:color(display-p3 .929 1 .973/.452);--sage-a11:color(display-p3 .969 1 .988/.688);--sage-a12:color(display-p3 .992 1 .996/.929)}}}:root,.light,.light-theme{--olive-1:#fcfdfc;--olive-2:#f8faf8;--olive-3:#eff1ef;--olive-4:#e7e9e7;--olive-5:#dfe2df;--olive-6:#d7dad7;--olive-7:#cccfcc;--olive-8:#b9bcb8;--olive-9:#898e87;--olive-10:#7f847d;--olive-11:#60655f;--olive-12:#1d211c}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--olive-1:color(display-p3 .989 .992 .989);--olive-2:color(display-p3 .974 .98 .973);--olive-3:color(display-p3 .939 .945 .937);--olive-4:color(display-p3 .907 .914 .905);--olive-5:color(display-p3 .878 .885 .875);--olive-6:color(display-p3 .846 .855 .843);--olive-7:color(display-p3 .803 .812 .8);--olive-8:color(display-p3 .727 .738 .723);--olive-9:color(display-p3 .541 .556 .532);--olive-10:color(display-p3 .5 .515 .491);--olive-11:color(display-p3 .38 .395 .374);--olive-12:color(display-p3 .117 .129 .111)}}}.dark,.dark-theme{--olive-1:#111210;--olive-2:#181917;--olive-3:#212220;--olive-4:#282a27;--olive-5:#2f312e;--olive-6:#383a36;--olive-7:#454843;--olive-8:#5c625b;--olive-9:#687066;--olive-10:#767d74;--olive-11:#afb5ad;--olive-12:#eceeec}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--olive-1:color(display-p3 .067 .07 .063);--olive-2:color(display-p3 .095 .098 .091);--olive-3:color(display-p3 .131 .135 .126);--olive-4:color(display-p3 .158 .163 .153);--olive-5:color(display-p3 .186 .192 .18);--olive-6:color(display-p3 .221 .229 .215);--olive-7:color(display-p3 .273 .284 .266);--olive-8:color(display-p3 .365 .382 .359);--olive-9:color(display-p3 .414 .438 .404);--olive-10:color(display-p3 .467 .49 .458);--olive-11:color(display-p3 .69 .709 .682);--olive-12:color(display-p3 .927 .933 .926)}}}:root,.light,.light-theme{--olive-a1:#00550003;--olive-a2:#00490007;--olive-a3:#00200010;--olive-a4:#00160018;--olive-a5:#00180020;--olive-a6:#00140028;--olive-a7:#000f0033;--olive-a8:#040f0047;--olive-a9:#050f0078;--olive-a10:#040e0082;--olive-a11:#020a00a0;--olive-a12:#010600e3}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--olive-a1:color(display-p3 .024 .349 .024/.012);--olive-a2:color(display-p3 .024 .302 .024/.028);--olive-a3:color(display-p3 .008 .129 .008/.063);--olive-a4:color(display-p3 .012 .094 .012/.095);--olive-a5:color(display-p3 .035 .098 .008/.126);--olive-a6:color(display-p3 .027 .078 .004/.157);--olive-a7:color(display-p3 .02 .059 0/.2);--olive-a8:color(display-p3 .02 .059 .004/.279);--olive-a9:color(display-p3 .02 .051 .004/.467);--olive-a10:color(display-p3 .024 .047 0/.51);--olive-a11:color(display-p3 .012 .039 0/.628);--olive-a12:color(display-p3 .008 .024 0/.891)}}}.dark,.dark-theme{--olive-a1:#0000;--olive-a2:#f1f2f008;--olive-a3:#f4f5f312;--olive-a4:#f3fef21a;--olive-a5:#f2fbf122;--olive-a6:#f4faed2c;--olive-a7:#f2fced3b;--olive-a8:#edfdeb57;--olive-a9:#ebfde766;--olive-a10:#f0fdec74;--olive-a11:#f6fef4b0;--olive-a12:#fdfffded}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--olive-a1:color(display-p3 0 0 0/0);--olive-a2:color(display-p3 .984 .988 .976/.03);--olive-a3:color(display-p3 .992 .996 .988/.068);--olive-a4:color(display-p3 .953 .996 .949/.102);--olive-a5:color(display-p3 .969 1 .965/.131);--olive-a6:color(display-p3 .973 1 .969/.169);--olive-a7:color(display-p3 .98 1 .961/.228);--olive-a8:color(display-p3 .961 1 .957/.334);--olive-a9:color(display-p3 .949 1 .922/.397);--olive-a10:color(display-p3 .953 1 .941/.452);--olive-a11:color(display-p3 .976 1 .965/.688);--olive-a12:color(display-p3 .992 1 .992/.929)}}}:root,.light,.light-theme{--sand-1:#fdfdfc;--sand-2:#f9f9f8;--sand-3:#f1f0ef;--sand-4:#e9e8e6;--sand-5:#e2e1de;--sand-6:#dad9d6;--sand-7:#cfceca;--sand-8:#bcbbb5;--sand-9:#8d8d86;--sand-10:#82827c;--sand-11:#63635e;--sand-12:#21201c}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--sand-1:color(display-p3 .992 .992 .989);--sand-2:color(display-p3 .977 .977 .973);--sand-3:color(display-p3 .943 .942 .936);--sand-4:color(display-p3 .913 .912 .903);--sand-5:color(display-p3 .885 .883 .873);--sand-6:color(display-p3 .854 .852 .839);--sand-7:color(display-p3 .813 .81 .794);--sand-8:color(display-p3 .738 .734 .713);--sand-9:color(display-p3 .553 .553 .528);--sand-10:color(display-p3 .511 .511 .488);--sand-11:color(display-p3 .388 .388 .37);--sand-12:color(display-p3 .129 .126 .111)}}}.dark,.dark-theme{--sand-1:#111110;--sand-2:#191918;--sand-3:#222221;--sand-4:#2a2a28;--sand-5:#31312e;--sand-6:#3b3a37;--sand-7:#494844;--sand-8:#62605b;--sand-9:#6f6d66;--sand-10:#7c7b74;--sand-11:#b5b3ad;--sand-12:#eeeeec}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--sand-1:color(display-p3 .067 .067 .063);--sand-2:color(display-p3 .098 .098 .094);--sand-3:color(display-p3 .135 .135 .129);--sand-4:color(display-p3 .164 .163 .156);--sand-5:color(display-p3 .193 .192 .183);--sand-6:color(display-p3 .23 .229 .217);--sand-7:color(display-p3 .285 .282 .267);--sand-8:color(display-p3 .384 .378 .357);--sand-9:color(display-p3 .434 .428 .403);--sand-10:color(display-p3 .487 .481 .456);--sand-11:color(display-p3 .707 .703 .68);--sand-12:color(display-p3 .933 .933 .926)}}}:root,.light,.light-theme{--sand-a1:#55550003;--sand-a2:#25250007;--sand-a3:#20100010;--sand-a4:#1f150019;--sand-a5:#1f180021;--sand-a6:#19130029;--sand-a7:#19140035;--sand-a8:#1915014a;--sand-a9:#0f0f0079;--sand-a10:#0c0c0083;--sand-a11:#080800a1;--sand-a12:#060500e3}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--sand-a1:color(display-p3 .349 .349 .024/.012);--sand-a2:color(display-p3 .161 .161 .024/.028);--sand-a3:color(display-p3 .067 .067 .008/.063);--sand-a4:color(display-p3 .129 .129 .012/.099);--sand-a5:color(display-p3 .098 .067 .008/.126);--sand-a6:color(display-p3 .102 .075 .004/.161);--sand-a7:color(display-p3 .098 .098 .004/.208);--sand-a8:color(display-p3 .086 .075 .004/.287);--sand-a9:color(display-p3 .051 .051 .004/.471);--sand-a10:color(display-p3 .047 .047 0/.514);--sand-a11:color(display-p3 .031 .031 0/.632);--sand-a12:color(display-p3 .024 .02 0/.891)}}}.dark,.dark-theme{--sand-a1:#0000;--sand-a2:#f4f4f309;--sand-a3:#f6f6f513;--sand-a4:#fefef31b;--sand-a5:#fbfbeb23;--sand-a6:#fffaed2d;--sand-a7:#fffbed3c;--sand-a8:#fff9eb57;--sand-a9:#fffae965;--sand-a10:#fffdee73;--sand-a11:#fffcf4b0;--sand-a12:#fffffded}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--sand-a1:color(display-p3 0 0 0/0);--sand-a2:color(display-p3 .992 .992 .988/.034);--sand-a3:color(display-p3 .996 .996 .992/.072);--sand-a4:color(display-p3 .992 .992 .953/.106);--sand-a5:color(display-p3 1 1 .965/.135);--sand-a6:color(display-p3 1 .976 .929/.177);--sand-a7:color(display-p3 1 .984 .929/.236);--sand-a8:color(display-p3 1 .976 .925/.341);--sand-a9:color(display-p3 1 .98 .925/.395);--sand-a10:color(display-p3 1 .992 .933/.45);--sand-a11:color(display-p3 1 .996 .961/.685);--sand-a12:color(display-p3 1 1 .992/.929)}}}:root{--black-a1:#0000000d;--black-a2:#0000001a;--black-a3:#00000026;--black-a4:#0003;--black-a5:#0000004d;--black-a6:#0006;--black-a7:#00000080;--black-a8:#0009;--black-a9:#000000b3;--black-a10:#000c;--black-a11:#000000e6;--black-a12:#000000f2}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root{--black-a1:color(display-p3 0 0 0/.05);--black-a2:color(display-p3 0 0 0/.1);--black-a3:color(display-p3 0 0 0/.15);--black-a4:color(display-p3 0 0 0/.2);--black-a5:color(display-p3 0 0 0/.3);--black-a6:color(display-p3 0 0 0/.4);--black-a7:color(display-p3 0 0 0/.5);--black-a8:color(display-p3 0 0 0/.6);--black-a9:color(display-p3 0 0 0/.7);--black-a10:color(display-p3 0 0 0/.8);--black-a11:color(display-p3 0 0 0/.9);--black-a12:color(display-p3 0 0 0/.95)}}}:root{--white-a1:#ffffff0d;--white-a2:#ffffff1a;--white-a3:#ffffff26;--white-a4:#fff3;--white-a5:#ffffff4d;--white-a6:#fff6;--white-a7:#ffffff80;--white-a8:#fff9;--white-a9:#ffffffb3;--white-a10:#fffc;--white-a11:#ffffffe6;--white-a12:#fffffff2}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root{--white-a1:color(display-p3 1 1 1/.05);--white-a2:color(display-p3 1 1 1/.1);--white-a3:color(display-p3 1 1 1/.15);--white-a4:color(display-p3 1 1 1/.2);--white-a5:color(display-p3 1 1 1/.3);--white-a6:color(display-p3 1 1 1/.4);--white-a7:color(display-p3 1 1 1/.5);--white-a8:color(display-p3 1 1 1/.6);--white-a9:color(display-p3 1 1 1/.7);--white-a10:color(display-p3 1 1 1/.8);--white-a11:color(display-p3 1 1 1/.9);--white-a12:color(display-p3 1 1 1/.95)}}}:root{--tomato-contrast:white;--red-contrast:white;--ruby-contrast:white;--crimson-contrast:white;--pink-contrast:white;--plum-contrast:white;--purple-contrast:white;--violet-contrast:white;--iris-contrast:white;--indigo-contrast:white;--blue-contrast:white;--cyan-contrast:white;--teal-contrast:white;--jade-contrast:white;--green-contrast:white;--grass-contrast:white;--orange-contrast:white;--brown-contrast:white;--sky-contrast:#1c2024;--mint-contrast:#1a211e;--lime-contrast:#1d211c;--yellow-contrast:#21201c;--amber-contrast:#21201c;--gold-contrast:white;--bronze-contrast:white;--gray-contrast:white}:root,.light,.light-theme{--gray-surface:#fffc;--mauve-surface:#fffc;--slate-surface:#fffc;--sage-surface:#fffc;--olive-surface:#fffc;--sand-surface:#fffc;--tomato-surface:#fff6f5cc;--red-surface:#fff5f5cc;--ruby-surface:#fff5f6cc;--crimson-surface:#fef5f8cc;--pink-surface:#fef5facc;--plum-surface:#fdf5fdcc;--purple-surface:#faf5fecc;--violet-surface:#f9f6ffcc;--iris-surface:#f6f6ffcc;--indigo-surface:#f5f8ffcc;--blue-surface:#f1f9ffcc;--cyan-surface:#eff9facc;--teal-surface:#f0faf8cc;--jade-surface:#f1faf5cc;--green-surface:#f1faf4cc;--grass-surface:#f3faf3cc;--brown-surface:#fbf8f4cc;--bronze-surface:#fdf5f3cc;--gold-surface:#f9f8efcc;--sky-surface:#eef9fdcc;--mint-surface:#effaf8cc;--lime-surface:#f6f9f0cc;--yellow-surface:#fefbe4cc;--amber-surface:#fefae4cc;--orange-surface:#fff5e9cc}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){:root,.light,.light-theme{--gray-surface:color(display-p3 1 1 1/.8);--mauve-surface:color(display-p3 1 1 1/.8);--slate-surface:color(display-p3 1 1 1/.8);--sage-surface:color(display-p3 1 1 1/.8);--olive-surface:color(display-p3 1 1 1/.8);--sand-surface:color(display-p3 1 1 1/.8);--tomato-surface:color(display-p3 .9922 .9647 .9608/.8);--red-surface:color(display-p3 .9961 .9647 .9647/.8);--ruby-surface:color(display-p3 .9961 .9647 .9647/.8);--crimson-surface:color(display-p3 .9922 .9608 .9725/.8);--pink-surface:color(display-p3 .9922 .9608 .9804/.8);--plum-surface:color(display-p3 .9843 .9647 .9843/.8);--purple-surface:color(display-p3 .9804 .9647 .9922/.8);--violet-surface:color(display-p3 .9725 .9647 .9961/.8);--iris-surface:color(display-p3 .9647 .9647 .9961/.8);--indigo-surface:color(display-p3 .9647 .9725 .9961/.8);--blue-surface:color(display-p3 .9529 .9765 .9961/.8);--cyan-surface:color(display-p3 .9412 .9765 .9804/.8);--teal-surface:color(display-p3 .9451 .9804 .9725/.8);--jade-surface:color(display-p3 .9529 .9804 .9608/.8);--green-surface:color(display-p3 .9569 .9804 .9608/.8);--grass-surface:color(display-p3 .9569 .9804 .9569/.8);--brown-surface:color(display-p3 .9843 .9725 .9569/.8);--bronze-surface:color(display-p3 .9843 .9608 .9529/.8);--gold-surface:color(display-p3 .9765 .9725 .9412/.8);--sky-surface:color(display-p3 .9412 .9765 .9843/.8);--mint-surface:color(display-p3 .9451 .9804 .9725/.8);--lime-surface:color(display-p3 .9725 .9765 .9412/.8);--yellow-surface:color(display-p3 .9961 .9922 .902/.8);--amber-surface:color(display-p3 .9922 .9843 .902/.8);--orange-surface:color(display-p3 .9961 .9608 .9176/.8)}}}.dark,.dark-theme{--gray-surface:#21212180;--mauve-surface:#22212380;--slate-surface:#1f212380;--sage-surface:#1e201f80;--olive-surface:#1f201e80;--sand-surface:#21212080;--tomato-surface:#2d191580;--red-surface:#2f151780;--ruby-surface:#2b191d80;--crimson-surface:#2f151f80;--pink-surface:#31132980;--plum-surface:#2f152f80;--purple-surface:#2b173580;--violet-surface:#25193980;--iris-surface:#1d1b3980;--indigo-surface:#171d3b80;--blue-surface:#11213d80;--cyan-surface:#11252d80;--teal-surface:#13272580;--jade-surface:#13271f80;--green-surface:#15251d80;--grass-surface:#19231b80;--brown-surface:#271f1b80;--bronze-surface:#27211d80;--gold-surface:#25231d80;--sky-surface:#13233b80;--mint-surface:#15272780;--lime-surface:#1b211580;--yellow-surface:#231f1380;--amber-surface:#271f1380;--orange-surface:#271d1380}@supports (color:color(display-p3 1 1 1)){@media (color-gamut:p3){.dark,.dark-theme{--gray-surface:color(display-p3 .1255 .1255 .1255/.5);--mauve-surface:color(display-p3 .1333 .1255 .1333/.5);--slate-surface:color(display-p3 .1176 .1255 .1333/.5);--sage-surface:color(display-p3 .1176 .1255 .1176/.5);--olive-surface:color(display-p3 .1176 .1255 .1176/.5);--sand-surface:color(display-p3 .1255 .1255 .1255/.5);--tomato-surface:color(display-p3 .1569 .0941 .0784/.5);--red-surface:color(display-p3 .1647 .0863 .0863/.5);--ruby-surface:color(display-p3 .1569 .0941 .1098/.5);--crimson-surface:color(display-p3 .1647 .0863 .1176/.5);--pink-surface:color(display-p3 .1725 .0784 .149/.5);--plum-surface:color(display-p3 .1647 .0863 .1725/.5);--purple-surface:color(display-p3 .149 .0941 .1961/.5);--violet-surface:color(display-p3 .1333 .102 .2118/.5);--iris-surface:color(display-p3 .1098 .102 .2118/.5);--indigo-surface:color(display-p3 .0941 .1098 .2196/.5);--blue-surface:color(display-p3 .0706 .1255 .2196/.5);--cyan-surface:color(display-p3 .0784 .1412 .1725/.5);--teal-surface:color(display-p3 .0863 .149 .1412/.5);--jade-surface:color(display-p3 .0863 .149 .1176/.5);--green-surface:color(display-p3 .0941 .1412 .1098/.5);--grass-surface:color(display-p3 .102 .1333 .102/.5);--brown-surface:color(display-p3 .1412 .1176 .102/.5);--bronze-surface:color(display-p3 .1412 .1255 .1176/.5);--gold-surface:color(display-p3 .1412 .1333 .1098/.5);--sky-surface:color(display-p3 .0863 .1333 .2196/.5);--mint-surface:color(display-p3 .0941 .149 .1412/.5);--lime-surface:color(display-p3 .1098 .1255 .0784/.5);--yellow-surface:color(display-p3 .1333 .1176 .0706/.5);--amber-surface:color(display-p3 .1412 .1176 .0784/.5);--orange-surface:color(display-p3 .1412 .1098 .0706/.5)}}}[data-accent-color=tomato]{--color-surface-accent:var(--tomato-surface);--accent-1:var(--tomato-1);--accent-2:var(--tomato-2);--accent-3:var(--tomato-3);--accent-4:var(--tomato-4);--accent-5:var(--tomato-5);--accent-6:var(--tomato-6);--accent-7:var(--tomato-7);--accent-8:var(--tomato-8);--accent-9:var(--tomato-9);--accent-contrast:var(--tomato-contrast);--accent-10:var(--tomato-10);--accent-11:var(--tomato-11);--accent-12:var(--tomato-12);--accent-a1:var(--tomato-a1);--accent-a2:var(--tomato-a2);--accent-a3:var(--tomato-a3);--accent-a4:var(--tomato-a4);--accent-a5:var(--tomato-a5);--accent-a6:var(--tomato-a6);--accent-a7:var(--tomato-a7);--accent-a8:var(--tomato-a8);--accent-a9:var(--tomato-a9);--accent-a10:var(--tomato-a10);--accent-a11:var(--tomato-a11);--accent-a12:var(--tomato-a12)}[data-accent-color=red]{--color-surface-accent:var(--red-surface);--accent-1:var(--red-1);--accent-2:var(--red-2);--accent-3:var(--red-3);--accent-4:var(--red-4);--accent-5:var(--red-5);--accent-6:var(--red-6);--accent-7:var(--red-7);--accent-8:var(--red-8);--accent-9:var(--red-9);--accent-contrast:var(--red-contrast);--accent-10:var(--red-10);--accent-11:var(--red-11);--accent-12:var(--red-12);--accent-a1:var(--red-a1);--accent-a2:var(--red-a2);--accent-a3:var(--red-a3);--accent-a4:var(--red-a4);--accent-a5:var(--red-a5);--accent-a6:var(--red-a6);--accent-a7:var(--red-a7);--accent-a8:var(--red-a8);--accent-a9:var(--red-a9);--accent-a10:var(--red-a10);--accent-a11:var(--red-a11);--accent-a12:var(--red-a12)}[data-accent-color=ruby]{--color-surface-accent:var(--ruby-surface);--accent-1:var(--ruby-1);--accent-2:var(--ruby-2);--accent-3:var(--ruby-3);--accent-4:var(--ruby-4);--accent-5:var(--ruby-5);--accent-6:var(--ruby-6);--accent-7:var(--ruby-7);--accent-8:var(--ruby-8);--accent-9:var(--ruby-9);--accent-contrast:var(--ruby-contrast);--accent-10:var(--ruby-10);--accent-11:var(--ruby-11);--accent-12:var(--ruby-12);--accent-a1:var(--ruby-a1);--accent-a2:var(--ruby-a2);--accent-a3:var(--ruby-a3);--accent-a4:var(--ruby-a4);--accent-a5:var(--ruby-a5);--accent-a6:var(--ruby-a6);--accent-a7:var(--ruby-a7);--accent-a8:var(--ruby-a8);--accent-a9:var(--ruby-a9);--accent-a10:var(--ruby-a10);--accent-a11:var(--ruby-a11);--accent-a12:var(--ruby-a12)}[data-accent-color=crimson]{--color-surface-accent:var(--crimson-surface);--accent-1:var(--crimson-1);--accent-2:var(--crimson-2);--accent-3:var(--crimson-3);--accent-4:var(--crimson-4);--accent-5:var(--crimson-5);--accent-6:var(--crimson-6);--accent-7:var(--crimson-7);--accent-8:var(--crimson-8);--accent-9:var(--crimson-9);--accent-contrast:var(--crimson-contrast);--accent-10:var(--crimson-10);--accent-11:var(--crimson-11);--accent-12:var(--crimson-12);--accent-a1:var(--crimson-a1);--accent-a2:var(--crimson-a2);--accent-a3:var(--crimson-a3);--accent-a4:var(--crimson-a4);--accent-a5:var(--crimson-a5);--accent-a6:var(--crimson-a6);--accent-a7:var(--crimson-a7);--accent-a8:var(--crimson-a8);--accent-a9:var(--crimson-a9);--accent-a10:var(--crimson-a10);--accent-a11:var(--crimson-a11);--accent-a12:var(--crimson-a12)}[data-accent-color=pink]{--color-surface-accent:var(--pink-surface);--accent-1:var(--pink-1);--accent-2:var(--pink-2);--accent-3:var(--pink-3);--accent-4:var(--pink-4);--accent-5:var(--pink-5);--accent-6:var(--pink-6);--accent-7:var(--pink-7);--accent-8:var(--pink-8);--accent-9:var(--pink-9);--accent-contrast:var(--pink-contrast);--accent-10:var(--pink-10);--accent-11:var(--pink-11);--accent-12:var(--pink-12);--accent-a1:var(--pink-a1);--accent-a2:var(--pink-a2);--accent-a3:var(--pink-a3);--accent-a4:var(--pink-a4);--accent-a5:var(--pink-a5);--accent-a6:var(--pink-a6);--accent-a7:var(--pink-a7);--accent-a8:var(--pink-a8);--accent-a9:var(--pink-a9);--accent-a10:var(--pink-a10);--accent-a11:var(--pink-a11);--accent-a12:var(--pink-a12)}[data-accent-color=plum]{--color-surface-accent:var(--plum-surface);--accent-1:var(--plum-1);--accent-2:var(--plum-2);--accent-3:var(--plum-3);--accent-4:var(--plum-4);--accent-5:var(--plum-5);--accent-6:var(--plum-6);--accent-7:var(--plum-7);--accent-8:var(--plum-8);--accent-9:var(--plum-9);--accent-contrast:var(--plum-contrast);--accent-10:var(--plum-10);--accent-11:var(--plum-11);--accent-12:var(--plum-12);--accent-a1:var(--plum-a1);--accent-a2:var(--plum-a2);--accent-a3:var(--plum-a3);--accent-a4:var(--plum-a4);--accent-a5:var(--plum-a5);--accent-a6:var(--plum-a6);--accent-a7:var(--plum-a7);--accent-a8:var(--plum-a8);--accent-a9:var(--plum-a9);--accent-a10:var(--plum-a10);--accent-a11:var(--plum-a11);--accent-a12:var(--plum-a12)}[data-accent-color=purple]{--color-surface-accent:var(--purple-surface);--accent-1:var(--purple-1);--accent-2:var(--purple-2);--accent-3:var(--purple-3);--accent-4:var(--purple-4);--accent-5:var(--purple-5);--accent-6:var(--purple-6);--accent-7:var(--purple-7);--accent-8:var(--purple-8);--accent-9:var(--purple-9);--accent-contrast:var(--purple-contrast);--accent-10:var(--purple-10);--accent-11:var(--purple-11);--accent-12:var(--purple-12);--accent-a1:var(--purple-a1);--accent-a2:var(--purple-a2);--accent-a3:var(--purple-a3);--accent-a4:var(--purple-a4);--accent-a5:var(--purple-a5);--accent-a6:var(--purple-a6);--accent-a7:var(--purple-a7);--accent-a8:var(--purple-a8);--accent-a9:var(--purple-a9);--accent-a10:var(--purple-a10);--accent-a11:var(--purple-a11);--accent-a12:var(--purple-a12)}[data-accent-color=violet]{--color-surface-accent:var(--violet-surface);--accent-1:var(--violet-1);--accent-2:var(--violet-2);--accent-3:var(--violet-3);--accent-4:var(--violet-4);--accent-5:var(--violet-5);--accent-6:var(--violet-6);--accent-7:var(--violet-7);--accent-8:var(--violet-8);--accent-9:var(--violet-9);--accent-contrast:var(--violet-contrast);--accent-10:var(--violet-10);--accent-11:var(--violet-11);--accent-12:var(--violet-12);--accent-a1:var(--violet-a1);--accent-a2:var(--violet-a2);--accent-a3:var(--violet-a3);--accent-a4:var(--violet-a4);--accent-a5:var(--violet-a5);--accent-a6:var(--violet-a6);--accent-a7:var(--violet-a7);--accent-a8:var(--violet-a8);--accent-a9:var(--violet-a9);--accent-a10:var(--violet-a10);--accent-a11:var(--violet-a11);--accent-a12:var(--violet-a12)}[data-accent-color=iris]{--color-surface-accent:var(--iris-surface);--accent-1:var(--iris-1);--accent-2:var(--iris-2);--accent-3:var(--iris-3);--accent-4:var(--iris-4);--accent-5:var(--iris-5);--accent-6:var(--iris-6);--accent-7:var(--iris-7);--accent-8:var(--iris-8);--accent-9:var(--iris-9);--accent-contrast:var(--iris-contrast);--accent-10:var(--iris-10);--accent-11:var(--iris-11);--accent-12:var(--iris-12);--accent-a1:var(--iris-a1);--accent-a2:var(--iris-a2);--accent-a3:var(--iris-a3);--accent-a4:var(--iris-a4);--accent-a5:var(--iris-a5);--accent-a6:var(--iris-a6);--accent-a7:var(--iris-a7);--accent-a8:var(--iris-a8);--accent-a9:var(--iris-a9);--accent-a10:var(--iris-a10);--accent-a11:var(--iris-a11);--accent-a12:var(--iris-a12)}[data-accent-color=indigo]{--color-surface-accent:var(--indigo-surface);--accent-1:var(--indigo-1);--accent-2:var(--indigo-2);--accent-3:var(--indigo-3);--accent-4:var(--indigo-4);--accent-5:var(--indigo-5);--accent-6:var(--indigo-6);--accent-7:var(--indigo-7);--accent-8:var(--indigo-8);--accent-9:var(--indigo-9);--accent-contrast:var(--indigo-contrast);--accent-10:var(--indigo-10);--accent-11:var(--indigo-11);--accent-12:var(--indigo-12);--accent-a1:var(--indigo-a1);--accent-a2:var(--indigo-a2);--accent-a3:var(--indigo-a3);--accent-a4:var(--indigo-a4);--accent-a5:var(--indigo-a5);--accent-a6:var(--indigo-a6);--accent-a7:var(--indigo-a7);--accent-a8:var(--indigo-a8);--accent-a9:var(--indigo-a9);--accent-a10:var(--indigo-a10);--accent-a11:var(--indigo-a11);--accent-a12:var(--indigo-a12)}[data-accent-color=blue]{--color-surface-accent:var(--blue-surface);--accent-1:var(--blue-1);--accent-2:var(--blue-2);--accent-3:var(--blue-3);--accent-4:var(--blue-4);--accent-5:var(--blue-5);--accent-6:var(--blue-6);--accent-7:var(--blue-7);--accent-8:var(--blue-8);--accent-9:var(--blue-9);--accent-contrast:var(--blue-contrast);--accent-10:var(--blue-10);--accent-11:var(--blue-11);--accent-12:var(--blue-12);--accent-a1:var(--blue-a1);--accent-a2:var(--blue-a2);--accent-a3:var(--blue-a3);--accent-a4:var(--blue-a4);--accent-a5:var(--blue-a5);--accent-a6:var(--blue-a6);--accent-a7:var(--blue-a7);--accent-a8:var(--blue-a8);--accent-a9:var(--blue-a9);--accent-a10:var(--blue-a10);--accent-a11:var(--blue-a11);--accent-a12:var(--blue-a12)}[data-accent-color=cyan]{--color-surface-accent:var(--cyan-surface);--accent-1:var(--cyan-1);--accent-2:var(--cyan-2);--accent-3:var(--cyan-3);--accent-4:var(--cyan-4);--accent-5:var(--cyan-5);--accent-6:var(--cyan-6);--accent-7:var(--cyan-7);--accent-8:var(--cyan-8);--accent-9:var(--cyan-9);--accent-contrast:var(--cyan-contrast);--accent-10:var(--cyan-10);--accent-11:var(--cyan-11);--accent-12:var(--cyan-12);--accent-a1:var(--cyan-a1);--accent-a2:var(--cyan-a2);--accent-a3:var(--cyan-a3);--accent-a4:var(--cyan-a4);--accent-a5:var(--cyan-a5);--accent-a6:var(--cyan-a6);--accent-a7:var(--cyan-a7);--accent-a8:var(--cyan-a8);--accent-a9:var(--cyan-a9);--accent-a10:var(--cyan-a10);--accent-a11:var(--cyan-a11);--accent-a12:var(--cyan-a12)}[data-accent-color=teal]{--color-surface-accent:var(--teal-surface);--accent-1:var(--teal-1);--accent-2:var(--teal-2);--accent-3:var(--teal-3);--accent-4:var(--teal-4);--accent-5:var(--teal-5);--accent-6:var(--teal-6);--accent-7:var(--teal-7);--accent-8:var(--teal-8);--accent-9:var(--teal-9);--accent-contrast:var(--teal-contrast);--accent-10:var(--teal-10);--accent-11:var(--teal-11);--accent-12:var(--teal-12);--accent-a1:var(--teal-a1);--accent-a2:var(--teal-a2);--accent-a3:var(--teal-a3);--accent-a4:var(--teal-a4);--accent-a5:var(--teal-a5);--accent-a6:var(--teal-a6);--accent-a7:var(--teal-a7);--accent-a8:var(--teal-a8);--accent-a9:var(--teal-a9);--accent-a10:var(--teal-a10);--accent-a11:var(--teal-a11);--accent-a12:var(--teal-a12)}[data-accent-color=jade]{--color-surface-accent:var(--jade-surface);--accent-1:var(--jade-1);--accent-2:var(--jade-2);--accent-3:var(--jade-3);--accent-4:var(--jade-4);--accent-5:var(--jade-5);--accent-6:var(--jade-6);--accent-7:var(--jade-7);--accent-8:var(--jade-8);--accent-9:var(--jade-9);--accent-contrast:var(--jade-contrast);--accent-10:var(--jade-10);--accent-11:var(--jade-11);--accent-12:var(--jade-12);--accent-a1:var(--jade-a1);--accent-a2:var(--jade-a2);--accent-a3:var(--jade-a3);--accent-a4:var(--jade-a4);--accent-a5:var(--jade-a5);--accent-a6:var(--jade-a6);--accent-a7:var(--jade-a7);--accent-a8:var(--jade-a8);--accent-a9:var(--jade-a9);--accent-a10:var(--jade-a10);--accent-a11:var(--jade-a11);--accent-a12:var(--jade-a12)}[data-accent-color=green]{--color-surface-accent:var(--green-surface);--accent-1:var(--green-1);--accent-2:var(--green-2);--accent-3:var(--green-3);--accent-4:var(--green-4);--accent-5:var(--green-5);--accent-6:var(--green-6);--accent-7:var(--green-7);--accent-8:var(--green-8);--accent-9:var(--green-9);--accent-contrast:var(--green-contrast);--accent-10:var(--green-10);--accent-11:var(--green-11);--accent-12:var(--green-12);--accent-a1:var(--green-a1);--accent-a2:var(--green-a2);--accent-a3:var(--green-a3);--accent-a4:var(--green-a4);--accent-a5:var(--green-a5);--accent-a6:var(--green-a6);--accent-a7:var(--green-a7);--accent-a8:var(--green-a8);--accent-a9:var(--green-a9);--accent-a10:var(--green-a10);--accent-a11:var(--green-a11);--accent-a12:var(--green-a12)}[data-accent-color=grass]{--color-surface-accent:var(--grass-surface);--accent-1:var(--grass-1);--accent-2:var(--grass-2);--accent-3:var(--grass-3);--accent-4:var(--grass-4);--accent-5:var(--grass-5);--accent-6:var(--grass-6);--accent-7:var(--grass-7);--accent-8:var(--grass-8);--accent-9:var(--grass-9);--accent-contrast:var(--grass-contrast);--accent-10:var(--grass-10);--accent-11:var(--grass-11);--accent-12:var(--grass-12);--accent-a1:var(--grass-a1);--accent-a2:var(--grass-a2);--accent-a3:var(--grass-a3);--accent-a4:var(--grass-a4);--accent-a5:var(--grass-a5);--accent-a6:var(--grass-a6);--accent-a7:var(--grass-a7);--accent-a8:var(--grass-a8);--accent-a9:var(--grass-a9);--accent-a10:var(--grass-a10);--accent-a11:var(--grass-a11);--accent-a12:var(--grass-a12)}[data-accent-color=orange]{--color-surface-accent:var(--orange-surface);--accent-1:var(--orange-1);--accent-2:var(--orange-2);--accent-3:var(--orange-3);--accent-4:var(--orange-4);--accent-5:var(--orange-5);--accent-6:var(--orange-6);--accent-7:var(--orange-7);--accent-8:var(--orange-8);--accent-9:var(--orange-9);--accent-contrast:var(--orange-contrast);--accent-10:var(--orange-10);--accent-11:var(--orange-11);--accent-12:var(--orange-12);--accent-a1:var(--orange-a1);--accent-a2:var(--orange-a2);--accent-a3:var(--orange-a3);--accent-a4:var(--orange-a4);--accent-a5:var(--orange-a5);--accent-a6:var(--orange-a6);--accent-a7:var(--orange-a7);--accent-a8:var(--orange-a8);--accent-a9:var(--orange-a9);--accent-a10:var(--orange-a10);--accent-a11:var(--orange-a11);--accent-a12:var(--orange-a12)}[data-accent-color=brown]{--color-surface-accent:var(--brown-surface);--accent-1:var(--brown-1);--accent-2:var(--brown-2);--accent-3:var(--brown-3);--accent-4:var(--brown-4);--accent-5:var(--brown-5);--accent-6:var(--brown-6);--accent-7:var(--brown-7);--accent-8:var(--brown-8);--accent-9:var(--brown-9);--accent-contrast:var(--brown-contrast);--accent-10:var(--brown-10);--accent-11:var(--brown-11);--accent-12:var(--brown-12);--accent-a1:var(--brown-a1);--accent-a2:var(--brown-a2);--accent-a3:var(--brown-a3);--accent-a4:var(--brown-a4);--accent-a5:var(--brown-a5);--accent-a6:var(--brown-a6);--accent-a7:var(--brown-a7);--accent-a8:var(--brown-a8);--accent-a9:var(--brown-a9);--accent-a10:var(--brown-a10);--accent-a11:var(--brown-a11);--accent-a12:var(--brown-a12)}[data-accent-color=sky]{--color-surface-accent:var(--sky-surface);--accent-1:var(--sky-1);--accent-2:var(--sky-2);--accent-3:var(--sky-3);--accent-4:var(--sky-4);--accent-5:var(--sky-5);--accent-6:var(--sky-6);--accent-7:var(--sky-7);--accent-8:var(--sky-8);--accent-9:var(--sky-9);--accent-contrast:var(--sky-contrast);--accent-10:var(--sky-10);--accent-11:var(--sky-11);--accent-12:var(--sky-12);--accent-a1:var(--sky-a1);--accent-a2:var(--sky-a2);--accent-a3:var(--sky-a3);--accent-a4:var(--sky-a4);--accent-a5:var(--sky-a5);--accent-a6:var(--sky-a6);--accent-a7:var(--sky-a7);--accent-a8:var(--sky-a8);--accent-a9:var(--sky-a9);--accent-a10:var(--sky-a10);--accent-a11:var(--sky-a11);--accent-a12:var(--sky-a12)}[data-accent-color=mint]{--color-surface-accent:var(--mint-surface);--accent-1:var(--mint-1);--accent-2:var(--mint-2);--accent-3:var(--mint-3);--accent-4:var(--mint-4);--accent-5:var(--mint-5);--accent-6:var(--mint-6);--accent-7:var(--mint-7);--accent-8:var(--mint-8);--accent-9:var(--mint-9);--accent-contrast:var(--mint-contrast);--accent-10:var(--mint-10);--accent-11:var(--mint-11);--accent-12:var(--mint-12);--accent-a1:var(--mint-a1);--accent-a2:var(--mint-a2);--accent-a3:var(--mint-a3);--accent-a4:var(--mint-a4);--accent-a5:var(--mint-a5);--accent-a6:var(--mint-a6);--accent-a7:var(--mint-a7);--accent-a8:var(--mint-a8);--accent-a9:var(--mint-a9);--accent-a10:var(--mint-a10);--accent-a11:var(--mint-a11);--accent-a12:var(--mint-a12)}[data-accent-color=lime]{--color-surface-accent:var(--lime-surface);--accent-1:var(--lime-1);--accent-2:var(--lime-2);--accent-3:var(--lime-3);--accent-4:var(--lime-4);--accent-5:var(--lime-5);--accent-6:var(--lime-6);--accent-7:var(--lime-7);--accent-8:var(--lime-8);--accent-9:var(--lime-9);--accent-contrast:var(--lime-contrast);--accent-10:var(--lime-10);--accent-11:var(--lime-11);--accent-12:var(--lime-12);--accent-a1:var(--lime-a1);--accent-a2:var(--lime-a2);--accent-a3:var(--lime-a3);--accent-a4:var(--lime-a4);--accent-a5:var(--lime-a5);--accent-a6:var(--lime-a6);--accent-a7:var(--lime-a7);--accent-a8:var(--lime-a8);--accent-a9:var(--lime-a9);--accent-a10:var(--lime-a10);--accent-a11:var(--lime-a11);--accent-a12:var(--lime-a12)}[data-accent-color=yellow]{--color-surface-accent:var(--yellow-surface);--accent-1:var(--yellow-1);--accent-2:var(--yellow-2);--accent-3:var(--yellow-3);--accent-4:var(--yellow-4);--accent-5:var(--yellow-5);--accent-6:var(--yellow-6);--accent-7:var(--yellow-7);--accent-8:var(--yellow-8);--accent-9:var(--yellow-9);--accent-contrast:var(--yellow-contrast);--accent-10:var(--yellow-10);--accent-11:var(--yellow-11);--accent-12:var(--yellow-12);--accent-a1:var(--yellow-a1);--accent-a2:var(--yellow-a2);--accent-a3:var(--yellow-a3);--accent-a4:var(--yellow-a4);--accent-a5:var(--yellow-a5);--accent-a6:var(--yellow-a6);--accent-a7:var(--yellow-a7);--accent-a8:var(--yellow-a8);--accent-a9:var(--yellow-a9);--accent-a10:var(--yellow-a10);--accent-a11:var(--yellow-a11);--accent-a12:var(--yellow-a12)}[data-accent-color=amber]{--color-surface-accent:var(--amber-surface);--accent-1:var(--amber-1);--accent-2:var(--amber-2);--accent-3:var(--amber-3);--accent-4:var(--amber-4);--accent-5:var(--amber-5);--accent-6:var(--amber-6);--accent-7:var(--amber-7);--accent-8:var(--amber-8);--accent-9:var(--amber-9);--accent-contrast:var(--amber-contrast);--accent-10:var(--amber-10);--accent-11:var(--amber-11);--accent-12:var(--amber-12);--accent-a1:var(--amber-a1);--accent-a2:var(--amber-a2);--accent-a3:var(--amber-a3);--accent-a4:var(--amber-a4);--accent-a5:var(--amber-a5);--accent-a6:var(--amber-a6);--accent-a7:var(--amber-a7);--accent-a8:var(--amber-a8);--accent-a9:var(--amber-a9);--accent-a10:var(--amber-a10);--accent-a11:var(--amber-a11);--accent-a12:var(--amber-a12)}[data-accent-color=gold]{--color-surface-accent:var(--gold-surface);--accent-1:var(--gold-1);--accent-2:var(--gold-2);--accent-3:var(--gold-3);--accent-4:var(--gold-4);--accent-5:var(--gold-5);--accent-6:var(--gold-6);--accent-7:var(--gold-7);--accent-8:var(--gold-8);--accent-9:var(--gold-9);--accent-contrast:var(--gold-contrast);--accent-10:var(--gold-10);--accent-11:var(--gold-11);--accent-12:var(--gold-12);--accent-a1:var(--gold-a1);--accent-a2:var(--gold-a2);--accent-a3:var(--gold-a3);--accent-a4:var(--gold-a4);--accent-a5:var(--gold-a5);--accent-a6:var(--gold-a6);--accent-a7:var(--gold-a7);--accent-a8:var(--gold-a8);--accent-a9:var(--gold-a9);--accent-a10:var(--gold-a10);--accent-a11:var(--gold-a11);--accent-a12:var(--gold-a12)}[data-accent-color=bronze]{--color-surface-accent:var(--bronze-surface);--accent-1:var(--bronze-1);--accent-2:var(--bronze-2);--accent-3:var(--bronze-3);--accent-4:var(--bronze-4);--accent-5:var(--bronze-5);--accent-6:var(--bronze-6);--accent-7:var(--bronze-7);--accent-8:var(--bronze-8);--accent-9:var(--bronze-9);--accent-contrast:var(--bronze-contrast);--accent-10:var(--bronze-10);--accent-11:var(--bronze-11);--accent-12:var(--bronze-12);--accent-a1:var(--bronze-a1);--accent-a2:var(--bronze-a2);--accent-a3:var(--bronze-a3);--accent-a4:var(--bronze-a4);--accent-a5:var(--bronze-a5);--accent-a6:var(--bronze-a6);--accent-a7:var(--bronze-a7);--accent-a8:var(--bronze-a8);--accent-a9:var(--bronze-a9);--accent-a10:var(--bronze-a10);--accent-a11:var(--bronze-a11);--accent-a12:var(--bronze-a12)}[data-accent-color=gray]{--color-surface-accent:var(--gray-surface);--accent-1:var(--gray-1);--accent-2:var(--gray-2);--accent-3:var(--gray-3);--accent-4:var(--gray-4);--accent-5:var(--gray-5);--accent-6:var(--gray-6);--accent-7:var(--gray-7);--accent-8:var(--gray-8);--accent-9:var(--gray-9);--accent-contrast:var(--gray-contrast);--accent-10:var(--gray-10);--accent-11:var(--gray-11);--accent-12:var(--gray-12);--accent-a1:var(--gray-a1);--accent-a2:var(--gray-a2);--accent-a3:var(--gray-a3);--accent-a4:var(--gray-a4);--accent-a5:var(--gray-a5);--accent-a6:var(--gray-a6);--accent-a7:var(--gray-a7);--accent-a8:var(--gray-a8);--accent-a9:var(--gray-a9);--accent-a10:var(--gray-a10);--accent-a11:var(--gray-a11);--accent-a12:var(--gray-a12)}:root{--sy-f-sys:-apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Droid Sans, Helvetica Neue;--sy-f-cjk:Noto Sans;--sy-f-heading:var(--sy-f-sys), var(--sy-f-cjk), sans-serif;--sy-f-text:var(--sy-f-sys), var(--sy-f-cjk), sans-serif;--sy-f-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--sy-s-banner-height:0rem;--sy-s-navbar-height:56px;--sy-s-offset-top:calc(var(--sy-s-navbar-height) + var(--sy-s-banner-height));--sy-c-divider:var(--gray-4);--sy-c-border:var(--gray-5);--sy-c-text:var(--gray-12);--sy-c-light:var(--gray-11);--sy-c-bold:var(--slate-12);--sy-c-heading:var(--sage-12);--sy-c-link:var(--accent-9);--sy-c-link-hover:var(--accent-a11);--sy-c-background:white;--sy-c-surface:var(--gray-a2);--sy-c-overlay:var(--black-a5);--sy-c-background-contrast:black;--sy-c-background-dropback:#fffc;--sy-c-foot-text:var(--sy-c-text);--sy-c-foot-background:var(--slate-2);--sy-c-foot-divider:var(--sy-c-divider);--sy-dropdown-shadow:0 12px 32px var(--gray-a6), 0 2px 6px var(--gray-a4)}[lang^=zh-Hans],[lang=zh],[lang=zh-CN]{--sy-f-cjk:PingFang SC, Hiragino Sans GB, Noto Sans SC, Microsoft YaHei}[lang^=zh-Hant],[lang=zh-TW]{--sy-f-cjk:PingFang TC, Noto Sans TC, Microsoft JhengHei}[lang=zh-HK],[lang=zh-Hant-HK]{--sy-f-cjk:PingFang HK, Noto Sans HK, Microsoft JhengHei}[lang=ja]{--sy-f-cjk:Hiragino Sans, Noto Sans JP, Yu Gothic}[data-accent-color=sky],[data-accent-color=mint],[data-accent-color=lime],[data-accent-color=yellow],[data-accent-color=amber]{--sy-c-link:var(--accent-a10)}html.light{color-scheme:light;--sy-c-background:white;--sy-c-background-contrast:black}html.dark{color-scheme:dark;--sy-c-background:var(--slate-1);--sy-c-overlay:var(--white-a2);--sy-c-background-contrast:var(--white-a10);--sy-c-foot-background:var(--black-a11);--sy-c-foot-divider:var(--black-a12);--sy-c-background-dropback:#212328cc}html{color:var(--sy-c-text);background-color:var(--sy-c-background)}:root{--lucide-alert-url:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Cpath d='m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3zM12 9v4m0 4h.01'/%3E%3C/svg%3E");--lucide-arrows-url:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Cpath d='m7 15 5 5 5-5M7 9l5-5 5 5'/%3E%3C/svg%3E");--lucide-award-url:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Ccircle cx='12' cy='8' r='6'/%3E%3Cpath d='M15.477 12.89 17 22l-5-3-5 3 1.523-9.11'/%3E%3C/svg%3E");--lucide-bell-url:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Cpath d='M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9m-4.27 13a2 2 0 0 1-3.46 0'/%3E%3C/svg%3E");--lucide-bookmark-url:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Cpath d='m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z'/%3E%3C/svg%3E");--lucide-calendar-url:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Crect width='18' height='18' x='3' y='4' rx='2' ry='2'/%3E%3Cpath d='M16 2v4M8 2v4m-5 4h18'/%3E%3C/svg%3E");--lucide-check-url:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");--lucide-chevron-down-url:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' class='lucide lucide-chevron-down'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");--lucide-chevron-left-url:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' class='lucide lucide-chevron-left'%3E%3Cpath d='m15 18-6-6 6-6'/%3E%3C/svg%3E");--lucide-chevron-right-url:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' class='lucide lucide-chevron-right'%3E%3Cpath d='m9 18 6-6-6-6'/%3E%3C/svg%3E");--lucide-chevron-up-url:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' class='lucide lucide-chevron-up'%3E%3Cpath d='m18 15-6-6-6 6'/%3E%3C/svg%3E");--lucide-close-url:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Cpath d='M18 6 6 18M6 6l12 12'/%3E%3C/svg%3E");--lucide-copy-url:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' class='lucide lucide-copy-icon lucide-copy' viewBox='0 0 24 24'%3E%3Crect width='14' height='14' x='8' y='8' rx='2' ry='2'/%3E%3Cpath d='M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2'/%3E%3C/svg%3E");--lucide-external-link-url:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Cpath d='M13 5h6v6m0-6L5 19'/%3E%3C/svg%3E");--lucide-flame-url:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Cpath d='M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z'/%3E%3C/svg%3E");--lucide-git-fork-url:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Ccircle cx='12' cy='18' r='3'/%3E%3Ccircle cx='6' cy='6' r='3'/%3E%3Ccircle cx='18' cy='6' r='3'/%3E%3Cpath d='M18 9v1a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2V9m6 3v3'/%3E%3C/svg%3E");--lucide-help-url:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3m.08 4h.01'/%3E%3C/svg%3E");--lucide-languages-url:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' class='lucide lucide-languages'%3E%3Cpath d='m5 8 6 6m-7 0 6-6 2-3M2 5h12M7 2h1m14 20-5-10-5 10m2-4h6'/%3E%3C/svg%3E");--lucide-laptop-url:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Crect width='18' height='12' x='3' y='4' rx='2' ry='2'/%3E%3Cpath d='M2 20h20'/%3E%3C/svg%3E");--lucide-link-url:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Cpath d='M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71'/%3E%3Cpath d='M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71'/%3E%3C/svg%3E");--lucide-loader-url:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' class='lucide lucide-loader-icon lucide-loader' viewBox='0 0 24 24'%3E%3Cpath d='M12 2v4M16.2 7.8l2.9-2.9M18 12h4M16.2 16.2l2.9 2.9M12 18v4M4.9 19.1l2.9-2.9M2 12h4M4.9 4.9l2.9 2.9'/%3E%3C/svg%3E");--lucide-menu-url:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' class='lucide lucide-menu'%3E%3Cpath d='M4 12h16M4 6h16M4 18h16'/%3E%3C/svg%3E");--lucide-milestone-url:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Cpath d='M18 6H5a2 2 0 0 0-2 2v3a2 2 0 0 0 2 2h13l4-3.5L18 6zm-6 7v8m0-18v3'/%3E%3C/svg%3E");--lucide-moon-url:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Cpath d='M12 3a6.364 6.364 0 0 0 9 9 9 9 0 1 1-9-9z'/%3E%3C/svg%3E");--lucide-outdent-url:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Cpath d='m7 8-4 4 4 4m14-4H11m10-6H11m10 12H11'/%3E%3C/svg%3E");--lucide-rocket-url:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Cpath d='M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09zM12 15l-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z'/%3E%3Cpath d='M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0m1 7v5s3.03-.55 4-2c1.08-1.62 0-5 0-5'/%3E%3C/svg%3E");--lucide-skull-url:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Ccircle cx='9' cy='12' r='1'/%3E%3Ccircle cx='15' cy='12' r='1'/%3E%3Cpath d='M8 20v2h8v-2m-3.5-3-.5-1-.5 1h1z'/%3E%3Cpath d='M16 20a2 2 0 0 0 1.56-3.25 8 8 0 1 0-11.12 0A2 2 0 0 0 8 20'/%3E%3C/svg%3E");--lucide-star-url:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Cpath d='m12 2 3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z'/%3E%3C/svg%3E");--lucide-sun-moon-url:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' class='lucide lucide-sun-moon-icon lucide-sun-moon' viewBox='0 0 24 24'%3E%3Cpath d='M12 2v2M14.837 16.385a6 6 0 1 1-7.223-7.222c.624-.147.97.66.715 1.248a4 4 0 0 0 5.26 5.259c.589-.255 1.396.09 1.248.715M16 12a4 4 0 0 0-4-4M19 5l-1.256 1.256M20 12h2'/%3E%3C/svg%3E");--lucide-sun-url:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Ccircle cx='12' cy='12' r='4'/%3E%3Cpath d='M12 2v2m0 16v2M4.93 4.93l1.41 1.41m11.32 11.32 1.41 1.41M2 12h2m16 0h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41'/%3E%3C/svg%3E");--lucide-zap-url:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2'%3E%3Cpath d='M13 2 3 14h9l-1 8 10-12h-9l1-8z'/%3E%3C/svg%3E")}.i-lucide.alert{--icon-url:var(--lucide-alert-url)}.i-lucide.arrows{--icon-url:var(--lucide-arrows-url)}.i-lucide.award{--icon-url:var(--lucide-award-url)}.i-lucide.bell{--icon-url:var(--lucide-bell-url)}.i-lucide.bookmark{--icon-url:var(--lucide-bookmark-url)}.i-lucide.calendar{--icon-url:var(--lucide-calendar-url)}.i-lucide.check{--icon-url:var(--lucide-check-url)}.i-lucide.chevron-down{--icon-url:var(--lucide-chevron-down-url)}.i-lucide.chevron-left{--icon-url:var(--lucide-chevron-left-url)}.i-lucide.chevron-right{--icon-url:var(--lucide-chevron-right-url)}.i-lucide.chevron-up{--icon-url:var(--lucide-chevron-up-url)}.i-lucide.close{--icon-url:var(--lucide-close-url)}.i-lucide.copy{--icon-url:var(--lucide-copy-url)}.i-lucide.external-link{--icon-url:var(--lucide-external-link-url)}.i-lucide.flame{--icon-url:var(--lucide-flame-url)}.i-lucide.git-fork{--icon-url:var(--lucide-git-fork-url)}.i-lucide.help{--icon-url:var(--lucide-help-url)}.i-lucide.languages{--icon-url:var(--lucide-languages-url)}.i-lucide.laptop{--icon-url:var(--lucide-laptop-url)}.i-lucide.link{--icon-url:var(--lucide-link-url)}.i-lucide.loader{--icon-url:var(--lucide-loader-url)}.i-lucide.menu{--icon-url:var(--lucide-menu-url)}.i-lucide.milestone{--icon-url:var(--lucide-milestone-url)}.i-lucide.moon{--icon-url:var(--lucide-moon-url)}.i-lucide.outdent{--icon-url:var(--lucide-outdent-url)}.i-lucide.rocket{--icon-url:var(--lucide-rocket-url)}.i-lucide.skull{--icon-url:var(--lucide-skull-url)}.i-lucide.star{--icon-url:var(--lucide-star-url)}.i-lucide.sun-moon{--icon-url:var(--lucide-sun-moon-url)}.i-lucide.sun{--icon-url:var(--lucide-sun-url)}.i-lucide.zap{--icon-url:var(--lucide-zap-url)}:root{--simpleicons-bitbucket-url:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M.778 1.213a.768.768 0 0 0-.768.892l3.263 19.81c.084.5.515.868 1.022.873H19.95a.772.772 0 0 0 .77-.646l3.27-20.03a.768.768 0 0 0-.768-.891zM14.52 15.53H9.522L8.17 8.466h7.561z'/%3E%3C/svg%3E");--simpleicons-discord-url:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z'/%3E%3C/svg%3E");--simpleicons-git-url:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M23.546 10.93 13.067.452a1.55 1.55 0 0 0-2.188 0L8.708 2.627l2.76 2.76a1.838 1.838 0 0 1 2.327 2.341l2.658 2.66a1.838 1.838 0 0 1 1.9 3.039 1.837 1.837 0 0 1-2.6 0 1.846 1.846 0 0 1-.404-1.996L12.86 8.955v6.525c.176.086.342.203.488.348a1.848 1.848 0 0 1 0 2.6 1.844 1.844 0 0 1-2.609 0 1.834 1.834 0 0 1 0-2.598c.182-.18.387-.316.605-.406V8.835a1.834 1.834 0 0 1-.996-2.41L7.636 3.7.45 10.881c-.6.605-.6 1.584 0 2.189l10.48 10.477a1.545 1.545 0 0 0 2.186 0l10.43-10.43a1.544 1.544 0 0 0 0-2.187'/%3E%3C/svg%3E");--simpleicons-github-url:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E");--simpleicons-gitlab-url:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m23.6 9.593-.033-.086L20.3.98a.851.851 0 0 0-.336-.405.875.875 0 0 0-1 .054.875.875 0 0 0-.29.44L16.47 7.818H7.537L5.332 1.07a.857.857 0 0 0-.29-.441.875.875 0 0 0-1-.054.859.859 0 0 0-.336.405L.433 9.502l-.032.086a6.066 6.066 0 0 0 2.012 7.01l.01.009.03.021 4.977 3.727 2.462 1.863 1.5 1.132a1.008 1.008 0 0 0 1.22 0l1.499-1.132 2.461-1.863 5.006-3.75.013-.01a6.068 6.068 0 0 0 2.01-7.002z'/%3E%3C/svg%3E");--simpleicons-linkedin-url:url("data:image/svg+xml;utf8,%3Csvg role='img' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Ctitle%3ELinkedIn%3C/title%3E%3Cpath d='M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z'/%3E%3C/svg%3E");--simpleicons-mastodon-url:url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z'/%3E%3C/svg%3E");--simpleicons-readthedocs-url:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M7.732 0a59.316 59.316 0 0 0-4.977.218V24a62.933 62.933 0 0 1 3.619-.687c.17-.028.34-.053.509-.078.215-.033.43-.066.644-.096l.205-.03zm1.18.003V22.96a61.042 61.042 0 0 1 12.333-.213V1.485A60.859 60.859 0 0 0 8.912.003zm1.707 1.81a.59.59 0 0 1 .015 0c3.06.088 6.125.404 9.167.95a.59.59 0 0 1 .476.686.59.59 0 0 1-.569.484.59.59 0 0 1-.116-.009 60.622 60.622 0 0 0-8.992-.931.59.59 0 0 1-.573-.607.59.59 0 0 1 .592-.572zm-4.212.028a.59.59 0 0 1 .578.565.59.59 0 0 1-.564.614 59.74 59.74 0 0 0-2.355.144.59.59 0 0 1-.04.002.59.59 0 0 1-.595-.542.59.59 0 0 1 .54-.635c.8-.065 1.6-.114 2.401-.148a.59.59 0 0 1 .035 0zm4.202 2.834a.59.59 0 0 1 .015 0 61.6 61.6 0 0 1 9.167.8.59.59 0 0 1 .488.677.59.59 0 0 1-.602.494.59.59 0 0 1-.076-.006 60.376 60.376 0 0 0-8.99-.786.59.59 0 0 1-.584-.596.59.59 0 0 1 .582-.583zm-4.211.097a.59.59 0 0 1 .587.555.59.59 0 0 1-.554.622c-.786.046-1.572.107-2.356.184a.59.59 0 0 1-.04.003.59.59 0 0 1-.603-.533.59.59 0 0 1 .53-.644c.8-.078 1.599-.14 2.4-.187a.59.59 0 0 1 .036 0zM10.6 7.535a.59.59 0 0 1 .015 0c3.06-.013 6.125.204 9.167.65a.59.59 0 0 1 .498.67.59.59 0 0 1-.593.504.59.59 0 0 1-.076-.006 60.142 60.142 0 0 0-8.992-.638.59.59 0 0 1-.592-.588.59.59 0 0 1 .573-.592zm1.153 2.846a61.093 61.093 0 0 1 8.02.515.59.59 0 0 1 .509.66.59.59 0 0 1-.586.514.59.59 0 0 1-.076-.005 59.982 59.982 0 0 0-8.99-.492.59.59 0 0 1-.603-.577.59.59 0 0 1 .578-.603c.382-.008.765-.012 1.148-.012zm1.139 2.832a60.92 60.92 0 0 1 6.871.394.59.59 0 0 1 .52.652.59.59 0 0 1-.577.523.59.59 0 0 1-.076-.004 59.936 59.936 0 0 0-8.991-.344.59.59 0 0 1-.61-.568.59.59 0 0 1 .567-.611c.765-.028 1.53-.042 2.296-.042z'/%3E%3C/svg%3E");--simpleicons-reddit-url:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 0C5.373 0 0 5.373 0 12c0 3.314 1.343 6.314 3.515 8.485l-2.286 2.286A.72.72 0 0 0 1.738 24H12c6.627 0 12-5.373 12-12S18.627 0 12 0Zm4.388 3.199a1.999 1.999 0 1 1-1.947 2.46v.002a2.368 2.368 0 0 0-2.032 2.341v.007c1.776.067 3.4.567 4.686 1.363a2.802 2.802 0 1 1 2.908 4.753c-.088 3.256-3.637 5.876-7.997 5.876-4.361 0-7.905-2.617-7.998-5.87a2.8 2.8 0 0 1 1.189-5.34c.645 0 1.239.218 1.712.585 1.275-.79 2.881-1.291 4.64-1.365v-.01a3.229 3.229 0 0 1 2.88-3.207 2 2 0 0 1 1.959-1.595Zm-8.085 8.376c-.784 0-1.459.78-1.506 1.797-.047 1.016.64 1.429 1.426 1.429.786 0 1.371-.369 1.418-1.385.047-1.017-.553-1.841-1.338-1.841Zm7.406 0c-.786 0-1.385.824-1.338 1.841.047 1.017.634 1.385 1.418 1.385.785 0 1.473-.413 1.426-1.429-.046-1.017-.721-1.797-1.506-1.797Zm-3.703 4.013c-.974 0-1.907.048-2.77.135a.222.222 0 0 0-.183.305 3.199 3.199 0 0 0 2.953 1.964 3.2 3.2 0 0 0 2.953-1.964.222.222 0 0 0-.184-.305 27.75 27.75 0 0 0-2.769-.135Z'/%3E%3C/svg%3E");--simpleicons-slack-url:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zm1.271 0a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zm0 1.271a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zm10.122 2.521a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zm-1.268 0a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zm-2.523 10.122a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zm0-1.268a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z'/%3E%3C/svg%3E");--simpleicons-x-twitter-url:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z'/%3E%3C/svg%3E");--simpleicons-youtube-url:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z'/%3E%3C/svg%3E")}.i-icon.bitbucket,.i-simpleicons.bitbucket{--icon-url:var(--simpleicons-bitbucket-url)}.i-icon.discord,.i-simpleicons.discord{--icon-url:var(--simpleicons-discord-url)}.i-icon.git,.i-simpleicons.git{--icon-url:var(--simpleicons-git-url)}.i-icon.github,.i-simpleicons.github{--icon-url:var(--simpleicons-github-url)}.i-icon.gitlab,.i-simpleicons.gitlab{--icon-url:var(--simpleicons-gitlab-url)}.i-icon.linkedin,.i-simpleicons.linkedin{--icon-url:var(--simpleicons-linkedin-url)}.i-icon.mastodon,.i-simpleicons.mastodon{--icon-url:var(--simpleicons-mastodon-url)}.i-icon.readthedocs,.i-simpleicons.readthedocs{--icon-url:var(--simpleicons-readthedocs-url)}.i-icon.reddit,.i-simpleicons.reddit{--icon-url:var(--simpleicons-reddit-url)}.i-icon.slack,.i-simpleicons.slack{--icon-url:var(--simpleicons-slack-url)}.i-icon.x-twitter,.i-simpleicons.x-twitter{--icon-url:var(--simpleicons-x-twitter-url)}.i-icon.youtube,.i-simpleicons.youtube{--icon-url:var(--simpleicons-youtube-url)}:root{--yue-c-text:var(--sy-c-text);--yue-c-heading:var(--sy-c-heading);--yue-c-bold:var(--sy-c-bold);--yue-c-link-1:var(--sy-c-text);--yue-c-link-2:var(--sy-c-bold);--yue-c-link-border:var(--sy-c-link);--yue-c-ol-marker:var(--gray-9);--yue-c-ul-marker:var(--sage-a5);--yue-c-hr:var(--sy-c-border);--yue-c-quote:var(--sy-c-text);--yue-c-quote-border:var(--accent-a3);--yue-c-quote-symbol:var(--accent-9);--yue-c-caption:var(--sy-c-light);--yue-c-code-text:var(--accent-a11);--yue-c-code-background:var(--accent-a3);--yue-c-table-border:var(--gray-a5);--yue-c-th-background:var(--color-surface-accent);--yue-c-th-border:var(--gray-a5);--yue-c-td-border:var(--gray-a4);--yue-c-row-background:var(--sy-c-surface)}.yue{color:var(--yue-c-text);font-size:1rem;line-height:1.75}.yue p{margin-top:1rem;margin-bottom:1.25rem}.yue a{color:var(--yue-c-link-1);text-decoration:underline;-webkit-text-decoration-color:var(--yue-c-link-border);-webkit-text-decoration-color:var(--yue-c-link-border);text-decoration-color:var(--yue-c-link-border);font-weight:500;text-decoration-thickness:1px}.yue a:hover{color:var(--yue-c-link-2);text-decoration-thickness:2px}.yue pre{overflow:auto}.yue pre a,.yue pre a:hover{border-bottom:none;text-decoration:none}.yue strong{color:var(--yue-c-bold);font-weight:600}.yue a strong,.yue blockquote strong,.yue thead th strong{color:inherit}.yue ol{margin-top:1.25em;margin-bottom:1.25em;padding-left:1.625em;list-style-type:decimal}.yue ol[type="1"]{list-style-type:decimal}.yue ol[type=A],.yue ol.upperalpha{list-style-type:upper-alpha}.yue ol[type=a],.yue ol.loweralpha{list-style-type:lower-alpha}.yue ol[type=I],.yue ol.upperroman{list-style-type:upper-roman}.yue ol[type=i],.yue ol.lowerroman{list-style-type:lower-roman}.yue ul{margin-top:1.25em;margin-bottom:1.25em;padding-left:1.625em;list-style-type:disc}.yue ol>li::marker{color:var(--yue-c-ol-marker);font-weight:400}.yue ul>li::marker{color:var(--yue-c-ul-marker)}.yue dl{margin-top:1.5rem;margin-bottom:1.5rem}.yue dt{color:var(--yue-c-bold);font-weight:600}.yue dd{margin-left:1.5rem}.yue hr{border-color:var(--yue-c-hr);border-top-width:1px;margin-top:3em;margin-bottom:3em}.yue blockquote{color:var(--yue-c-quote);border-left-width:.25rem;border-left-color:var(--yue-c-quote-border);margin-top:1.2rem;margin-bottom:1.2rem;padding-left:1rem}.yue blockquote .attribution{font-size:.85em;font-style:italic}[lang^=zh] .yue blockquote .attribution,[lang=ko] .yue blockquote .attribution,[lang=ja] .yue blockquote .attribution{font-style:normal}.yue h1{color:var(--yue-c-heading);margin-top:0;margin-bottom:.888889em;font-size:2.25em;font-weight:800;line-height:1.11111}.yue h1 strong{color:inherit;font-weight:900}.yue h2{color:var(--yue-c-heading);margin-top:2em;margin-bottom:1em;font-size:1.5em;font-weight:700;line-height:1.33333}.yue h2 strong{color:inherit;font-weight:800}.yue h3{color:var(--yue-c-heading);margin-top:1.6em;margin-bottom:.6em;font-size:1.25em;font-weight:600;line-height:1.6}.yue h3 strong{color:inherit;font-weight:700}.yue h4{color:var(--yue-c-heading);margin-top:1.5em;margin-bottom:.5em;font-weight:600;line-height:1.5}.yue h4 strong{color:inherit;font-weight:700}.yue h5,.yue h6{color:var(--yue-c-heading);font-weight:600}.yue img{max-width:100%;display:inline}.yue img.rounded{border-radius:.5rem}.yue a>img,.yue figure>*,.yue figure img{margin-top:0;margin-bottom:0}.yue figcaption{color:var(--yue-c-caption);margin-top:.857143em;font-size:.875em;line-height:1.42857}.yue figcaption>p{margin-top:0}.yue code{color:var(--yue-c-code-text);font-size:.875em;font-weight:600}.yue a code,.yue h1 code,.yue h2 code,.yue h3 code,.yue h4 code,.yue blockquote code,.yue th code{color:inherit}.yue h2 code{font-size:.875em}.yue li>code,.yue p>code{background-color:var(--yue-c-code-background);border-radius:3px;padding:2px 4px;font-weight:500}.yue h3 code{font-size:.9em}.yue kbd{font-family:var(--sy-f-mono)}.yue video,.yue figure{margin-top:2em;margin-bottom:2em}.yue li{margin-top:.5em;margin-bottom:.5em}.yue ol>li,.yue ul>li{padding-left:.375em}.yue ul ul,.yue ul ol,.yue ol ul,.yue ol ol{margin-top:.75em;margin-bottom:.75em}.yue hr+*,.yue h2+*,.yue h3+*,.yue h4+*{margin-top:0}.yue table{table-layout:auto;text-align:left;width:100%;margin-top:2em;margin-bottom:2em;font-size:.86em;line-height:1.7}.yue table>caption{color:var(--yue-c-caption);margin-bottom:.4rem}.yue thead tr{border-bottom-width:1px;border-bottom-color:var(--yue-c-th-border)}.yue thead th{color:var(--yue-c-heading);vertical-align:middle;font-weight:600}.yue tbody tr{border-bottom-width:1px;border-bottom-color:var(--yue-c-td-border)}.yue tbody tr:last-child{border-bottom-width:0}.yue tbody td{vertical-align:middle}.yue tfoot{border-top-width:1px;border-top-color:var(--yue-c-th-border)}.yue tfoot td{vertical-align:top}.yue td>p{margin:.25rem 0}.yue thead th>p{margin:0}.yue thead th,.yue tbody td,.yue tfoot td{padding:.5rem}.yue section{clear:both}.yue section>div{margin-bottom:2rem}.yue dd>p:first-child{margin-top:0}.yue p.lead{color:var(--sy-c-light);margin-bottom:0;font-size:1.2rem}.yue p.lead+hr{margin-top:1rem}.yue p.rubric{color:var(--yue-c-heading);margin-top:2rem;font-weight:600}.yue .sidebar{background-color:var(--sy-c-surface);border:1px solid var(--sy-c-border);clear:right;float:right;border-radius:6px;width:30%;margin-bottom:1rem;margin-left:1rem;margin-right:0}@media (max-width:767px){.yue .sidebar{float:none;width:100%;margin-left:0}}.yue .sidebar>*{padding-left:1rem;padding-right:1rem}.yue .sidebar img{margin-top:1rem;margin-bottom:1rem}.yue .sidebar-title{border-bottom:1px solid var(--sy-c-border);margin:0;padding-top:.5rem;padding-bottom:.5rem;font-weight:500}.yue dl.simple>dd>p{margin:0}.yue ul.simple p,.yue ol.simple p{margin-top:.85rem;margin-bottom:.85rem}.yue a.headerlink{visibility:hidden;vertical-align:middle;opacity:.5;color:var(--sy-c-light);font-size:85%;font-weight:300;font-family:var(--sy-f-mono);border-bottom:none;margin-left:6px;font-style:normal;text-decoration:none;display:inline-block}.yue a.headerlink:hover{opacity:1}.yue .math-wrapper{width:100%;margin-top:2rem;margin-bottom:2rem;overflow-x:auto}.yue .math a.headerlink{opacity:1;visibility:visible}.yue a.headerlink:hover,dt:hover a.headerlink,h1:hover a.headerlink,h2:hover a.headerlink,h3:hover a.headerlink,h4:hover a.headerlink,h5:hover a.headerlink,h6:hover a.headerlink{visibility:visible}.yue a.image-reference,.yue .toctree-wrapper a{border-bottom:none;text-decoration:none}.yue .toctree-wrapper p.caption{font-size:.86rem;font-weight:500;font-family:var(--sy-f-heading);color:var(--sy-c-light);text-transform:uppercase;letter-spacing:.4px;border-bottom:1px solid var(--sy-c-divider);padding:.8rem 0 .4rem}.yue .align-left{clear:left;float:left;margin:0 1rem 1rem}.yue .align-right{clear:right;float:right;margin:0 1rem 1rem}.yue .align-center{text-align:center;margin-left:auto;margin-right:auto;display:block}.yue figure.align-center img{margin-left:auto;margin-right:auto}a.footnote-reference{vertical-align:top;font-size:.65rem}aside.footnote>span,div.citation>span{float:left;padding-right:.25rem;font-weight:500}aside.footnote>p,div.citation>p{margin-top:.5rem;margin-bottom:.5rem;margin-left:2rem}.yue kbd.kbd:not(.compound){border-radius:3px;margin-right:.25rem;padding:2px 5px;font-size:.86rem}.yue kbd.compound>kbd{margin-left:.25rem}.yue .menuselection{font-size:.86rem;font-weight:500}.light .searchbox kbd,.light .yue kbd.kbd:not(.compound){background:linear-gradient(-225deg,#e6e6e6,#f8f8f8);border:0;box-shadow:inset 0 -2px #dbdbdb,inset 0 0 1px 1px #fff,0 1px 2px 1px #50505066}.dark .searchbox kbd,.dark .yue kbd.kbd:not(.compound){background:linear-gradient(-225deg,#353434,#141414);border:0;box-shadow:inset 0 -2px #373737,inset 0 0 1px 1px #222,0 1px 2px 1px #000}.yue p.centered{text-align:center}.yue section>img{margin-bottom:1rem}.hlist td{vertical-align:top}.light .light-hidden,.dark .dark-hidden,.light .dark-only,.dark .light-only{display:none}.yue .genindex-jumpbox,.yue .modindex-jumpbox{border-top:1px solid var(--sy-c-border);border-bottom:1px solid var(--sy-c-border);padding:2px .4rem}.yue table.modindextable td:first-of-type{width:28px}.yue table.modindextable img.toggler{margin:0}.yue table.modindextable tr.cap{background:var(--sy-c-surface);font-size:1.12rem;font-family:var(--sy-f-mono)}.yue h2+table.indextable,.yue table.indextable ul{margin-top:0}:root{--attention-icon:var(--lucide-alert-url);--attention-1:var(--crimson-surface);--attention-2:var(--crimson-a3);--attention-3:var(--crimson-9);--attention-4:var(--crimson-a11);--caution-icon:var(--lucide-zap-url);--caution-1:var(--amber-surface);--caution-2:var(--amber-a3);--caution-3:var(--amber-9);--caution-4:var(--amber-11);--danger-icon:var(--lucide-skull-url);--danger-1:var(--ruby-surface);--danger-2:var(--ruby-a3);--danger-3:var(--ruby-9);--danger-4:var(--ruby-a11);--error-icon:var(--lucide-close-url);--error-1:var(--red-surface);--error-2:var(--red-a3);--error-3:var(--red-9);--error-4:var(--red-a11);--hint-icon:var(--lucide-bell-url);--hint-1:var(--cyan-surface);--hint-2:var(--cyan-a3);--hint-3:var(--cyan-9);--hint-4:var(--cyan-a11);--important-icon:var(--lucide-flame-url);--important-1:var(--violet-surface);--important-2:var(--violet-a3);--important-3:var(--violet-9);--important-4:var(--violet-a11);--note-icon:var(--lucide-calendar-url);--note-1:var(--blue-surface);--note-2:var(--blue-a3);--note-3:var(--blue-9);--note-4:var(--blue-a11);--tip-icon:var(--lucide-rocket-url);--tip-1:var(--green-surface);--tip-2:var(--green-a3);--tip-3:var(--green-9);--tip-4:var(--green-a11);--warning-icon:var(--lucide-zap-url);--warning-1:var(--orange-surface);--warning-2:var(--orange-a3);--warning-3:var(--orange-9);--warning-4:var(--orange-a11);--seealso-icon:var(--lucide-link-url);--seealso-1:var(--gold-surface);--seealso-2:var(--gold-a3);--seealso-3:var(--gold-9);--seealso-4:var(--gold-a11);--todo-icon:var(--lucide-bookmark-url);--todo-1:var(--bronze-surface);--todo-2:var(--bronze-a3);--todo-3:var(--bronze-9);--todo-4:var(--bronze-a11);--versionadded-icon:var(--lucide-flame-url);--versionadded-1:var(--green-surface);--versionadded-2:var(--green-9);--versionchanged-icon:var(--lucide-zap-url);--versionchanged-1:var(--amber-surface);--versionchanged-2:var(--amber-9);--versionremoved-icon:var(--lucide-skull-url);--versionremoved-1:var(--red-surface);--versionremoved-2:var(--red-9);--deprecated-icon:var(--lucide-alert-url);--deprecated-1:var(--orange-surface);--deprecated-2:var(--orange-9)}.admonition{--icon-url:var(--lucide-bell-url);--color-1:var(--color-surface-accent);--color-2:var(--accent-a3);--color-3:var(--accent-9);--color-4:var(--accent-a11);border-left:4px solid var(--color-3);background-color:var(--color-1);flex-direction:column;margin-top:1rem;margin-bottom:1rem;padding:.825rem 1rem;display:flex;position:relative}.admonition:before{content:"";background-color:var(--color-3);border-radius:100%;width:20px;height:20px;position:absolute;top:6px;left:-12px}.admonition:after{content:"";-webkit-mask:var(--icon-url) no-repeat;-webkit-mask:var(--icon-url) no-repeat;mask:var(--icon-url) no-repeat;background-color:#fff;width:12px;height:12px;font-style:normal;position:absolute;top:10px;left:-8px;-webkit-mask-size:100% 100%;mask-size:100% 100%}.admonition p.admonition-title{color:var(--color-4);background-color:var(--color-2);--yue-c-code:var(--color-4);--yue-c-bold:var(--color-4);margin:-.825rem -1rem .825rem -19px;padding:4px 18px;font-size:.85rem;font-weight:600;line-height:1.72;position:relative}.admonition p.admonition-title svg{display:inline-block}.admonition.attention{--icon-url:var(--attention-icon);--color-1:var(--attention-1);--color-2:var(--attention-2);--color-3:var(--attention-3);--color-4:var(--attention-4)}.admonition.caution{--icon-url:var(--caution-icon);--color-1:var(--caution-1);--color-2:var(--caution-2);--color-3:var(--caution-3);--color-4:var(--caution-4)}.admonition.danger{--icon-url:var(--danger-icon);--color-1:var(--danger-1);--color-2:var(--danger-2);--color-3:var(--danger-3);--color-4:var(--danger-4)}.admonition.error{--icon-url:var(--error-icon);--color-1:var(--error-1);--color-2:var(--error-2);--color-3:var(--error-3);--color-4:var(--error-4)}.admonition.hint{--icon-url:var(--hint-icon);--color-1:var(--hint-1);--color-2:var(--hint-2);--color-3:var(--hint-3);--color-4:var(--hint-4)}.admonition.important{--icon-url:var(--important-icon);--color-1:var(--important-1);--color-2:var(--important-2);--color-3:var(--important-3);--color-4:var(--important-4)}.admonition.note{--icon-url:var(--note-icon);--color-1:var(--note-1);--color-2:var(--note-2);--color-3:var(--note-3);--color-4:var(--note-4)}.admonition.tip{--icon-url:var(--tip-icon);--color-1:var(--tip-1);--color-2:var(--tip-2);--color-3:var(--tip-3);--color-4:var(--tip-4)}.admonition.warning{--icon-url:var(--warning-icon);--color-1:var(--warning-1);--color-2:var(--warning-2);--color-3:var(--warning-3);--color-4:var(--warning-4)}.admonition.seealso{--icon-url:var(--seealso-icon);--color-1:var(--seealso-1);--color-2:var(--seealso-2);--color-3:var(--seealso-3);--color-4:var(--seealso-4)}.admonition.admonition-todo{--icon-url:var(--todo-icon);--color-1:var(--todo-1);--color-2:var(--todo-2);--color-3:var(--todo-3);--color-4:var(--todo-4)}.yue .admonition>*{margin-top:0;margin-bottom:1rem}.yue .admonition>:last-child{margin-bottom:0!important}span.versionmodified{color:var(--sy-c-bold);font-weight:600}div.versionadded,div.versionchanged,div.versionremoved,div.deprecated{border-left:4px solid var(--color-2);background-color:var(--color-1);margin:1rem 0;padding:6px 1rem;line-height:1.72;position:relative}div.versionadded:before,div.versionchanged:before,div.versionremoved:before,div.deprecated:before{content:"";color:#fff;background-color:var(--color-2);text-align:center;width:20px;height:20px;font:normal bold 14px/20px var(--sy-f-mono);border-radius:100%;position:absolute;top:10px;left:-12px}div.versionadded:after,div.versionchanged:after,div.versionremoved:after,div.deprecated:after{content:"";-webkit-mask:var(--icon-url) no-repeat;-webkit-mask:var(--icon-url) no-repeat;mask:var(--icon-url) no-repeat;background-color:#fff;width:12px;height:12px;font-style:normal;position:absolute;top:14px;left:-8px;-webkit-mask-size:100% 100%;mask-size:100% 100%}div.versionadded{--color-1:var(--versionadded-1);--color-2:var(--versionadded-2);--icon-url:var(--versionadded-icon)}div.versionchanged{--color-1:var(--versionchanged-1);--color-2:var(--versionchanged-2);--icon-url:var(--versionchanged-icon)}div.versionremoved{--color-1:var(--versionremoved-1);--color-2:var(--versionremoved-2);--icon-url:var(--versionremoved-icon)}div.deprecated{--color-1:var(--deprecated-1);--color-2:var(--deprecated-2);--icon-url:var(--deprecated-icon)}div.versionadded>p,div.versionchanged>p,div.versionremoved>p,div.deprecated>p{margin:0}.yue blockquote.epigraph{text-align:center;border-left:0;padding:1rem 2.4rem}.yue blockquote.highlights{background-color:var(--sy-c-surface);border-left-width:4px;padding-top:.2rem;padding-bottom:.2rem}.yue blockquote.pull-quote{border-left:0;padding:2.4rem 3.6rem 1.2rem;font-size:1.24rem;position:relative}.yue blockquote.pull-quote:before{content:"“";color:var(--yue-c-quote-symbol);font:700 4rem/1 Times New Roman,Georgia,Palatino,Times,serif;position:absolute;top:0;left:.5rem}.yue blockquote.pull-quote .attribution{text-align:right}:root{--code-block-background:var(--accent-a2);--code-block-caption-background:var(--accent-a3);--code-block-highlight:var(--accent-a3);--code-block-linenos-divider:var(--gray-a6)}html.light{--syntax-text:var(--syntax-light-text);--syntax-comment:var(--syntax-light-comment)}html.dark,html.light .dark-code{--syntax-text:var(--syntax-dark-text);--syntax-comment:var(--syntax-dark-comment)}.light .dark-code{--code-block-background:var(--black-a12);--code-block-caption-background:#1c2024;--code-block-highlight:var(--white-a2);--code-block-linenos-divider:var(--white-a4)}.highlight{color:var(--syntax-text)}.highlight .hll{background-color:var(--code-block-highlight);display:block}pre.literal-block{background-color:var(--code-block-background);border-radius:6px;padding:1rem;font-size:.96rem;line-height:1.48;overflow:auto}.highlight,.literal-block-wrapper{--margin:1rem;--radius:6px}.literal-block-wrapper div[class^=highlight-]{display:flex}.literal-block-wrapper .highlight{width:100%}.highlight>pre{padding:var(--margin);font-size:.96rem;line-height:1.48;font-family:var(--sy-f-mono);background-color:var(--code-block-background);border-radius:var(--radius);overflow:auto}.win .highlight>pre{font-family:"Twemoji Country Flags", var(--sy-f-mono)}.highlight .gp,.highlight .linenos{-webkit-user-select:none;user-select:none}.highlight .linenos{box-shadow:-.05rem 0 var(--code-block-linenos-divider) inset;opacity:.6;margin-right:.8rem;padding-right:.8rem;display:inline-block}.highlight .hll{margin-left:calc(0rem - var(--margin));margin-right:calc(0rem - var(--margin));padding:0 var(--margin)}.code-block-caption{color:var(--syntax-text);background-color:var(--code-block-caption-background);padding:.4rem var(--margin);border-radius:var(--radius) var(--radius) 0 0;font-size:.84rem;font-weight:600;display:flex}.code-block-caption+div>.highlight>pre{border-top-left-radius:0;border-top-right-radius:0}div[class^=highlight]>.highlight>pre{display:grid}.yue .table-wrapper{border:1px solid var(--yue-c-table-border);border-radius:6px;width:100%;margin-top:2rem;margin-bottom:2rem;overflow-x:auto}.yue .table-wrapper table{margin:0}.yue .table-wrapper thead tr{border-top:1px solid var(--yue-c-td-border)}.yue .table-wrapper thead tr:first-child{border-top:0}.yue .table-wrapper th{background-color:var(--yue-c-th-background);border-left:1px solid var(--yue-c-td-border);padding:.725rem 1rem}.yue .table-wrapper td{border-left:1px solid var(--yue-c-td-border);padding:.5rem 1rem}.yue .table-wrapper tr>th:first-child,.yue .table-wrapper tr>td:first-child{border-left:0}.yue .table-wrapper caption{border-bottom:1px solid var(--yue-c-th-border);margin:0;padding:.5rem}.yue .table-wrapper tbody tr.row-odd{background-color:var(--yue-c-row-background)}.yue table.hlist td{vertical-align:top}.table-wrapper{scrollbar-gutter:auto;overflow-x:auto}.table-wrapper::-webkit-scrollbar{width:.75rem;height:.75rem}.table-wrapper::-webkit-scrollbar-thumb{border-radius:10px}.table-wrapper::-webkit-scrollbar-track{background-color:#0000}.table-wrapper:hover::-webkit-scrollbar-thumb{background-color:#9b9b9b33;background-clip:content-box;border:3px solid #0000}.yue table.ghost th,.yue table.ghost td{background-color:#0000;border-left:0;border-right:0}.yue table.ghost caption{border-bottom:3px solid var(--yue-c-td-border);margin-bottom:0;padding-bottom:.5rem}.yue table.ghost thead tr:first-child{border-top:0;border-bottom-width:3px}.yue .table-wrapper.ghost{border:0}.yue .table-wrapper.sphinx-datatable{overflow-x:hidden}.yue .table-wrapper:not(.ghost) .dt-layout-row{margin:0}.table-wrapper .dt-layout-row:not(.dt-layout-table){padding:.5rem 1rem}.table-wrapper .dt-layout-row.dt-layout-table{border-top:1px solid var(--yue-c-th-border);border-bottom:1px solid var(--yue-c-th-border);overflow-x:auto}.table-wrapper .dt-layout-table th,.yue .table-wrapper div.dt-container.dt-empty-footer td{border-bottom:0}.yue .table-wrapper .dt-layout-table th:hover{outline-color:var(--accent-a3)}.table-wrapper div.dt-container .dt-search input{border-color:var(--sy-c-border);font-size:.92rem;line-height:1.4}.table-wrapper div.dt-container .dt-input{border-color:var(--sy-c-border)}.table-wrapper div.dt-container .dt-paging .dt-paging-button{justify-content:center;align-items:center;width:32px;height:32px;padding:0;font-size:.875rem;display:inline-flex}.table-wrapper div.dt-container .dt-paging .dt-paging-button:hover{background:var(--accent-a3);border-color:var(--accent-a3);color:var(--accent-a11)!important}.table-wrapper div.dt-container .dt-paging .dt-paging-button.current{border-color:var(--gray-a4);background:0 0}.table-wrapper div.dt-container .dt-paging .dt-paging-button.current:hover{background:var(--accent-a3)}.table-wrapper .dt-info{font-size:.875rem}:root{--sig-property:var(--gray-12);--sig-name:var(--accent-10);--sig-typehint:var(--indigo-9);--sig-param:var(--gray-11)}dt.sig{text-indent:-2.4rem;border-radius:6px;padding:.25rem .5rem .25rem 3rem;font-size:.92rem;position:relative}dt.sig:after{content:"";clear:both;display:table}dt.sig:hover{background:var(--sy-c-surface)}dt.sig+dd{margin-left:2rem;font-size:.92rem}dt.sig>em.property:first-child{color:var(--sig-property)}dl.field-list{margin-top:0}dl.field-list a{font-weight:400}dt.sig+dd>div{margin-bottom:1rem}dt.sig+dd>dl.field-list>dt{text-transform:uppercase;font-size:.76rem}em.property,em.sig-param{font-style:normal}em.sig-param{color:var(--sig-param)}.sig-param a.reference{font-weight:400}span.sig-prename{color:var(--sig-name);font-weight:400}span.sig-name{color:var(--sig-name);font-weight:600}span.sig-return-icon{color:var(--sy-c-light)}span.sig-return-typehint,span.sig-return-typehint>a{color:var(--sig-typehint)}span.sig-paren,span.pre{font-family:var(--sy-f-mono)}dt.sig>a.internal{color:var(--sy-c-light);border:0;font-size:.82rem}dt.sig>a.internal:before{content:"\a ";white-space:pre}.viewcode-block{position:relative}.viewcode-back{font-size:.8rem;position:absolute;top:-1.5rem}.classifier{font-style:oblique;font-weight:400}.classifier:before{content:":";margin-left:.1rem;margin-right:.5rem;font-style:normal;display:inline-block}.yue .table-wrapper.autosummary{border-left:0;border-right:0;border-radius:0}.yue .table-wrapper table.autosummary td{border:none;padding-top:.25rem;padding-bottom:.25rem}.yue p.rubric+div.autosummary{margin-top:0}.hamburger{cursor:pointer;width:16px;height:14px;display:inline-block;position:relative;overflow:hidden}.hamburger>span{background-color:var(--sy-c-text);width:16px;height:2px;transition:top .25s,transform .25s;position:absolute;left:0}.hamburger_1{top:0}.hamburger_2{top:6px;transform:translate(-.5rem)}.hamburger_3{top:12px;transform:translate(-.25rem)}button[aria-expanded=true] .hamburger .hamburger_1{top:6px;transform:translate(0)rotate(225deg)}button[aria-expanded=true] .hamburger .hamburger_2{top:6px;transform:translate(18px)}button[aria-expanded=true] .hamburger .hamburger_3{top:6px;transform:translate(0)rotate(135deg)}.searchbox{position:relative}.searchbox input{appearance:none;width:100%;font-size:.92rem;font-family:var(--sy-f-text);background:var(--sy-c-surface);border-radius:6px;outline:0;padding:6px 12px}.searchbox kbd,.searchbox button{font-size:.68rem;font-weight:600;font-family:var(--sy-f-mono);border:1px solid var(--sy-c-border);background-color:var(--sy-c-background);opacity:1;border-radius:3px;margin:6px;padding:2px 6px;transition:opacity .2s;position:absolute;right:0}.searchbox input:focus+kbd{opacity:0}.searchform{align-items:center;display:flex;position:relative}.searchform input[name=q]{appearance:none;width:100%;font-size:.92rem;font-family:var(--sy-f-text);background:var(--sy-c-surface);border-radius:6px;outline:0;padding:6px 12px}.searchform input[name=q]+button{font-size:.68rem;font-weight:600;font-family:var(--sy-f-text);border:1px solid var(--sy-c-divider);background-color:var(--sy-c-background);opacity:1;border-radius:3px;margin:6px;padding:2px 6px;transition:opacity .2s;position:absolute;right:0}.search .highlighted{background-color:var(--accent-a4)}#search-results{border-top:1px solid var(--sy-c-border)}#search-results h2{margin-top:2rem;margin-bottom:.725rem}#search-results .search-summary{color:var(--sy-c-light);font-weight:500}#search-results ul.search{margin-left:0;padding-top:.625rem;padding-bottom:2rem;padding-left:0;list-style-type:none}#search-results ul.search>li{padding-left:0}#search-results ul.search>li+li{border-top:1px solid var(--sy-c-divider);padding-top:1rem}#search-results ul.search li>a{font-weight:600}#search-results ul.search p.context{margin-top:.5rem;font-size:.875rem}.demo{border:1px solid var(--sy-c-border);border-radius:6px}.demo-code .highlight>pre{border-bottom-right-radius:0;border-bottom-left-radius:0}.demo-result{padding:1rem}.container.image-1,.container.video-1{border:.5rem solid var(--accent-a3);border-radius:6px}.container.image-1>img{border-radius:4px;margin:0}.container.video-1 video,.container.video-1 iframe{border-radius:4px;width:100%;margin:0}.container.image-2,.container.video-2{border:1px solid var(--sy-c-border);border-radius:6px;padding:1rem}.container.image-2>img,.container.video-2>video{margin:0}.container.buttons{margin:2rem 0 4.2rem}.container.buttons>p{flex-wrap:wrap;gap:1rem;display:flex}.container.buttons a{border:2px solid var(--sy-c-border);background-color:var(--sy-c-surface);border-radius:2.6rem;padding:0 2rem;font-weight:600;line-height:2.6rem;text-decoration:none;transition:all .2s;display:inline-block}.container.buttons a:first-child{color:var(--accent-contrast);background-color:var(--accent-9);border-color:var(--accent-9)}.container.buttons a:hover{color:var(--sy-c-bold);border-color:var(--accent-9);background-color:var(--sy-c-background)}.container.rounded-image img{border-radius:99999px}#ethical-ad-placement .ethical-sidebar{background-color:var(--sy-c-surface);border:none;padding:.8rem;position:relative}#ethical-ad-placement .ethical-text a{color:var(--sy-c-text)!important}#ethical-ad-placement .ethical-text a:hover{color:var(--sy-c-link-hover)!important}.sy-main #ethical-ad-placement .ethical-sidebar{max-width:380px;margin-left:0}.sy-main #ethical-ad-placement .ethical-image-link{flex-shrink:0;margin-right:.4rem}.sy-main #ethical-ad-placement .ethical-content{display:flex}.sy-main #ethical-ad-placement .ethical-text{margin-top:0}.sy-main #ethical-ad-placement .ethical-callout{position:absolute;bottom:.4rem;right:.4rem}#carbonads{background-color:var(--sy-c-surface);border:none;border-radius:8px;margin:1rem 0;padding:.8rem .8rem 1.6rem;display:block;position:relative}#carbonads a{border:0;font-weight:400}#carbonads img{margin:0}.carbon-wrap{flex-direction:column;justify-content:space-between;align-items:center;display:flex}.carbon-text{text-align:center;margin:.5rem 0;font-size:.78rem;line-height:1.42;display:block}.carbon-text:hover{color:var(--sy-c-link-hover)}.carbon-poweredby{opacity:.68;text-transform:uppercase;font-size:.68rem;position:absolute;bottom:.5rem;right:.8rem}.carbon-poweredby:hover{text-decoration:underline}.sy-main #carbonads{max-width:380px;margin-top:1.6rem;padding:1rem}.sy-main .carbon-wrap{flex-direction:row;align-items:flex-start}.sy-main .carbon-text{text-align:left;margin-top:0;margin-left:1rem;font-size:.86rem}#bsa-custom-container{text-align:right}.yue a.bsa-container{text-align:left;border-bottom:0;border-radius:6px;flex-flow:row;justify-content:space-between;align-items:center;padding:15px 20px;text-decoration:none;display:flex;box-shadow:inset 0 0 0 1px #0000001a}.yue a.bsa-ad-via{background:var(--gray-a3);border:none;border-radius:2px;padding:3px 10px;font-size:10px;font-weight:300}.bsa-main{flex-flow:row;flex-grow:1;justify-content:center;align-items:center;margin:0 auto;display:flex}.bsa-img{max-height:40px;margin-right:20px;line-height:0}.yue a.bsa-container .bsa-img{margin-top:0;margin-bottom:0}.bsa-details{flex-flow:column;margin-right:20px;display:flex}.bsa-tagline{letter-spacing:1.5px;text-transform:uppercase;margin-bottom:3px;font-size:9px;font-weight:600;line-height:1}.bsa-desc{letter-spacing:1px;max-width:600px;font-size:12px;font-weight:400;line-height:1.4}.bsa-cta{letter-spacing:1px;text-transform:uppercase;white-space:nowrap;border-radius:3px;padding:10px 16px;font-size:10px;font-weight:600;line-height:1;transition:all .3s ease-in-out;transform:translateY(-1px)}@media (max-width:940px){.bsa-details{margin-right:0;font-size:14px}.bsa-cta{display:none}}@media (min-width:768px) and (max-width:820px){.bsa-img{display:none}}@media (max-width:480px){.bsa-img{display:none}}.repo-stats{border:1px solid var(--sy-c-divider);border-radius:6px;margin-bottom:1rem;padding:.5rem}.repo-stats:hover{background-color:var(--sy-c-surface)}.repo-stats-count{color:var(--sy-c-light)}.repo-stats strong{font-weight:500;font-family:var(--sy-f-mono);color:inherit}.edit-this-page{border-top:1px solid var(--sy-c-divider);margin:1rem 0;padding:.5rem 0;font-size:.8rem;font-weight:600}.repo-stats+.edit-this-page{border-top:0;margin-top:0;padding-top:0}.edit-this-page a{color:var(--sy-c-text)}.edit-this-page a:hover{color:var(--sy-c-link-hover)}.edit-this-page a:after{content:" →"}.back-to-top{z-index:10;background:var(--sy-c-background);border-radius:2rem;align-items:center;gap:.25rem;padding:.4rem .8rem .4rem .6rem;font-size:.8rem;display:none;position:fixed;bottom:68px;left:50%;transform:translate(-50%);box-shadow:0 .2rem .5rem #0000000d,0 0 1px #6b728080}.dark .back-to-top{background:var(--slate-2);box-shadow:0 .2rem .5rem #ffffff0d,0 0 1px #9aa4b880}.back-to-top:hover{color:var(--accent-contrast);background:var(--accent-9)}.back-to-top svg{fill:currentColor;width:1rem;height:1rem;display:inline-block}.back-to-top[data-visible=true]{display:flex}.icon-link{display:inline-block}.icon-link span{vertical-align:middle;display:inline-block}.icon-link .icon{border:1px solid var(--sy-c-border);opacity:.8;border-radius:6px;margin-right:.4rem;padding:.1rem}.icon-link svg{width:1.5rem;height:1.5rem}.icon-link:hover .icon{opacity:1}#copy-page-trigger{border:1px solid var(--gray-a4);background-color:var(--sy-c-background);border-radius:.625rem;align-items:center;font-size:.875rem;display:inline-flex;overflow:hidden}#copy-page-trigger i.i-lucide{transition:transform .2s}#copy-page-trigger button[aria-expanded=true]>i{transform:rotate(-180deg)}#copy-page-trigger button{background-color:var(--sy-c-background);vertical-align:middle;height:32px;transition:background-color .2s}#copy-page-trigger button:hover{background-color:var(--sy-c-surface)}#copy-page-trigger button+button{border-left:1px solid var(--gray-a4)}#copy-page-content{z-index:9;border:1px solid var(--gray-a4);box-shadow:var(--sy-dropdown-shadow);background:var(--sy-c-background);border-radius:.625rem;min-width:200px;margin-top:.25rem;padding:.75rem .5rem;position:absolute}#copy-page-content[aria-hidden=true]{display:none}#copy-page-content button,#copy-page-content a{white-space:nowrap;border-radius:.25rem;align-items:center;padding:.4rem .5rem;font-size:.875rem;display:inline-flex}#copy-page-content button:hover,#copy-page-content a:hover{background:var(--gray-a3)}#copy-page-content .iconify-icon,#copy-page-content iconify-icon{border:1px solid var(--gray-a4);border-radius:.25rem;justify-content:center;align-items:center;width:28px;height:28px;margin-right:.5rem;font-size:.825rem;display:inline-flex}#copy-page-content a:after{content:"";-webkit-mask:var(--lucide-external-link-url) no-repeat;-webkit-mask:var(--lucide-external-link-url) no-repeat;mask:var(--lucide-external-link-url) no-repeat;vertical-align:top;background-color:var(--gray-a9);width:.625em;height:.625em;margin-left:.1rem;font-style:normal;display:inline-block;position:relative;top:-2px;-webkit-mask-size:100% 100%;mask-size:100% 100%}#copy-page-trigger .i-lucide[data-icon=copy],#copy-page-content .i-lucide[data-icon=copy]{--icon-url:var(--lucide-copy-url)}#copy-page-trigger .i-lucide[data-icon=check],#copy-page-content .i-lucide[data-icon=check]{--icon-url:var(--lucide-check-url)}#copy-page-trigger .i-lucide[data-icon=loader],#copy-page-content .i-lucide[data-icon=loader]{--icon-url:var(--lucide-loader-url)}@media (min-width:64rem){.copy-page-wrapper+.yue>section>h1:first-of-type{padding-right:180px}#copy-page-content{right:0}}.announcement{width:100%;color:var(--sy-c-banner,var(--accent-contrast));background-color:var(--sy-c-banner-bg,var(--accent-a11));z-index:20;align-items:center;padding:.8rem 2rem;display:flex;position:sticky;top:0;left:0}.announcement a{text-decoration:underline}.announcement ::selection{color:var(--sy-c-banner,var(--accent-contrast))}.announcement-inner{width:100%}.announcement-close{position:absolute;top:.8rem;right:1rem}.sy-head{top:var(--sy-s-banner-height);height:var(--sy-s-navbar-height);z-index:20;background-color:#0000;position:sticky}.sy-head-blur{-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);background-color:var(--sy-c-background-dropback);width:100%;height:100%;box-shadow:0 0 var(--sy-c-background-contrast), 0 2px 4px var(--gray-a1), 0 1px 0 var(--sy-c-divider);z-index:-1;position:absolute;top:0;left:0}.sy-head-inner{padding-left:max(env(safe-area-inset-right), 1.5rem);padding-right:max(env(safe-area-inset-right), 1rem);height:var(--sy-s-navbar-height);justify-content:space-between;align-items:center;display:flex}.sy-head-brand img{height:28px}.sy-head-brand img+strong,.sy-head-brand .dark-logo{display:none}.dark .sy-head-brand .dark-logo{display:inline}.dark .sy-head-brand .light-logo{display:none}.light .sy-head-brand .light-logo{display:inline}.light .sy-head-brand .dark-logo{display:none}.sy-head-links button,.sy-head-links a{white-space:nowrap;padding:.5rem;font-size:.95rem;font-weight:500}.sy-head-links .link i.external-link{opacity:.6;color:var(--sy-c-light);margin-left:2px;font-size:68%}.sy-head-links .link>ul a{white-space:normal;display:block}.sy-head-links .link>ul a:hover{background:var(--sy-c-surface);border-radius:6px}.sy-head-links .link>ul small{color:var(--sy-c-light);font-weight:400;display:block}.sy-head-socials{align-items:center;display:flex}.sy-head-socials a{align-items:center;padding:.5rem;display:flex}.sy-head-actions button{height:var(--sy-s-navbar-height);padding:0 .5rem}@media (max-width:767px){body[data-expanded*=head-nav]{overflow:hidden}.sy-head-nav{top:var(--sy-s-offset-top);box-sizing:border-box;border-top:1px solid var(--sy-c-divider);background-color:var(--sy-c-background);width:100%;padding:4rem 1.8rem 0;display:none;position:fixed;bottom:0;left:0;right:0;overflow-y:auto}.sy-head-nav[aria-hidden=false]{display:block}.sy-head-links{margin-left:auto;margin-right:auto}.sy-head-links .link{margin:.5rem 0}.sy-head-links .link i.chevron{display:none}.sy-head-links .link>ul{margin:.5rem 0 .5rem 1rem}.sy-head-extra form.searchbox{position:absolute;top:1rem;left:1.8rem;right:1.8rem}.sy-head-extra{flex-direction:column;width:100%;padding:2rem 0 1rem}}@media (min-width:768px){.sy-head-inner{padding-right:max(env(safe-area-inset-right), 1.5rem)}.sy-head-nav{flex-grow:1;justify-content:space-between;align-items:center;display:flex}.sy-head-links[data-align=right]{--head-links-justify-content:flex-end}.sy-head-links[data-align=center]{--head-links-justify-content:center}.sy-head-links{white-space:nowrap;justify-content:var(--head-links-justify-content,flex-start);flex-grow:1;padding:0 1rem;display:flex;overflow:auto}.sy-head-links .link{height:var(--sy-s-navbar-height);align-items:center;display:inline-flex}.sy-head-links a:hover{color:var(--sy-c-link-hover)}.sy-head-links .link:hover>a{background-color:var(--sy-c-surface);border-radius:6px}.sy-head-links .link i.chevron-down{color:var(--sy-c-light)}.sy-head-links .link>ul{visibility:hidden;background-color:var(--sy-c-background);height:0;top:var(--sy-s-navbar-height);border:1px solid var(--sy-c-divider);box-shadow:var(--sy-dropdown-shadow);z-index:9;border-radius:6px;max-width:320px;margin-top:-10px;padding:15px;position:absolute}.sy-head-links .link>ul[aria-hidden=false],.sy-head-links .link:hover>ul{visibility:visible;height:auto}.sy-head-links .link>ul>li{padding:.2rem 0}.sy-head-socials{margin-left:.5rem}}.sy-foot{border-top:1px solid var(--sy-c-foot-divider);color:var(--sy-c-foot-text);background-color:var(--sy-c-foot-background);padding-top:1.5rem;padding-bottom:1rem}.sy-foot-inner{padding-left:max(env(safe-area-inset-right), 1.5rem);padding-right:max(env(safe-area-inset-right), 1.5rem)}.sy-foot-copyright{font-size:.84rem}.sy-foot-copyright a{font-weight:500}.sy-foot-copyright a:hover{text-decoration:underline}.sy-foot-socials a{color:var(--sy-c-foot-text);font-size:1.4rem}.sy-foot-socials a+a{margin-left:.5rem}.sy-foot-socials a svg{width:1.4rem;height:1.4rem;display:inline-block}.sy-lside .sidebar-links{margin-bottom:2rem}@media (min-width:768px){.sy-lside .sy-lside-inner{top:var(--sy-s-offset-top)}.sy-lside .sy-scrollbar{max-height:calc(100vh - var(--sy-s-offset-top));overflow-x:hidden}}.yue *{scroll-margin-top:calc(var(--sy-s-offset-top) + 68px)}.sy-main{min-height:calc(100vh - var(--sy-s-offset-top) - 80px)}.sy-content{max-width:64rem;min-height:calc(100vh - var(--sy-s-offset-top) - 80px)}@media (max-width:767px){#lside{z-index:18;top:var(--sy-s-offset-top);width:300px;max-width:100%;height:calc(100vh - var(---sy-s-offset-top));background:var(--sy-c-background);transition:transform .2s;position:fixed;bottom:0;left:0;overflow:auto;transform:translate(-100%)}#lside[aria-hidden=false]{transform:translate(0)}.lside-overlay{top:var(--sy-s-offset-top);background-color:var(--sy-c-overlay);opacity:0;width:0;height:0;transition:width 0 .25s, height 0 .25s, opacity .25s;position:fixed;left:0}#lside[aria-hidden=false]+.lside-overlay{opacity:1;z-index:16;width:100%;height:100%}}@media (max-width:1279px){.sy-rside{z-index:25;background:var(--sy-c-background);width:20rem;max-width:100%;box-shadow:0 0 var(--sy-c-background-contrast), -12px 0 16px var(--gray-a1);padding-top:2rem;padding-bottom:1rem;transition:transform .2s;position:fixed;top:0;bottom:0;right:0;overflow:auto;transform:translate(110%)}#rside[aria-hidden=false]{transform:translate(0)}.rside-close{justify-content:center;align-items:center;width:2rem;height:2rem;font-size:1.4rem;display:flex;position:absolute;top:16px;right:16px}.rside-overlay{background-color:var(--sy-c-overlay);opacity:0;width:0;height:0;transition:width 0 .25s, height 0 .25s, opacity .25s;position:fixed;top:0;left:0}#rside[aria-hidden=false]+.rside-overlay{opacity:1;z-index:22;width:100%;height:100%}}@media (min-width:768px){.sy-main{width:calc(100% - 18rem);max-width:52rem}}@media (min-width:1280px){.sy-main{width:calc(100% - 34rem);max-width:none}.sy-rside .sy-scrollbar{max-height:calc(100vh - var(--sy-s-offset-top) - env(safe-area-inset-bottom))}.yue *{scroll-margin-top:calc(var(--sy-s-offset-top) + 24px)}}.nav-languages button,.nav-versions button{cursor:pointer;white-space:nowrap;background:0 0;border:0;margin:0;padding:0}.nav-versions .chevron-down{color:var(--sy-c-light)}@media (max-width:767px){.nav-versions,.nav-languages{background-color:var(--sy-c-surface);border-radius:6px;width:100%;margin-bottom:1rem;padding-bottom:.6rem}.nav-versions button,.nav-languages button{color:var(--sy-c-light);padding:.5rem 1rem;font-size:.76rem;font-weight:500}.nav-versions button>i,.nav-languages button>i{display:none}.nav-versions ul{padding:0 .6rem}.nav-versions li{padding:.2rem .4rem;display:inline-block}.nav-languages li{padding:.32rem 1rem;font-size:.94rem}}@media (min-width:768px){.nav-versions,.nav-languages{width:auto;height:var(--sy-s-navbar-height);color:var(--sy-c-text);background:0 0;align-items:center;display:flex;position:relative}.nav-languages button,.nav-versions button{border-right:1px solid var(--gray-3);padding:0 .5rem}.nav-versions-choices,.nav-languages-choices{visibility:hidden;box-sizing:border-box;background-color:var(--sy-c-background);min-width:120px;max-height:60vh;box-shadow:var(--sy-dropdown-shadow);border-radius:6px;padding:.8rem 1rem;position:absolute;top:3rem;right:-.6rem;overflow:hidden auto}.nav-versions:hover .nav-versions-choices,.nav-languages:hover .nav-languages-choices{visibility:visible}.nav-versions li,.nav-languages li{padding:.1rem 0}.nav-versions a,.nav-languages a{color:var(--sy-c-text);white-space:nowrap;padding:.2rem .6rem;display:block}.nav-versions a:hover,.nav-languages a:hover{color:var(--sy-c-link-hover);background:var(--sy-c-surface);border-radius:6px}}.sy-breadcrumbs{top:var(--sy-s-offset-top);background-color:var(--sy-c-background);z-index:5;padding:0 1.5rem;position:sticky}.sy-breadcrumbs-inner{border-bottom:1px solid var(--sy-c-divider);padding:.8rem 0}.sy-breadcrumbs ol{white-space:nowrap;font-size:.94rem;display:flex;overflow:auto}.sy-breadcrumbs button{align-items:center;display:flex}.sy-breadcrumbs ol a{color:var(--sy-c-light)}.sy-breadcrumbs ol a:hover{color:var(--sy-c-bold)}.sy-breadcrumbs ol a+span{color:var(--sy-c-light);padding:0 .4rem;font-weight:300}@media (min-width:1280px){.sy-breadcrumbs{display:none}}@media (min-width:768px){.sy-breadcrumbs-inner{padding:1.5rem 0 1rem}}.globaltoc{padding-bottom:20px}.globaltoc .caption{font-size:.86rem;font-weight:500;font-family:var(--sy-f-heading);color:var(--sy-c-light);text-transform:uppercase;letter-spacing:.4px;border-top:1px solid var(--sy-c-divider);padding:.8rem 0 .4rem}.globaltoc>p.caption:first-of-type{border-top:none;padding-top:0}.globaltoc .caption+ul{margin-bottom:1.5rem}.globaltoc ul+.caption{margin-top:2.5rem}.globaltoc li{margin:.6rem 0}.globaltoc li>ul{margin-left:.6rem;font-size:.96rem}.globaltoc li.toctree-l1>ul{border-left:1px solid var(--gray-3);margin-left:.2rem}.globaltoc li.toctree-l2{border-left:1px solid #0000;margin-left:-1px;padding-left:.9rem}.globaltoc li.toctree-l2.current{border-color:var(--sy-c-link)}.globaltoc>ul a.current{color:var(--sy-c-link);font-weight:500}.globaltoc>ul a:hover{color:var(--sy-c-link-hover)}.globaltoc a.external:after{content:"";-webkit-mask:var(--lucide-external-link-url) no-repeat;-webkit-mask:var(--lucide-external-link-url) no-repeat;mask:var(--lucide-external-link-url) no-repeat;vertical-align:middle;background-color:var(--sy-c-light);width:.825rem;height:.825rem;margin-left:.2rem;font-style:normal;display:inline-block;-webkit-mask-size:100% 100%;mask-size:100% 100%}.globaltoc li{position:relative}.globaltoc li>button{border-radius:3px;justify-content:center;align-items:center;width:1.2rem;height:1.2rem;display:flex;position:absolute;top:.2rem;right:0}.globaltoc li>button:hover{background-color:var(--sy-c-surface)}.globaltoc li.current>ul,.globaltoc li._expand>ul{display:block}.globaltoc li>ul,.globaltoc li._collapse>ul{display:none}.globaltoc li>button>i{transition:transform .2s;transform:rotate(0)}.globaltoc li.current>button>i,.globaltoc li._expand>button>i{transform:rotate(90deg)}.globaltoc li._collapse>button>i{transform:rotate(0)}.sy-deprecated{background-color:#ffdd001a;border-radius:6px;padding:.8rem;font-size:.85rem}.sy-deprecated a{color:var(--sy-c-link);text-decoration:underline}.sy-deprecated a:hover{color:var(--sy-c-link-hover)}.sy-rside-inner>div{margin-bottom:1rem}.sy-rside-inner>div>h3{letter-spacing:.4px;text-transform:uppercase;margin-bottom:1rem;font-size:.8rem;font-weight:500}html[lang=zh] .sy-rside-inner>div>h3,html[lang=zh-TW] .sy-rside-inner>div>h3,html[lang=ja] .sy-rside-inner>div>h3,html[lang=ko] .sy-rside-inner>div>h3{letter-spacing:0;font-size:.86rem;font-weight:600}.localtoc>ul li{margin-top:.36rem;margin-bottom:.36rem}.localtoc>ul li>a:hover{color:var(--sy-c-link-hover)}.localtoc>ul li.active>a{color:var(--sy-c-link)}.localtoc>ul>li ul{padding-left:.8rem}.sy-rside ul.this-page-menu{margin-top:-.6rem}.sy-rside ul.this-page-menu a{font-size:.96rem}.sy-rside ul.this-page-menu a:hover{color:var(--sy-c-link-hover)}.navigation{border-top:1px solid var(--sy-c-divider);gap:2rem;margin-top:2rem;padding-top:1rem}.navigation>div{width:100%}.navigation a{align-items:center;display:inline-flex}.navigation a:hover{color:var(--sy-c-link-hover)}.navigation-next{text-align:right}.navigation-next a{justify-content:end}.navigation .page-info{padding:0 8px}.navigation .page-info>span{color:var(--sy-c-light);font-size:.8rem}:root{--readthedocs-search-font-family:var(--sy-f-text);--readthedocs-search-color:var(--sy-c-text);--readthedocs-search-input-background-color:var(--gray-3);--readthedocs-search-content-border-color:var(--gray-4);--readthedocs-search-content-background-color:var(--sy-c-background);--readthedocs-search-result-section-color:var(--sy-c-text);--readthedocs-search-result-section-subheading-color:var(--sy-c-heading);--readthedocs-search-result-section-highlight-color:var(--accent-9);--readthedocs-search-result-section-border-color:var(--sy-c-border)}.yue button.copybtn{color:var(--syntax-text);background-color:#0000;border:none;justify-content:center;align-items:center}.yue button.copybtn>svg{width:1.4rem;height:1.4rem}.yue button.copybtn:hover{color:var(--syntax-comment)}.yue .highlight button.copybtn:hover{background-color:#0000}.yue button.copybtn:after{color:var(--syntax-text);background-color:#0000}.yue button.copybtn.success{border-color:var(--green-a10);color:var(--green-a10)}.yue button.copybtn.success:after{color:var(--green-a10)}.code-block-caption+div>.highlight .copybtn{opacity:.5;top:-2em}html.light .sd-tab-content{--code-block-background:var(--color-surface-accent);--code-block-caption-background:var(--accent-3)}.yue{--sd-color-primary:var(--accent-a11);--sd-color-secondary:var(--gold-a11);--sd-color-success:var(--green-a11);--sd-color-info:var(--blue-a11);--sd-color-warning:var(--orange-a11);--sd-color-danger:var(--red-a11);--sd-color-light:var(--sand-a2);--sd-color-muted:var(--gray-8);--sd-color-dark:#212122;--sd-color-black:black;--sd-color-white:white;--sd-color-primary-highlight:var(--accent-a8);--sd-color-secondary-highlight:var(--gold-a8);--sd-color-success-highlight:var(--green-a8);--sd-color-info-highlight:var(--blue-a8);--sd-color-warning-highlight:var(--orange-a8);--sd-color-danger-highlight:var(--red-a8);--sd-color-light-highlight:var(--gray-4);--sd-color-muted-highlight:var(--gray-11);--sd-color-dark-highlight:#121211;--sd-color-black-highlight:black;--sd-color-white-highlight:#d9d9d9;--sd-color-primary-text:var(--accent-contrast);--sd-color-secondary-text:var(--gold-contrast);--sd-color-success-text:var(--green-contrast);--sd-color-info-text:var(--blue-contrast);--sd-color-warning-text:var(--orange-contrast);--sd-color-danger-text:var(--red-contrast);--sd-color-light-text:var(--sy-c-text);--sd-color-muted-text:#fff;--sd-color-dark-text:#fff;--sd-color-black-text:#fff;--sd-color-white-text:#212529;--sd-color-shadow:var(--gray-1);--sd-color-card-border:var(--sy-c-border);--sd-color-card-border-hover:var(--accent-a9);--sd-color-tabs-label-inactive:var(--sy-c-bold);--sd-color-tabs-label-active:var(--sd-color-primary);--sd-color-tabs-underline-active:var(--sd-color-primary);--sd-color-tabs-label-hover:var(--accent-9);--sd-color-tabs-underline-hover:var(--accent-9)}.yue .surface{--sd-color-card-text:var(--sy-c-light);--sd-color-card-border:transparent;--sd-color-card-background:var(--sy-c-surface)}.yue a.sd-badge,.yue a.sd-badge:hover{border-bottom:0}.yue .sd-badge{border-radius:3px;font-weight:600}.yue .sd-btn{border-color:var(--sy-c-border)}.yue .sd-tab-set.outline{border:1px solid var(--sy-c-border);border-radius:4px;overflow:auto}.yue .sd-tab-set>label{padding:1rem .25rem .5rem;font-size:.84rem;font-weight:500}.yue .sd-tab-set.outline>label{margin-left:1rem;padding-top:.5rem}.yue .sd-tab-set>label~label{margin-left:1rem}.yue .sd-tab-content{box-shadow:0 -.0625rem var(--sy-c-divider);padding:0}.yue .sd-tab-content .code-block-caption,.yue .sd-tab-content .highlight pre{border-radius:0}.yue .sd-card-title{color:var(--sy-c-text)}.yue .sd-card-title a{border-bottom:0}.yue .sd-card-title>svg,.yue .sd-card-title>iconify-icon{margin-right:.25rem;position:relative;top:-1px}.yue .sd-card-hover:hover{transform:scale(1)}.yue .sd-card-hover:hover .sd-card-title{color:var(--sy-c-link-hover)}.yue .sd-card a.sd-hide-link-text,.yue .sd-card a.sd-hide-link-text:hover{border-bottom:0}.yue .surface .sd-card-header,.yue .surface .sd-card-body,.yue .surface .sd-card-footer{padding-left:1.5rem;padding-right:1.5rem}.yue .surface .sd-card-header,.yue .surface .sd-card-footer{border-color:var(--sy-c-border)}@media (print){.yue .sd-card{page-break-inside:avoid}}.yue a.sd-text-wrap:hover{border-bottom-width:1px}.sphinx-tabs [role=tablist]{border-color:var(--sy-c-divider)}.yue .sphinx-tabs-tab{color:var(--sy-c-text);line-height:inherit;border:none;border-bottom:.125rem solid #0000;padding:1rem .25rem .5rem;font-size:.84rem;font-weight:500}.yue .sphinx-tabs-tab:hover{color:var(--sd-color-tabs-label-hover);border-color:var(--sd-color-tabs-underline-hover)}.yue .sphinx-tabs-tab[aria-selected=true]{border:none;border-bottom:.125rem solid var(--sd-color-tabs-underline-active);color:var(--sd-color-tabs-label-active);background-color:#0000}.yue .sphinx-tabs-tab+.sphinx-tabs-tab{margin-left:1rem}.yue .sphinx-tabs-panel{background-color:#0000;border:none;border-radius:0;margin:0;padding:0}.yue .sphinx-tabs-panel.code-tab{padding:0}.yue .sphinx-tabs-panel.code-tab .code-block-caption,.yue .sphinx-tabs-panel.code-tab .highlight pre{border-radius:0}html.light .jupyter_container .cell_output{--code-block-background:var(--color-surface-accent);--code-block-caption-background:var(--accent-3)}.yue{--jp-widgets-input-border-color:var(--gray-5);--jp-widgets-input-focus-border-color:var(--gray-8);--jp-widgets-slider-active-handle-color:var(--gray-4);--jp-widgets-slider-handle-border-color:var(--sy-c-border)}.yue .jupyter_container{background-color:var(--sy-c-background);border:3px solid var(--sy-c-border);box-shadow:none;border-radius:6px;overflow:hidden}.sy-main .yue .jupyter_container div[class^=highlight]{padding:0}.yue .jupyter_container div.highlight{background-color:var(--code-block-background)}.yue .jupyter_container div.cell_input{background-color:var(--code-block-background);border:0;border-radius:0}.yue .jupyter_container div.code_cell pre{padding:0}.jupyter_container div.cell_output .output,.jupyter_container div.cell_output .stderr,.jupyter_container div.cell_output .widget-subarea{padding:.5rem}.jupyter_container div.cell_output .stderr .stderr{padding:0}.widget-hslider .slider-container,.jupyter-widget-hslider .slider-container{align-items:center;display:flex}.widget-slider .noUi-target,.jupyter-widget-slider .noUi-target{width:100%}.jupyter_container div.code_cell .highlight>pre{padding:1rem}.jupyter_container div.code_cell .highlight .hll{margin-left:-1rem;margin-right:-1rem;padding:0 1rem}.jupyter_container div.code_cell .highlight .linenos{margin-right:.8rem}.yue .jupyter_container .stderr{color:var(--red-a11);background-color:var(--red-a3)}.yue .jupyter_container .stderr .stderr{background-color:#0000}.nbinput .highlight{--radius:1px}.yue div.nblast.container{padding-top:5px}.yue div.nbinput.container div.input_area{border-color:var(--sy-c-border)}.yue div.nboutput.container div.output_area.stderr{color:var(--red-a11);background-color:var(--red-a3)}.yue div.nboutput.container div.output_area>.math-wrapper>div.math{padding-top:0}.yue .jp-RenderedHTMLCommon thead,.yue div.rendered_html thead{border-color:var(--sy-c-border)}.yue .jp-RenderedHTMLCommon tbody tr,.yue div.rendered_html tbody tr{color:var(--sy-c-text)}.yue .jp-RenderedHTMLCommon tbody tr:nth-child(odd),.yue div.rendered_html tbody tr:nth-child(odd){background-color:var(--sy-c-surface)}.yue .jp-RenderedHTMLCommon tbody tr:hover,.yue div.rendered_html tbody tr:hover{background-color:var(--color-surface-accent)}.yue{--sg-text-color:var(--sy-c-text);--sg-background-color:var(--sy-c-background);--sg-code-background-color:var(--code-block-background);--sg-tr-hover-color:var(--accent-a3);--sg-tr-odd-color:var(--sy-c-surface);--sg-tooltip-foreground:var(--sy-c-background-contrast);--sg-tooltip-background:var(--sy-c-background);--sg-tooltip-border:var(--gray-7) transparent;--sg-thumb-box-shadow-color:var(--gray-a4);--sg-thumb-hover-border:var(--accent-a9);--sg-script-out:var(--sy-c-light);--sg-script-pre:var(--code-block-background);--sg-pytb-foreground:var(--syntax-text);--sg-pytb-background:var(--red-a2);--sg-pytb-border-color:var(--red-a8);--sg-download-a-background-color:var(--accent-a3);--sg-download-a-background-image:none;--sg-download-a-border-color:1px solid var(--accent-a3);--sg-download-a-color:var(--accent-a11);--sg-download-a-hover-background-color:var(--accent-a4);--sg-download-a-hover-box-shadow-1:transparent;--sg-download-a-hover-box-shadow-2:transparent}.yue .sphx-glr-thumbnails a,.yue .sphx-glr-download a,.yue .sphx-glr-download a:hover{border-bottom:0}.yue p.sphx-glr-signature a{border-bottom:0;border-radius:0;text-decoration:underline}.yue p.sphx-glr-signature a:hover{color:var(--sy-c-link-hover)}.yue .sphx-glr-footer img{margin:0;display:inline}html.light,html.dark{--docsearch-background-color:var(--sy-c-background);--docsearch-icon-color:var(--gray-11);--docsearch-secondary-text-color:var(--gray-11);--docsearch-modal-background:var(--sy-c-background);--docsearch-footer-background:var(--sy-c-surface);--docsearch-primary-color:var(--accent-9);--docsearch-soft-primary-color:var(--accent-a2);--docsearch-subtle-color:var(--gray-4);--docsearch-text-color:var(--sy-c-text);--docsearch-key-background:var(--gray-1);--docsearch-searchbox-background:var(--gray-3);--docsearch-searchbox-focus-background:var(--sy-c-background);--docsearch-muted-color:var(--gray-10);--docsearch-focus-color:var(--accent-a8);--docsearch-highlight-color:var(--accent-a11);--docsearch-hit-color:var(--sy-c-text);--docsearch-hit-background:var(--gray-a2);--docsearch-hit-highlight-color:var(--accent-a3);--docsearch-hit-shadow:inset 0 0 1px 0 var(--gray-a11);--docsearch-container-background:var(--gray-a5)}html.dark{--docsearch-modal-shadow:inset 1px 1px 0 0 #373737,0 3px 8px 0 #141414}#docsearch .DocSearch-Button{box-sizing:border-box;width:auto;height:32px;margin:0}#docsearch .DocSearch-Search-Icon{width:1rem;height:1rem}@media (max-width:767px){html.light,html.dark{--docsearch-searchbox-background:transparent}#docsearch{position:absolute;top:1rem;left:1.8rem;right:1.8rem}#docsearch .DocSearch-Button{width:100%;margin-left:0}}dl.sqla dt{color:var(--sig-name);margin-bottom:.5rem}dl.sqla dt>em{color:var(--sig-param);font-style:normal;font-weight:400}dl.sqla dd>p.rubric{text-transform:uppercase;margin-top:1.5rem;font-size:.76rem}dl.sqla dd>p.rubric+.table-wrapper{border-left:0;border-right:0;border-radius:0;margin-top:.75rem}dl.sqla p.rubric+.table-wrapper th,dl.sqla p.rubric+.table-wrapper td{background-color:#0000;border-left:0;border-right:0}dl.sqla p.rubric+.table-wrapper td>p{margin:0}dl.sqla p.rubric+.table-wrapper tr.row-odd{background-color:#0000}dl.sqla p.rubric+.table-wrapper tr.row-even{background-color:var(--yue-c-row-background)}.yue details.toggle-details{background-color:var(--slate-a2);border-radius:.2em;padding:0 1rem}.yue details.toggle-details summary{border-left-color:var(--accent-a9);background-color:var(--gray-a2);margin-left:-1rem;margin-right:-1rem}.yue details.toggle-details[open] summary{border-radius:.2em .2em 0 0}.yue .toggle-details__container{margin-top:0;margin-bottom:0;padding-top:1rem;padding-bottom:1rem}.yue .toggle-details__container :first-child{margin-top:0}.yue .toggle-details__container :last-child{margin-bottom:0}.yue .admonition.toggle-hidden .admonition-title~*{margin-bottom:0!important}.sphinx-contributors{container:contributors/inline-size}.sphinx-contributors .sphinx-contributors_list__item{margin:0;padding:0}.sphinx-contributors .sphinx-contributors_contributor__username,.sphinx-contributors .sphinx-contributors_contributor__contributions{margin:0}.sphinx-contributors.sphinx-contributors--avatars .sphinx-contributors_list{justify-content:flex-start;gap:1rem;padding:0}.sphinx-contributors--avatars .sphinx-contributors_contributor__username{margin-top:.5rem}.sphinx-contributors--avatars .sphinx-contributors_contributor__contributions{font-size:.875rem}.sphinx-contributors--avatars .sphinx-contributors_contributor__contributions:before{content:"";padding:0}.sphinx-contributors--avatars .sphinx-contributors_contributor{align-items:center}@container contributors (max-width:800px){.sphinx-contributors.sphinx-contributors--avatars .sphinx-contributors_list{column-gap:0}.sphinx-contributors--avatars .sphinx-contributors_list__item{flex-basis:25%}}@container contributors (max-width:700px){.sphinx-contributors--avatars img.sphinx-contributors_contributor__image{width:120px}}@container contributors (max-width:600px){.sphinx-contributors--avatars .sphinx-contributors_list__item{flex-basis:33.33%}}@container contributors (max-width:500px){.sphinx-contributors--avatars img.sphinx-contributors_contributor__image{width:120px}}@container contributors (max-width:400px){.sphinx-contributors--avatars .sphinx-contributors_list__item{flex-basis:50%}}.sphinx-sponsors--2xl{--sponsor-avatar-size:120px;--sponsor-item-padding:20px}.sphinx-sponsors--xl{--sponsor-avatar-size:90px;--sponsor-item-padding:15px}.sphinx-sponsors--lg{--sponsor-avatar-size:70px;--sponsor-item-padding:15px}.sphinx-sponsors--md{--sponsor-avatar-size:50px;--sponsor-item-padding:10px}.sphinx-sponsors--base{--sponsor-avatar-size:40px;--sponsor-item-padding:6px}.sphinx-sponsors--sm{--sponsor-avatar-size:35px;--sponsor-item-padding:5px}.sphinx-sponsors--xs{--sponsor-avatar-size:25px;--sponsor-item-padding:4px}.sphinx-sponsors{margin:2rem 0}.sphinx-sponsors .rubric{font-size:1.25em}ul.sphinx-sponsors_container{gap:var(--sponsor-item-padding);flex-wrap:wrap;margin:0;padding:0;list-style-type:none;display:flex}.sphinx-sponsors li.sphinx-sponsors_item{flex-direction:column;align-items:center;margin:0;padding:0;display:flex}.sphinx-sponsors_avatar{width:var(--sponsor-avatar-size);background:var(--gray-a3);display:inline-flex}.sphinx-sponsors_name{margin-top:.25rem;font-size:.875rem}.sphinx-sponsors--center{text-align:center}.sphinx-sponsors--center ul.sphinx-sponsors_container{gap:0}.sphinx-sponsors--center li.sphinx-sponsors_item{padding:var(--sponsor-item-padding)}.sphinx-sponsors--center .sphinx-sponsors_container{justify-content:center}.sphinx-sponsors--rounded .sphinx-sponsors_avatar,.sphinx-sponsors--rounded img{border-radius:100%}.yue{--xr-font-color0:var(--sy-c-heading);--xr-font-color2:var(--sy-c-text);--xr-font-color3:var(--sy-c-light);--xr-border-color:var(--sy-c-border);--xr-disabled-color:var(--gray-a6);--xr-background-color:var(--sy-c-background);--xr-background-color-row-even:var(--sy-c-background);--xr-background-color-row-odd:var(--gray-2)}.yue .xr-array-data pre{margin:0}.yue iconify-icon[data-accent-color]{color:var(--accent-9)}.dark pre.mermaid>svg{filter:brightness(.8)invert(.82)contrast(1.2)}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false} \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/_static/shibuya.js b/tools/docs/_build/dirhtml/_static/shibuya.js new file mode 100644 index 0000000..be39892 --- /dev/null +++ b/tools/docs/_build/dirhtml/_static/shibuya.js @@ -0,0 +1,43 @@ +(()=>{var ie=Object.create;var ft=Object.defineProperty;var re=Object.getOwnPropertyDescriptor;var se=Object.getOwnPropertyNames;var ce=Object.getPrototypeOf,ae=Object.prototype.hasOwnProperty;var le=(t=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(t,{get:(e,n)=>(typeof require<"u"?require:e)[n]}):t)(function(t){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+t+'" is not supported')});var ue=(t,e,n,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of se(e))!ae.call(t,o)&&o!==n&&ft(t,o,{get:()=>e[o],enumerable:!(i=re(e,o))||i.enumerable});return t};var de=(t,e,n)=>(n=t!=null?ie(ce(t)):{},ue(e||!t||!t.__esModule?ft(n,"default",{value:t,enumerable:!0}):n,t));var Lt=Object.freeze({left:0,top:0,width:16,height:16}),R=Object.freeze({rotate:0,vFlip:!1,hFlip:!1}),j=Object.freeze({...Lt,...R}),K=Object.freeze({...j,body:"",hidden:!1}),fe=Object.freeze({width:null,height:null}),Et=Object.freeze({...fe,...R});function he(t,e=0){let n=t.replace(/^-?[0-9.]*/,"");function i(o){for(;o<0;)o+=4;return o%4}if(n===""){let o=parseInt(t);return isNaN(o)?0:i(o)}else if(n!==t){let o=0;switch(n){case"%":o=25;break;case"deg":o=90}if(o){let r=parseFloat(t.slice(0,t.length-n.length));return isNaN(r)?0:(r=r/o,r%1===0?i(r):0)}}return e}var pe=/[\s,]+/;function ge(t,e){e.split(pe).forEach(n=>{switch(n.trim()){case"horizontal":t.hFlip=!0;break;case"vertical":t.vFlip=!0;break}})}var Tt={...Et,preserveAspectRatio:""};function ht(t){let e={...Tt},n=(i,o)=>t.getAttribute(i)||o;return e.width=n("width",null),e.height=n("height",null),e.rotate=he(n("rotate","")),ge(e,n("flip","")),e.preserveAspectRatio=n("preserveAspectRatio",n("preserveaspectratio","")),e}function me(t,e){for(let n in Tt)if(t[n]!==e[n])return!0;return!1}var Ot=/^[a-z0-9]+(-[a-z0-9]+)*$/,N=(t,e,n,i="")=>{let o=t.split(":");if(t.slice(0,1)==="@"){if(o.length<2||o.length>3)return null;i=o.shift().slice(1)}if(o.length>3||!o.length)return null;if(o.length>1){let s=o.pop(),a=o.pop(),l={provider:o.length>0?o[0]:i,prefix:a,name:s};return e&&!M(l)?null:l}let r=o[0],c=r.split("-");if(c.length>1){let s={provider:i,prefix:c.shift(),name:c.join("-")};return e&&!M(s)?null:s}if(n&&i===""){let s={provider:i,prefix:"",name:r};return e&&!M(s,n)?null:s}return null},M=(t,e)=>t?!!((e&&t.prefix===""||t.prefix)&&t.name):!1;function be(t,e){let n=t.icons,i=t.aliases||Object.create(null),o=Object.create(null);function r(c){if(n[c])return o[c]=[];if(!(c in o)){o[c]=null;let s=i[c]&&i[c].parent,a=s&&r(s);a&&(o[c]=[s].concat(a))}return o[c]}return Object.keys(n).concat(Object.keys(i)).forEach(r),o}function ye(t,e){let n={};!t.hFlip!=!e.hFlip&&(n.hFlip=!0),!t.vFlip!=!e.vFlip&&(n.vFlip=!0);let i=((t.rotate||0)+(e.rotate||0))%4;return i&&(n.rotate=i),n}function pt(t,e){let n=ye(t,e);for(let i in K)i in R?i in t&&!(i in n)&&(n[i]=R[i]):i in e?n[i]=e[i]:i in t&&(n[i]=t[i]);return n}function ve(t,e,n){let i=t.icons,o=t.aliases||Object.create(null),r={};function c(s){r=pt(i[s]||o[s],r)}return c(e),n.forEach(c),pt(t,r)}function jt(t,e){let n=[];if(typeof t!="object"||typeof t.icons!="object")return n;t.not_found instanceof Array&&t.not_found.forEach(o=>{e(o,null),n.push(o)});let i=be(t);for(let o in i){let r=i[o];r&&(e(o,ve(t,o,r)),n.push(o))}return n}var we={provider:"",aliases:{},not_found:{},...Lt};function J(t,e){for(let n in e)if(n in t&&typeof t[n]!=typeof e[n])return!1;return!0}function Nt(t){if(typeof t!="object"||t===null)return null;let e=t;if(typeof e.prefix!="string"||!t.icons||typeof t.icons!="object"||!J(t,we))return null;let n=e.icons;for(let o in n){let r=n[o];if(!o||typeof r.body!="string"||!J(r,K))return null}let i=e.aliases||Object.create(null);for(let o in i){let r=i[o],c=r.parent;if(!o||typeof c!="string"||!n[c]&&!i[c]||!J(r,K))return null}return e}var F=Object.create(null);function xe(t,e){return{provider:t,prefix:e,icons:Object.create(null),missing:new Set}}function v(t,e){let n=F[t]||(F[t]=Object.create(null));return n[e]||(n[e]=xe(t,e))}function Pt(t,e){return Nt(e)?jt(e,(n,i)=>{i?t.icons[n]=i:t.missing.add(n)}):[]}function Ie(t,e,n){try{if(typeof n.body=="string")return t.icons[e]={...n},!0}catch{}return!1}function Ae(t,e){let n=[];return(typeof t=="string"?[t]:Object.keys(F)).forEach(i=>{(typeof i=="string"&&typeof e=="string"?[e]:Object.keys(F[i]||{})).forEach(o=>{let r=v(i,o);n=n.concat(Object.keys(r.icons).map(c=>(i!==""?"@"+i+":":"")+o+":"+c))})}),n}var T=!1;function Mt(t){return typeof t=="boolean"&&(T=t),T}function O(t){let e=typeof t=="string"?N(t,!0,T):t;if(e){let n=v(e.provider,e.prefix),i=e.name;return n.icons[i]||(n.missing.has(i)?null:void 0)}}function qt(t,e){let n=N(t,!0,T);if(!n)return!1;let i=v(n.provider,n.prefix);return e?Ie(i,n.name,e):(i.missing.add(n.name),!0)}function gt(t,e){if(typeof t!="object")return!1;if(typeof e!="string"&&(e=t.provider||""),T&&!e&&!t.prefix){let i=!1;return Nt(t)&&(t.prefix="",jt(t,(o,r)=>{qt(o,r)&&(i=!0)})),i}let n=t.prefix;return M({prefix:n,name:"a"})?!!Pt(v(e,n),t):!1}function Se(t){return!!O(t)}function Ce(t){let e=O(t);return e&&{...j,...e}}function Rt(t,e){t.forEach(n=>{let i=n.loaderCallbacks;i&&(n.loaderCallbacks=i.filter(o=>o.id!==e))})}function ke(t){t.pendingCallbacksFlag||(t.pendingCallbacksFlag=!0,setTimeout(()=>{t.pendingCallbacksFlag=!1;let e=t.loaderCallbacks?t.loaderCallbacks.slice(0):[];if(!e.length)return;let n=!1,i=t.provider,o=t.prefix;e.forEach(r=>{let c=r.icons,s=c.pending.length;c.pending=c.pending.filter(a=>{if(a.prefix!==o)return!0;let l=a.name;if(t.icons[l])c.loaded.push({provider:i,prefix:o,name:l});else if(t.missing.has(l))c.missing.push({provider:i,prefix:o,name:l});else return n=!0,!0;return!1}),c.pending.length!==s&&(n||Rt([t],r.id),r.callback(c.loaded.slice(0),c.missing.slice(0),c.pending.slice(0),r.abort))})}))}var _e=0;function Le(t,e,n){let i=_e++,o=Rt.bind(null,n,i);if(!e.pending.length)return o;let r={id:i,icons:e,callback:t,abort:o};return n.forEach(c=>{(c.loaderCallbacks||(c.loaderCallbacks=[])).push(r)}),o}function Ee(t){let e={loaded:[],missing:[],pending:[]},n=Object.create(null);t.sort((o,r)=>o.provider!==r.provider?o.provider.localeCompare(r.provider):o.prefix!==r.prefix?o.prefix.localeCompare(r.prefix):o.name.localeCompare(r.name));let i={provider:"",prefix:"",name:""};return t.forEach(o=>{if(i.name===o.name&&i.prefix===o.prefix&&i.provider===o.provider)return;i=o;let r=o.provider,c=o.prefix,s=o.name,a=n[r]||(n[r]=Object.create(null)),l=a[c]||(a[c]=v(r,c)),u;s in l.icons?u=e.loaded:c===""||l.missing.has(s)?u=e.missing:u=e.pending;let f={provider:r,prefix:c,name:s};u.push(f)}),e}var X=Object.create(null);function mt(t,e){X[t]=e}function Z(t){return X[t]||X[""]}function Te(t,e=!0,n=!1){let i=[];return t.forEach(o=>{let r=typeof o=="string"?N(o,e,n):o;r&&i.push(r)}),i}function ot(t){let e;if(typeof t.resources=="string")e=[t.resources];else if(e=t.resources,!(e instanceof Array)||!e.length)return null;return{resources:e,path:t.path||"/",maxURL:t.maxURL||500,rotate:t.rotate||750,timeout:t.timeout||5e3,random:t.random===!0,index:t.index||0,dataAfterTimeout:t.dataAfterTimeout!==!1}}var B=Object.create(null),_=["https://api.simplesvg.com","https://api.unisvg.com"],q=[];for(;_.length>0;)_.length===1||Math.random()>.5?q.push(_.shift()):q.push(_.pop());B[""]=ot({resources:["https://api.iconify.design"].concat(q)});function bt(t,e){let n=ot(e);return n===null?!1:(B[t]=n,!0)}function H(t){return B[t]}function Oe(){return Object.keys(B)}var je={resources:[],index:0,timeout:2e3,rotate:750,random:!1,dataAfterTimeout:!1};function Ne(t,e,n,i){let o=t.resources.length,r=t.random?Math.floor(Math.random()*o):t.index,c;if(t.random){let d=t.resources.slice(0);for(c=[];d.length>1;){let b=Math.floor(Math.random()*d.length);c.push(d[b]),d=d.slice(0,b).concat(d.slice(b+1))}c=c.concat(d)}else c=t.resources.slice(r).concat(t.resources.slice(0,r));let s=Date.now(),a="pending",l=0,u,f=null,h=[],g=[];typeof i=="function"&&g.push(i);function w(){f&&(clearTimeout(f),f=null)}function x(){a==="pending"&&(a="aborted"),w(),h.forEach(d=>{d.status==="pending"&&(d.status="aborted")}),h=[]}function p(d,b){b&&(g=[]),typeof d=="function"&&g.push(d)}function z(){return{startTime:s,payload:e,status:a,queriesSent:l,queriesPending:h.length,subscribe:p,abort:x}}function I(){a="failed",g.forEach(d=>{d(void 0,u)})}function y(){h.forEach(d=>{d.status==="pending"&&(d.status="aborted")}),h=[]}function m(d,b,k){let P=b!=="success";switch(h=h.filter(A=>A!==d),a){case"pending":break;case"failed":if(P||!t.dataAfterTimeout)return;break;default:return}if(b==="abort"){u=k,I();return}if(P){u=k,h.length||(c.length?G():I());return}if(w(),y(),!t.random){let A=t.resources.indexOf(d.resource);A!==-1&&A!==t.index&&(t.index=A)}a="completed",g.forEach(A=>{A(k)})}function G(){if(a!=="pending")return;w();let d=c.shift();if(d===void 0){if(h.length){f=setTimeout(()=>{w(),a==="pending"&&(y(),I())},t.timeout);return}I();return}let b={status:"pending",resource:d,callback:(k,P)=>{m(b,k,P)}};h.push(b),l++,f=setTimeout(G,t.rotate),n(d,e,b.callback)}return setTimeout(G),z}function Ft(t){let e={...je,...t},n=[];function i(){n=n.filter(c=>c().status==="pending")}function o(c,s,a){let l=Ne(e,c,s,(u,f)=>{i(),a&&a(u,f)});return n.push(l),l}function r(c){return n.find(s=>c(s))||null}return{query:o,find:r,setIndex:c=>{e.index=c},getIndex:()=>e.index,cleanup:i}}function yt(){}var Y=Object.create(null);function Pe(t){if(!Y[t]){let e=H(t);if(!e)return;Y[t]={config:e,redundancy:Ft(e)}}return Y[t]}function Dt(t,e,n){let i,o;if(typeof t=="string"){let r=Z(t);if(!r)return n(void 0,424),yt;o=r.send;let c=Pe(t);c&&(i=c.redundancy)}else{let r=ot(t);if(r){i=Ft(r);let c=Z(t.resources?t.resources[0]:"");c&&(o=c.send)}}return!i||!o?(n(void 0,424),yt):i.query(e,o,n)().abort}function vt(){}function Me(t){t.iconsLoaderFlag||(t.iconsLoaderFlag=!0,setTimeout(()=>{t.iconsLoaderFlag=!1,ke(t)}))}function qe(t){let e=[],n=[];return t.forEach(i=>{(i.match(Ot)?e:n).push(i)}),{valid:e,invalid:n}}function L(t,e,n){function i(){let o=t.pendingIcons;e.forEach(r=>{o&&o.delete(r),t.icons[r]||t.missing.add(r)})}if(n&&typeof n=="object")try{if(!Pt(t,n).length){i();return}}catch(o){console.error(o)}i(),Me(t)}function wt(t,e){t instanceof Promise?t.then(n=>{e(n)}).catch(()=>{e(null)}):e(t)}function Re(t,e){t.iconsToLoad?t.iconsToLoad=t.iconsToLoad.concat(e).sort():t.iconsToLoad=e,t.iconsQueueFlag||(t.iconsQueueFlag=!0,setTimeout(()=>{t.iconsQueueFlag=!1;let{provider:n,prefix:i}=t,o=t.iconsToLoad;if(delete t.iconsToLoad,!o||!o.length)return;let r=t.loadIcon;if(t.loadIcons&&(o.length>1||!r)){wt(t.loadIcons(o,i,n),l=>{L(t,o,l)});return}if(r){o.forEach(l=>{wt(r(l,i,n),u=>{L(t,[l],u?{prefix:i,icons:{[l]:u}}:null)})});return}let{valid:c,invalid:s}=qe(o);if(s.length&&L(t,s,null),!c.length)return;let a=i.match(Ot)?Z(n):null;if(!a){L(t,c,null);return}a.prepare(n,i,c).forEach(l=>{Dt(n,l,u=>{L(t,l.icons,u)})})}))}var it=(t,e)=>{let n=Ee(Te(t,!0,Mt()));if(!n.pending.length){let s=!0;return e&&setTimeout(()=>{s&&e(n.loaded,n.missing,n.pending,vt)}),()=>{s=!1}}let i=Object.create(null),o=[],r,c;return n.pending.forEach(s=>{let{provider:a,prefix:l}=s;if(l===c&&a===r)return;r=a,c=l,o.push(v(a,l));let u=i[a]||(i[a]=Object.create(null));u[l]||(u[l]=[])}),n.pending.forEach(s=>{let{provider:a,prefix:l,name:u}=s,f=v(a,l),h=f.pendingIcons||(f.pendingIcons=new Set);h.has(u)||(h.add(u),i[a][l].push(u))}),o.forEach(s=>{let a=i[s.provider][s.prefix];a.length&&Re(s,a)}),e?Le(e,n,o):vt},Fe=t=>new Promise((e,n)=>{let i=typeof t=="string"?N(t,!0):t;if(!i){n(t);return}it([i||t],o=>{if(o.length&&i){let r=O(i);if(r){e({...j,...r});return}}n(t)})});function xt(t){try{let e=typeof t=="string"?JSON.parse(t):t;if(typeof e.body=="string")return{...e}}catch{}}function De(t,e){if(typeof t=="object")return{data:xt(t),value:t};if(typeof t!="string")return{value:t};if(t.includes("{")){let r=xt(t);if(r)return{data:r,value:t}}let n=N(t,!0,!0);if(!n)return{value:t};let i=O(n);if(i!==void 0||!n.prefix)return{value:t,name:n,data:i};let o=it([n],()=>e(t,n,O(n)));return{value:t,name:n,loading:o}}var Bt=!1;try{Bt=navigator.vendor.indexOf("Apple")===0}catch{}function Be(t,e){switch(e){case"svg":case"bg":case"mask":return e}return e!=="style"&&(Bt||t.indexOf("=0;){let o=t.indexOf(">",i),r=t.indexOf("",r);if(c===-1)break;n+=t.slice(o+1,r).trim(),t=t.slice(0,i).trim()+t.slice(c+1)}return{defs:n,content:t}}function Ve(t,e){return t?""+t+""+e:e}function Ue(t,e,n){let i=Qe(t);return Ve(i.defs,e+i.content+n)}var ze=t=>t==="unset"||t==="undefined"||t==="none";function Ht(t,e){let n={...j,...t},i={...Et,...e},o={left:n.left,top:n.top,width:n.width,height:n.height},r=n.body;[n,i].forEach(x=>{let p=[],z=x.hFlip,I=x.vFlip,y=x.rotate;z?I?y+=2:(p.push("translate("+(o.width+o.left).toString()+" "+(0-o.top).toString()+")"),p.push("scale(-1 1)"),o.top=o.left=0):I&&(p.push("translate("+(0-o.left).toString()+" "+(o.height+o.top).toString()+")"),p.push("scale(1 -1)"),o.top=o.left=0);let m;switch(y<0&&(y-=Math.floor(y/4)*4),y=y%4,y){case 1:m=o.height/2+o.top,p.unshift("rotate(90 "+m.toString()+" "+m.toString()+")");break;case 2:p.unshift("rotate(180 "+(o.width/2+o.left).toString()+" "+(o.height/2+o.top).toString()+")");break;case 3:m=o.width/2+o.left,p.unshift("rotate(-90 "+m.toString()+" "+m.toString()+")");break}y%2===1&&(o.left!==o.top&&(m=o.left,o.left=o.top,o.top=m),o.width!==o.height&&(m=o.width,o.width=o.height,o.height=m)),p.length&&(r=Ue(r,'',""))});let c=i.width,s=i.height,a=o.width,l=o.height,u,f;c===null?(f=s===null?"1em":s==="auto"?l:s,u=tt(f,a/l)):(u=c==="auto"?a:c,f=s===null?tt(u,l/a):s==="auto"?l:s);let h={},g=(x,p)=>{ze(p)||(h[x]=p.toString())};g("width",u),g("height",f);let w=[o.left,o.top,a,l];return h.viewBox=w.join(" "),{attributes:h,viewBox:w,body:r}}function rt(t,e){let n=t.indexOf("xlink:")===-1?"":' xmlns:xlink="http://www.w3.org/1999/xlink"';for(let i in e)n+=" "+i+'="'+e[i]+'"';return'"+t+""}function Ge(t){return t.replace(/"/g,"'").replace(/%/g,"%25").replace(/#/g,"%23").replace(//g,"%3E").replace(/\s+/g," ")}function Je(t){return"data:image/svg+xml,"+Ge(t)}function $t(t){return'url("'+Je(t)+'")'}var Ye=()=>{let t;try{if(t=fetch,typeof t=="function")return t}catch{}},D=Ye();function We(t){D=t}function Ke(){return D}function Xe(t,e){let n=H(t);if(!n)return 0;let i;if(!n.maxURL)i=0;else{let o=0;n.resources.forEach(c=>{o=Math.max(o,c.length)});let r=e+".json?icons=";i=n.maxURL-o-n.path.length-r.length}return i}function Ze(t){return t===404}var tn=(t,e,n)=>{let i=[],o=Xe(t,e),r="icons",c={type:r,provider:t,prefix:e,icons:[]},s=0;return n.forEach((a,l)=>{s+=a.length+1,s>=o&&l>0&&(i.push(c),c={type:r,provider:t,prefix:e,icons:[]},s=a.length),c.icons.push(a)}),i.push(c),i};function en(t){if(typeof t=="string"){let e=H(t);if(e)return e.path}return"/"}var nn=(t,e,n)=>{if(!D){n("abort",424);return}let i=en(e.provider);switch(e.type){case"icons":{let r=e.prefix,c=e.icons.join(","),s=new URLSearchParams({icons:c});i+=r+".json?"+s.toString();break}case"custom":{let r=e.uri;i+=r.slice(0,1)==="/"?r.slice(1):r;break}default:n("abort",400);return}let o=503;D(t+i).then(r=>{let c=r.status;if(c!==200){setTimeout(()=>{n(Ze(c)?"abort":"next",c)});return}return o=501,r.json()}).then(r=>{if(typeof r!="object"||r===null){setTimeout(()=>{r===404?n("abort",r):n("next",o)});return}setTimeout(()=>{n("success",r)})}).catch(()=>{n("next",o)})},on={prepare:tn,send:nn};function rn(t,e,n){v(n||"",e).loadIcons=t}function sn(t,e,n){v(n||"",e).loadIcon=t}var W="data-style",Qt="";function cn(t){Qt=t}function It(t,e){let n=Array.from(t.childNodes).find(i=>i.hasAttribute&&i.hasAttribute(W));n||(n=document.createElement("style"),n.setAttribute(W,W),t.appendChild(n)),n.textContent=":host{display:inline-block;vertical-align:"+(e?"-0.125em":"0")+"}span,svg{display:block;margin:auto}"+Qt}function Vt(){mt("",on),Mt(!0);let t;try{t=window}catch{}if(t){if(t.IconifyPreload!==void 0){let n=t.IconifyPreload,i="Invalid IconifyPreload syntax.";typeof n=="object"&&n!==null&&(n instanceof Array?n:[n]).forEach(o=>{try{(typeof o!="object"||o===null||o instanceof Array||typeof o.icons!="object"||typeof o.prefix!="string"||!gt(o))&&console.error(i)}catch{console.error(i)}})}if(t.IconifyProviders!==void 0){let n=t.IconifyProviders;if(typeof n=="object"&&n!==null)for(let i in n){let o="IconifyProviders["+i+"] is invalid.";try{let r=n[i];if(typeof r!="object"||!r||r.resources===void 0)continue;bt(i,r)||console.error(o)}catch{console.error(o)}}}}return{iconLoaded:Se,getIcon:Ce,listIcons:Ae,addIcon:qt,addCollection:gt,calculateSize:tt,buildIcon:Ht,iconToHTML:rt,svgToURL:$t,loadIcons:it,loadIcon:Fe,addAPIProvider:bt,setCustomIconLoader:sn,setCustomIconsLoader:rn,appendCustomStyle:cn,_api:{getAPIConfig:H,setAPIModule:mt,sendAPIQuery:Dt,setFetch:We,getFetch:Ke,listAPIProviders:Oe}}}var et={"background-color":"currentColor"},Ut={"background-color":"transparent"},At={image:"var(--svg)",repeat:"no-repeat",size:"100% 100%"},St={"-webkit-mask":et,mask:et,background:Ut};for(let t in St){let e=St[t];for(let n in At)e[t+"-"+n]=At[n]}function Ct(t){return t?t+(t.match(/^[-0-9.]+$/)?"px":""):"inherit"}function an(t,e,n){let i=document.createElement("span"),o=t.body;o.indexOf("");let r=t.attributes,c=rt(o,{...r,width:e.width+"",height:e.height+""}),s=$t(c),a=i.style,l={"--svg":s,width:Ct(r.width),height:Ct(r.height),...n?et:Ut};for(let u in l)a.setProperty(u,l[u]);return i}var E;function ln(){try{E=window.trustedTypes.createPolicy("iconify",{createHTML:t=>t})}catch{E=null}}function un(t){return E===void 0&&ln(),E?E.createHTML(t):t}function dn(t){let e=document.createElement("span"),n=t.attributes,i="";n.width||(i="width: inherit;"),n.height||(i+="height: inherit;"),i&&(n.style=i);let o=rt(t.body,n);return e.innerHTML=un(o),e.firstChild}function nt(t){return Array.from(t.childNodes).find(e=>{let n=e.tagName&&e.tagName.toUpperCase();return n==="SPAN"||n==="SVG"})}function kt(t,e){let n=e.icon.data,i=e.customisations,o=Ht(n,i);i.preserveAspectRatio&&(o.attributes.preserveAspectRatio=i.preserveAspectRatio);let r=e.renderedMode,c;r==="svg"?c=dn(o):c=an(o,{...j,...n},r==="mask");let s=nt(t);s?c.tagName==="SPAN"&&s.tagName===c.tagName?s.setAttribute("style",c.getAttribute("style")):t.replaceChild(c,s):t.appendChild(c)}function _t(t,e,n){let i=n&&(n.rendered?n:n.lastRender);return{rendered:!1,inline:e,icon:t,lastRender:i}}function fn(t="iconify-icon"){let e,n;try{e=window.customElements,n=window.HTMLElement}catch{return}if(!e||!n)return;let i=e.get(t);if(i)return i;let o=["icon","mode","inline","noobserver","width","height","rotate","flip"],r=class extends n{_shadowRoot;_initialised=!1;_state;_checkQueued=!1;_connected=!1;_observer=null;_visible=!0;constructor(){super();let s=this._shadowRoot=this.attachShadow({mode:"open"}),a=this.hasAttribute("inline");It(s,a),this._state=_t({value:""},a),this._queueCheck()}connectedCallback(){this._connected=!0,this.startObserver()}disconnectedCallback(){this._connected=!1,this.stopObserver()}static get observedAttributes(){return o.slice(0)}attributeChangedCallback(s){switch(s){case"inline":{let a=this.hasAttribute("inline"),l=this._state;a!==l.inline&&(l.inline=a,It(this._shadowRoot,a));break}case"noobserver":{this.hasAttribute("noobserver")?this.startObserver():this.stopObserver();break}default:this._queueCheck()}}get icon(){let s=this.getAttribute("icon");if(s&&s.slice(0,1)==="{")try{return JSON.parse(s)}catch{}return s}set icon(s){typeof s=="object"&&(s=JSON.stringify(s)),this.setAttribute("icon",s)}get inline(){return this.hasAttribute("inline")}set inline(s){s?this.setAttribute("inline","true"):this.removeAttribute("inline")}get observer(){return this.hasAttribute("observer")}set observer(s){s?this.setAttribute("observer","true"):this.removeAttribute("observer")}restartAnimation(){let s=this._state;if(s.rendered){let a=this._shadowRoot;if(s.renderedMode==="svg")try{a.lastChild.setCurrentTime(0);return}catch{}kt(a,s)}}get status(){let s=this._state;return s.rendered?"rendered":s.icon.data===null?"failed":"loading"}_queueCheck(){this._checkQueued||(this._checkQueued=!0,setTimeout(()=>{this._check()}))}_check(){if(!this._checkQueued)return;this._checkQueued=!1;let s=this._state,a=this.getAttribute("icon");if(a!==s.icon.value){this._iconChanged(a);return}if(!s.rendered||!this._visible)return;let l=this.getAttribute("mode"),u=ht(this);(s.attrMode!==l||me(s.customisations,u)||!nt(this._shadowRoot))&&this._renderIcon(s.icon,u,l)}_iconChanged(s){let a=De(s,(l,u,f)=>{let h=this._state;if(h.rendered||this.getAttribute("icon")!==l)return;let g={value:l,name:u,data:f};g.data?this._gotIconData(g):h.icon=g});a.data?this._gotIconData(a):this._state=_t(a,this._state.inline,this._state)}_forceRender(){if(!this._visible){let s=nt(this._shadowRoot);s&&this._shadowRoot.removeChild(s);return}this._queueCheck()}_gotIconData(s){this._checkQueued=!1,this._renderIcon(s,ht(this),this.getAttribute("mode"))}_renderIcon(s,a,l){let u=Be(s.data.body,l),f=this._state.inline;kt(this._shadowRoot,this._state={rendered:!0,icon:s,inline:f,customisations:a,attrMode:l,renderedMode:u})}startObserver(){if(!this._observer&&!this.hasAttribute("noobserver"))try{this._observer=new IntersectionObserver(s=>{let a=s.some(l=>l.isIntersecting);a!==this._visible&&(this._visible=a,this._forceRender())}),this._observer.observe(this)}catch{if(this._observer){try{this._observer.disconnect()}catch{}this._observer=null}}}stopObserver(){this._observer&&(this._observer.disconnect(),this._observer=null,this._visible=!0,this._connected&&this._forceRender())}};o.forEach(s=>{s in r.prototype||Object.defineProperty(r.prototype,s,{get:function(){return this.getAttribute(s)},set:function(a){a!==null?this.setAttribute(s,a):this.removeAttribute(s)}})});let c=Vt();for(let s in c)r[s]=r.prototype[s]=c[s];return e.define(t,r),r}var hn=fn()||Vt(),{iconLoaded:Nn,getIcon:Pn,listIcons:Mn,addIcon:qn,addCollection:Rn,calculateSize:Fn,buildIcon:Dn,iconToHTML:Bn,svgToURL:Hn,loadIcons:$n,loadIcon:Qn,setCustomIconLoader:Vn,setCustomIconsLoader:Un,addAPIProvider:zn,_api:Gn}=hn;function pn(t){let e=t.getAttribute("aria-controls"),n=document.getElementById(e);n&&(n.addEventListener("click",i=>{i.stopPropagation()}),t.addEventListener("click",i=>{i.stopPropagation();let o=Gt(),r=o.indexOf(e);n.getAttribute("aria-hidden")==="false"?(o.splice(r,1),document.body.setAttribute("data-expanded",o.join(" ")),n.setAttribute("aria-hidden","true"),st(e,"false")):(o.push(e),document.body.setAttribute("data-expanded",o.join(" ")),n.setAttribute("aria-hidden","false"),st(e,"true"))}))}function st(t,e){let n=document.querySelectorAll('[aria-controls="'+t+'"]');for(let i=0;i{let t=Gt();document.body.setAttribute("data-expanded",""),t.forEach(e=>{document.getElementById(e).setAttribute("aria-hidden","true"),st(e,"false")})});var Jt=new Map;function gn(t){t.addEventListener("click",()=>{let e=t.querySelector("i");e.setAttribute("data-icon","loader");let n=t.getAttribute("data-url");n?mn(e,n):bn(e)})}function mn(t,e){let n=Jt.get(e);if(n){navigator.clipboard.writeText(n),ct(t);return}navigator.clipboard.write([new ClipboardItem({"text/plain":fetch(e).then(i=>i.text()).then(i=>(Jt.set(e,i),ct(t),i))})]).catch(()=>{t.setAttribute("data-icon","copy")})}function bn(t){import("https://esm.sh/turndown").then(e=>{let n=new e.default({headingStyle:"atx",bulletListMarker:"-",codeBlockStyle:"fenced"});return n.addRule("highlight",{filter:i=>i.nodeName==="DIV"&&i.classList.contains("highlight"),replacement:function(i,o){let r=Yt(o.parentNode.className),c="```";return r&&(c+=r),c+` +`+o.textContent.trim()+"\n```"}}),n.addRule("literal-block-wrapper",{filter:i=>i.nodeName==="DIV"&&i.classList.contains("literal-block-wrapper")&&i.querySelector(".highlight"),replacement:function(i,o){let r=o.querySelector(".highlight"),c=Yt(r.parentNode.className),s="```";if(c){let a=o.querySelector(".caption-text");s+=c,a&&(s+=' "'+a.textContent.trim()+'"')}return s+` +`+r.textContent+"\n```"}}),n}).then(e=>{let n=document.querySelector(".yue").cloneNode(!0),i=e.turndown(yn(n));return navigator.clipboard.writeText(i)}).then(()=>{ct(t)})}function ct(t){t.setAttribute("data-icon","check"),setTimeout(()=>{t.setAttribute("data-icon","copy")},500)}function yn(t){return t.querySelectorAll(".headerlink").forEach(e=>{e.remove()}),t.querySelectorAll(".copybtn").forEach(e=>{e.remove()}),t.querySelectorAll("span.linenos").forEach(e=>{e.remove()}),t.innerHTML}function Yt(t){let e=t.match(/highlight-(\S+)/);if(e)return e[1]}var Wt=document.querySelectorAll(".js-copy");for(let t=0;t{$.parentNode.removeChild($),document.head.removeChild(t)}),e(),window.addEventListener("resize",e)}var wn;var Q=["auto","light","dark"],V=document.querySelector(".js-theme");function xn(){let t=Xt();t+=1,Q[t]||(t=0);let e=Q[t];setColorMode(e),localStorage._theme=e,Zt(e)}function Kt(){return document.documentElement.getAttribute("data-color-mode")||"auto"}function Xt(){return Q.indexOf(Kt())}function Zt(t){let e=V.getAttribute("data-aria-"+t);V.setAttribute("aria-label",e)}V&&(V.addEventListener("click",xn),Zt(Q[Xt()]||"auto"));window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",e=>{Kt()==="auto"&&setColorMode("auto")});function In(){let t=document.querySelector(".globaltoc");if(!t)return;let e=parseInt(t.getAttribute("data-expand-depth"),10),n=o=>{if(!e)return!1;let r=0;for(;o.parentNode&&o.parentNode!==t;)o=o.parentNode,o.nodeName==="UL"&&(r+=1);return e>=r};t.querySelectorAll("li > ul").forEach(o=>{let r=o.parentNode;r.classList.contains("current")||n(r)?r.classList.add("_expand"):r.classList.add("_collapse");let c=An(o);r.appendChild(c)})}function An(t){let e=document.createElement("button");e.innerHTML='';let n=t.parentNode,i=t.previousSibling,o=i.textContent,r=()=>{n.classList.contains("_expand")?e.setAttribute("aria-label","Collapse "+o):e.setAttribute("aria-label","Expand "+o)};r();let c=s=>{s.preventDefault(),n.classList.contains("_expand")?(n.classList.remove("_expand"),n.classList.add("_collapse")):(n.classList.remove("_collapse"),n.classList.add("_expand")),r()};return i.getAttribute("href")==="#"&&i.addEventListener("click",c),e.addEventListener("click",c),e}var at=document.querySelector(".globaltoc a.current");at&&at.scrollIntoViewIfNeeded&&at.scrollIntoViewIfNeeded();In();var te=0,lt=200,S=[],C=document.querySelector(".back-to-top");function ee(){let t=document.querySelector(".yue > section");if(t){let e=window.getComputedStyle(t);lt=parseInt(e.scrollMarginTop,10)||0}}function Sn(t){let e;return t.nodeName==="DT"?e=t.parentNode.getBoundingClientRect():e=t.getBoundingClientRect(),e.top<=lt&&e.bottom>=lt}function Cn(){document.querySelectorAll(".localtoc li.active").forEach(t=>{t.classList.remove("active")})}function ne(t){let e=document.querySelector(`.localtoc a[href="#${t}"]`);if(!e)return;let n=e.parentNode;n.classList.contains("active")||(Cn(),n.classList.add("active"),n.scrollIntoViewIfNeeded&&n.scrollIntoViewIfNeeded(!1))}function oe(){let t;for(let e=0;e=t){let e=S[S.length-1];e&&ne(e.id)}else oe();C&&(window.scrollY&&window.scrollY{window.scrollTo(0,0)});document.querySelector(".localtoc")&&(window.addEventListener("scroll",kn),window.addEventListener("DOMContentLoaded",()=>{S=[...document.querySelectorAll(".yue > section section[id]"),...document.querySelectorAll(".yue dt.sig[id]")],ee(),oe()}),window.addEventListener("resize",ee));var U=document.querySelector(".js-repo-stats");async function _n(t,e){let n=`https://api.github.com/repos/${t}/${e}`,o=await(await fetch(n)).json(),r={stars:o.watchers,forks:o.forks};ut(r),sessionStorage.setItem("_sy/repo/stats",JSON.stringify(r))}async function Ln(t,e){let n="https://gitlab.com/api/v4/projects/"+encodeURIComponent(t+"/"+e),o=await(await fetch(n)).json(),r={stars:o.star_count,forks:o.forks_count};ut(r),sessionStorage.setItem("_sy/repo/stats",JSON.stringify(r))}function ut({stars:t,forks:e}){t&&(document.querySelector(".js-repo-stars").textContent=t),e&&(document.querySelector(".js-repo-forks").textContent=e)}function En(){let t=sessionStorage.getItem("_sy/repo/stats");if(t)ut(JSON.parse(t));else{let e=U.getAttribute("data-user"),n=U.getAttribute("data-repo"),i=U.getAttribute("data-type");i==="github"?_n(e,n):i==="gitlab"&&Ln(e,n)}}U&&En();function Tn(t,e){let n=document.createElement("script");n.id="_carbonads_js",n.src=`//cdn.carbonads.com/carbon.js?serve=${t}&placement=${e}`;let i=document.querySelector(".yue > section"),o=document.querySelector(".yue > section > section");if(o)i.insertBefore(n,o);else{let r=document.querySelector(".yue > section > p");r?i.insertBefore(n,r.nextSibling):i.appendChild(n)}}var dt=document.querySelector(".js-carbon");if(dt){let t=dt.getAttribute("data-carbon-code"),e=dt.getAttribute("data-carbon-placement");t&&e&&Tn(t,e)}var On=` +:host > div .results .hit h2 { + color: var(--sy-c-heading); + margin-bottom: 0; + border-bottom: 0; + font-weight: 600; +} +:host > div .results .hit .hit-block .content { + color: var(--sy-c-text); +} +:host > div .results .hit-block a.hit:hover, :host > div .results .hit-block .hit.active { + background-color: var(--gray-5); + border-radius: 4px; +} + +:host > div div.hit-block a.hit-block-heading:hover { + text-decoration: underline; +} + +:host > div div.hit-block a.hit-block-heading i, +:host > div div.hit-block .hit-block-heading-container .close-icon { + color: var(--sy-c-light); + margin-bottom: 0; + display: flex; +} +`;document.addEventListener("readthedocs-addons-data-ready",function(t){document.querySelector(".searchbox input").addEventListener("focusin",()=>{let e=new CustomEvent("readthedocs-search-show");document.dispatchEvent(e)}),setTimeout(()=>{let e=document.querySelector("readthedocs-search");if(e){let n=document.createElement("style");n.textContent=On,e.shadowRoot.appendChild(n)}},1e3)});/windows/i.test(navigator.userAgent)&&document.body.classList.add("win");})(); +/*! Bundled license information: + +iconify-icon/dist/iconify-icon.mjs: + (** + * (c) Iconify + * + * For the full copyright and license information, please view the license.txt + * files at https://github.com/iconify/iconify + * + * Licensed under MIT. + * + * @license MIT + * @version 3.0.2 + *) +*/ diff --git a/tools/docs/_build/dirhtml/_static/sphinx_highlight.js b/tools/docs/_build/dirhtml/_static/sphinx_highlight.js new file mode 100644 index 0000000..8a96c69 --- /dev/null +++ b/tools/docs/_build/dirhtml/_static/sphinx_highlight.js @@ -0,0 +1,154 @@ +/* Highlighting utilities for Sphinx HTML documentation. */ +"use strict"; + +const SPHINX_HIGHLIGHT_ENABLED = true + +/** + * highlight a given string on a node by wrapping it in + * span elements with the given class name. + */ +const _highlight = (node, addItems, text, className) => { + if (node.nodeType === Node.TEXT_NODE) { + const val = node.nodeValue; + const parent = node.parentNode; + const pos = val.toLowerCase().indexOf(text); + if ( + pos >= 0 && + !parent.classList.contains(className) && + !parent.classList.contains("nohighlight") + ) { + let span; + + const closestNode = parent.closest("body, svg, foreignObject"); + const isInSVG = closestNode && closestNode.matches("svg"); + if (isInSVG) { + span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + } else { + span = document.createElement("span"); + span.classList.add(className); + } + + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + const rest = document.createTextNode(val.substr(pos + text.length)); + parent.insertBefore( + span, + parent.insertBefore( + rest, + node.nextSibling + ) + ); + node.nodeValue = val.substr(0, pos); + /* There may be more occurrences of search term in this node. So call this + * function recursively on the remaining fragment. + */ + _highlight(rest, addItems, text, className); + + if (isInSVG) { + const rect = document.createElementNS( + "http://www.w3.org/2000/svg", + "rect" + ); + const bbox = parent.getBBox(); + rect.x.baseVal.value = bbox.x; + rect.y.baseVal.value = bbox.y; + rect.width.baseVal.value = bbox.width; + rect.height.baseVal.value = bbox.height; + rect.setAttribute("class", className); + addItems.push({ parent: parent, target: rect }); + } + } + } else if (node.matches && !node.matches("button, select, textarea")) { + node.childNodes.forEach((el) => _highlight(el, addItems, text, className)); + } +}; +const _highlightText = (thisNode, text, className) => { + let addItems = []; + _highlight(thisNode, addItems, text, className); + addItems.forEach((obj) => + obj.parent.insertAdjacentElement("beforebegin", obj.target) + ); +}; + +/** + * Small JavaScript module for the documentation. + */ +const SphinxHighlight = { + + /** + * highlight the search words provided in localstorage in the text + */ + highlightSearchWords: () => { + if (!SPHINX_HIGHLIGHT_ENABLED) return; // bail if no highlight + + // get and clear terms from localstorage + const url = new URL(window.location); + const highlight = + localStorage.getItem("sphinx_highlight_terms") + || url.searchParams.get("highlight") + || ""; + localStorage.removeItem("sphinx_highlight_terms") + url.searchParams.delete("highlight"); + window.history.replaceState({}, "", url); + + // get individual terms from highlight string + const terms = highlight.toLowerCase().split(/\s+/).filter(x => x); + if (terms.length === 0) return; // nothing to do + + // There should never be more than one element matching "div.body" + const divBody = document.querySelectorAll("div.body"); + const body = divBody.length ? divBody[0] : document.querySelector("body"); + window.setTimeout(() => { + terms.forEach((term) => _highlightText(body, term, "highlighted")); + }, 10); + + const searchBox = document.getElementById("searchbox"); + if (searchBox === null) return; + searchBox.appendChild( + document + .createRange() + .createContextualFragment( + '" + ) + ); + }, + + /** + * helper function to hide the search marks again + */ + hideSearchWords: () => { + document + .querySelectorAll("#searchbox .highlight-link") + .forEach((el) => el.remove()); + document + .querySelectorAll("span.highlighted") + .forEach((el) => el.classList.remove("highlighted")); + localStorage.removeItem("sphinx_highlight_terms") + }, + + initEscapeListener: () => { + // only install a listener if it is really needed + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return; + if (DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS && (event.key === "Escape")) { + SphinxHighlight.hideSearchWords(); + event.preventDefault(); + } + }); + }, +}; + +_ready(() => { + /* Do not call highlightSearchWords() when we are on the search page. + * It will highlight words from the *previous* search query. + */ + if (typeof Search === "undefined") SphinxHighlight.highlightSearchWords(); + SphinxHighlight.initEscapeListener(); +}); diff --git a/tools/docs/_build/dirhtml/_static/tfmri_icon.svg b/tools/docs/_build/dirhtml/_static/tfmri_icon.svg new file mode 100644 index 0000000..1206fd3 --- /dev/null +++ b/tools/docs/_build/dirhtml/_static/tfmri_icon.svg @@ -0,0 +1,248 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/docs/_build/dirhtml/_static/tfmri_icon_128px.png b/tools/docs/_build/dirhtml/_static/tfmri_icon_128px.png new file mode 100644 index 0000000..49145d3 Binary files /dev/null and b/tools/docs/_build/dirhtml/_static/tfmri_icon_128px.png differ diff --git a/tools/docs/_build/dirhtml/_static/tfmri_icon_192px.png b/tools/docs/_build/dirhtml/_static/tfmri_icon_192px.png new file mode 100644 index 0000000..cf524b0 Binary files /dev/null and b/tools/docs/_build/dirhtml/_static/tfmri_icon_192px.png differ diff --git a/tools/docs/_build/dirhtml/_static/tfmri_icon_24px.png b/tools/docs/_build/dirhtml/_static/tfmri_icon_24px.png new file mode 100644 index 0000000..113b0f1 Binary files /dev/null and b/tools/docs/_build/dirhtml/_static/tfmri_icon_24px.png differ diff --git a/tools/docs/_build/dirhtml/_static/tfmri_icon_256px.png b/tools/docs/_build/dirhtml/_static/tfmri_icon_256px.png new file mode 100644 index 0000000..de3254e Binary files /dev/null and b/tools/docs/_build/dirhtml/_static/tfmri_icon_256px.png differ diff --git a/tools/docs/_build/dirhtml/_static/tfmri_icon_32px.png b/tools/docs/_build/dirhtml/_static/tfmri_icon_32px.png new file mode 100644 index 0000000..74285e0 Binary files /dev/null and b/tools/docs/_build/dirhtml/_static/tfmri_icon_32px.png differ diff --git a/tools/docs/_build/dirhtml/_static/tfmri_icon_384px.png b/tools/docs/_build/dirhtml/_static/tfmri_icon_384px.png new file mode 100644 index 0000000..5176005 Binary files /dev/null and b/tools/docs/_build/dirhtml/_static/tfmri_icon_384px.png differ diff --git a/tools/docs/_build/dirhtml/_static/tfmri_icon_48px.png b/tools/docs/_build/dirhtml/_static/tfmri_icon_48px.png new file mode 100644 index 0000000..86e9ca6 Binary files /dev/null and b/tools/docs/_build/dirhtml/_static/tfmri_icon_48px.png differ diff --git a/tools/docs/_build/dirhtml/_static/tfmri_icon_96px.png b/tools/docs/_build/dirhtml/_static/tfmri_icon_96px.png new file mode 100644 index 0000000..3bea671 Binary files /dev/null and b/tools/docs/_build/dirhtml/_static/tfmri_icon_96px.png differ diff --git a/tools/docs/_build/dirhtml/_static/tfmri_logo.svg b/tools/docs/_build/dirhtml/_static/tfmri_logo.svg new file mode 100644 index 0000000..8eba502 --- /dev/null +++ b/tools/docs/_build/dirhtml/_static/tfmri_logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/_static/tfmri_logo_dark.svg b/tools/docs/_build/dirhtml/_static/tfmri_logo_dark.svg new file mode 100644 index 0000000..39b5e7d --- /dev/null +++ b/tools/docs/_build/dirhtml/_static/tfmri_logo_dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/_static/thumb.png b/tools/docs/_build/dirhtml/_static/thumb.png new file mode 100644 index 0000000..e19d466 Binary files /dev/null and b/tools/docs/_build/dirhtml/_static/thumb.png differ diff --git a/tools/docs/_build/dirhtml/api_docs/im2sim/configs/HalfUNetConfig/index.html b/tools/docs/_build/dirhtml/api_docs/im2sim/configs/HalfUNetConfig/index.html new file mode 100644 index 0000000..8c325e2 --- /dev/null +++ b/tools/docs/_build/dirhtml/api_docs/im2sim/configs/HalfUNetConfig/index.html @@ -0,0 +1,293 @@ + + + + + im2sim.configs.HalfUNetConfig - IM2SIM Documentation + + + + + + + + + + + + + + + +
+
+
+ + + + im2sim + +
+ +
+
+
+
+
+
+
+ + + + +
+
+
+
+
+ + +
+ +
+
+

im2sim.configs.HalfUNetConfig#

+
+
+class HalfUNetConfig(hidden_channels=64, num_downsamples=4, pool_spec=<factory>, upsample_spec=<factory>, block_cfg=<factory>, blocks_per_level=2, out_activation=None, stem_block_cfg=None, encoder_block_cfg=None, out_block_cfg=None, fusion_type=ResidualConnectionType.ADD)[source]#
+

Bases: Config

+

Configuration class for defining the parameters of a half U-Net architecture (see im2sim.models.HalfUNet).

+

Attributes can either be set directly when creating an instance of the class or modified later.

+

Configuration presets can be applied to quickly set up common configurations for different use cases.

+

The configuration can also be saved to and loaded from a YAML file.

+
+
Parameters:
+
    +
  • hidden_channels (int) – Number of channels in the hidden layers. Default is 64.

  • +
  • num_downsamples (int) – Number of downsampling operations in the encoder. Default is 4.

  • +
  • pool_spec (LayerConfig | list[LayerConfig]) – Specification for the pooling layers. Default is a MaxPool layer with kernel size 2 for all levels.

  • +
  • upsample_spec (LayerConfig | list[LayerConfig]) – Specification for the upsampling layers. Default is an Upsample layer with scale factor 2 and mode ‘trilinear’ for all levels. +The mode is automatically changed to ‘bilinear’ for 2D data and ‘nearest’ for 1D.

  • +
  • block_cfg (ImageConvBlockConfig) – Configuration for the convolutional blocks. Default is a standard convolutional block with 2 layers, ReLU activation, and batch normalization.

  • +
  • blocks_per_level (int) – Number of convolutional blocks per level in the encoder. Default is 2.

  • +
  • out_activation (str | None) – Activation function for the output layer. Default is None, which means no activation is applied.

  • +
  • stem_block_cfg (ImageConvBlockConfig | None) – Configuration for the stem block. If None, it defaults to a single convolutional block with the same configuration as block_cfg.

  • +
  • encoder_block_cfg (list[ImageConvBlockConfig] | ImageConvBlockConfig | None) – Configuration for the encoder blocks. If None, it defaults to a list of block_cfg repeated for each downsampling level.

  • +
  • out_block_cfg (ImageConvBlockConfig | None) – Configuration for the output block. If None, it defaults to a single convolutional block with the same configuration as block_cfg and the specified out_activation.

  • +
  • fusion_type (ResidualConnectionType) – Type of residual connection to use in the network. It can be either ‘add’ (default), ‘concat’ or ‘average’. +This determines how the encoder features are fused.

  • +
+
+
+

Examples

+

To create a HalfUNet model for single class segmentation, you can do the following:

+
>>> cfg = HalfUNetConfig(num_downsamples=3, hidden_channels=64)
+>>> cfg = cfg.apply_presets(["single_class_segmentation", "residual", "SE", "ghost_depthwise_separable"])
+>>> model = HalfUNet.build(in_channles=20, out_channels=1, rank=3, cfg=cfg)
+
+
+

For more presets, see the Preset Library below.

+

Preset Library

+

residual

+

Apply a residual connection to all encoder blocks in the HalfUNet configuration.

+

The residual connection type is set to “add” for all encoder blocks, +which means that the output of each encoder block will be added to its input before being passed to the next layer. +This can help with gradient flow and improve training stability.

+

dilated_bottleneck

+

Apply dilated convolutions to the bottleneck (lowest resolution) block in the HalfUNet configuration.

+

This change modifies the last encoder block to use dilated convolutions, which can help increase the receptive field without increasing the number of parameters.

+

recon

+

Apply a reconstruction preset to all blocks in the HalfUNet configuration.

+

This preset is typically used for image reconstruction or superresolution tasks, +where the output is expected to be a continuous value (e.g., pixel intensity).

+

single_class_segmentation

+

Apply a single-class segmentation preset to all blocks in the HalfUNet configuration.

+

Use this preset for binary segmentation tasks, where the output is expected to be a probability map for a single class.

+

multiclass_segmentation

+

Apply a multi-class segmentation preset to all blocks in the HalfUNet configuration.

+

Use this preset for multi-class segmentation tasks, where the output is expected to be a probability map for multiple classes.

+

depthwise_separable

+

Apply a depthwise separable convolution (see im2sim.layers.DepthwiseSeparableConv) preset to all encoder blocks in the HalfUNet configuration.

+

ghost_depthwise

+

Apply a ghost depthwise convolution (see im2sim.layers.GhostConv) preset to all encoder blocks in the HalfUNet configuration.

+

ghost_depthwise_separable

+

Apply a ghost depthwise separable convolution (see im2sim.layers.GhostConv) preset to all encoder blocks in the HalfUNet configuration.

+

ECA

+

Apply an Efficient Channel Attention (ECA) (see im2sim.layers.EfficientChannelAttn) preset to all encoder blocks in the HalfUNet configuration.

+

SE

+

Apply a Squeeze-and-Excitation (SE) (see im2sim.layers.SqueezeExcite) preset to all encoder blocks in the HalfUNet configuration.

+
+ +
+ +
+
+
+
+
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/api_docs/im2sim/configs/ImageConvBlockConfig/index.html b/tools/docs/_build/dirhtml/api_docs/im2sim/configs/ImageConvBlockConfig/index.html new file mode 100644 index 0000000..a4462ab --- /dev/null +++ b/tools/docs/_build/dirhtml/api_docs/im2sim/configs/ImageConvBlockConfig/index.html @@ -0,0 +1,296 @@ + + + + + im2sim.configs.ImageConvBlockConfig - IM2SIM Documentation + + + + + + + + + + + + + + + +
+
+
+ + + + im2sim + +
+ +
+
+
+
+
+
+
+ + + + +
+
+
+
+
+ + +
+ +
+
+

im2sim.configs.ImageConvBlockConfig#

+
+
+class ImageConvBlockConfig(depth=2, activation='ReLU', out_activation=None, conv_config=<factory>, norm_config=<factory>, dropout_config=<factory>, attn_config=<factory>, dropout_position=1, residual_connections=None, residual_type=ResidualConnectionType.ADD)[source]#
+

Bases: Config

+

Configuration class for defining the parameters of an image convolutional block.

+

Attributes can either be set directly when creating an instance of the class or modified later.

+

Configuration presets can be applied to quickly set up common configurations for different use cases.

+

The configuration can also be saved to and loaded from a YAML file.

+
+
Parameters:
+
    +
  • depth (int) – The number of convolutional layers in the block. Default is 2.

  • +
  • activation (str | None) – The activation function to use after each convolutional layer. Default is “ReLU”.

  • +
  • out_activation (str | None) – The activation function to use after the final layer. Default is None.

  • +
  • conv_config (LayerConfig) – Configuration for the convolutional layers, including kernel size and padding.

  • +
  • norm_config (LayerConfig) – Configuration for the normalization layers, such as InstanceNorm with affine set to True.

  • +
  • dropout_config (LayerConfig) – Configuration for the dropout layers. Default is no dropout.

  • +
  • attn_config (LayerConfig) – Configuration for the attention layers. Default is no attention.

  • +
  • dropout_position (int | list[int]) – Specifies the position(s) of the dropout layers within the block. Default is 1.

  • +
  • residual_connections (dict [int, list[ int ]] | None) – Specifies the residual connections within the block. +The keys represent the target layers, and the values are lists of source layers. Default is None. +Example: {1: [0]} means that the input to the block will be added to the output of layer 1.

  • +
  • residual_type (str) – The type of residual connection to use (e.g., “add”). Default is ResidualConnectionType.ADD.

  • +
+
+
+

Examples

+

To create a configuration for an image convolutional block with a depth of 3, ReLU activation, and softmax output activation, you can use the following code:

+
>>> cfg = ImageConvBlockConfig(depth=3, activation="ReLU", out_activation="softmax")
+
+
+

To apply a preset configuration for a single convolutional layer without normalization or dropout, you can use:

+
>>> cfg = ImageConvBlockConfig.apply_presets(cfg, ["single_conv"])
+
+
+

To save the configuration to a YAML file and load it back, you can use:

+
>>> cfg.save("config.yaml")
+>>> loaded_cfg = ImageConvBlockConfig().load("config.yaml")
+
+
+

Preset Library

+

single_conv

+

Converts the blcok into a single convolutional layer with no normalization, dropout, or residual connections.

+

single_block

+

Sets the block depth to 1 and removes dropout and residual connections, but keeps normalization and activation.

+

0_residual

+

Configures the block to have a residual connection from the input to the output of the last layer.

+

1_residual

+

Configures the block to have a residual connection from the output of the first layer to the output of the last layer.

+

concat_residual

+

Configures the block to have a residual connection from the input to the output of the last layer, using concatenation instead of addition.

+

recon

+

Configures the block for reconstruction tasks by removing normalization and dropout layers.

+

segmentation

+

Configures the block for segmentation tasks by using InstanceNorm with trainable parameters.

+

depthwise_separable

+

Configures the block to use depthwise separable convolutions (see im2sim.layers.DepthwiseSeparableConv) instead of standard convolutions.

+

ghost_depthwise

+

Configures the block to use Ghost convolutions instead of standard convolutions.

+

ghost_depthwise_separable

+

Configures the block to use Ghost depthwise separable convolutions instead of standard convolutions.

+

dilated_convs

+

Configures the block to use dilated convolutions with dilation of 2 instead of standard convolutions.

+

ECA

+

Configures the block to use Efficient Channel Attention (ECA)

+

SE

+

Configures the block to use Squeeze-and-Excitation (SE) attention

+
+ +
+ +
+
+
+
+
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/api_docs/im2sim/configs/index.html b/tools/docs/_build/dirhtml/api_docs/im2sim/configs/index.html new file mode 100644 index 0000000..f00f574 --- /dev/null +++ b/tools/docs/_build/dirhtml/api_docs/im2sim/configs/index.html @@ -0,0 +1,249 @@ + + + + + im2sim.configs - IM2SIM Documentation + + + + + + + + + + + + + + + +
+
+
+ + + + im2sim + +
+ +
+
+
+
+
+
+
+ + + + +
+
+
+
+
+ + +
+ +
+
+

im2sim.configs#

+

Configuration classes for model and training settings.

+
+

Classes#

+
+ + + + + + + + + +

HalfUNetConfig

Configuration class for defining the parameters of a half U-Net architecture (see im2sim.models.HalfUNet).

ImageConvBlockConfig

Configuration class for defining the parameters of an image convolutional block.

+
+
+
+

Functions#

+
+ + + +
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/api_docs/im2sim/data/index.html b/tools/docs/_build/dirhtml/api_docs/im2sim/data/index.html new file mode 100644 index 0000000..542dd18 --- /dev/null +++ b/tools/docs/_build/dirhtml/api_docs/im2sim/data/index.html @@ -0,0 +1,243 @@ + + + + + im2sim.data - IM2SIM Documentation + + + + + + + + + + + + + + + +
+
+
+ + + + im2sim + +
+ +
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/api_docs/im2sim/index.html b/tools/docs/_build/dirhtml/api_docs/im2sim/index.html new file mode 100644 index 0000000..f81b579 --- /dev/null +++ b/tools/docs/_build/dirhtml/api_docs/im2sim/index.html @@ -0,0 +1,270 @@ + + + + + im2sim - IM2SIM Documentation + + + + + + + + + + + + + + + +
+
+
+ + + + im2sim + +
+ +
+
+
+
+
+
+
+ + + + +
+
+
+
+
+ + +
+ +
+
+

im2sim#

+

IM2SIM.

+
+

Modules#

+
+ + + + + + + + + + + + + + + + + + + + + + + + +

configs

Configuration classes for model and training settings.

data

Data loading and preprocessing utilities.

layers

Custom layers for building deep learning models.

losses

Custom loss functions for training deep learning models.

models

Predefined deep learning models for various tasks.

ops

Custom operations for deep learning models.

plot

Utilities for visualizing data and model outputs.

+
+
+
+

Classes#

+
+ + + +
+
+
+
+

Functions#

+
+ + + +
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/api_docs/im2sim/layers/ConditionedSqueezeExcite/index.html b/tools/docs/_build/dirhtml/api_docs/im2sim/layers/ConditionedSqueezeExcite/index.html new file mode 100644 index 0000000..ee4a65b --- /dev/null +++ b/tools/docs/_build/dirhtml/api_docs/im2sim/layers/ConditionedSqueezeExcite/index.html @@ -0,0 +1,279 @@ + + + + + im2sim.layers.ConditionedSqueezeExcite - IM2SIM Documentation + + + + + + + + + + + + + + + +
+
+
+ + + + im2sim + +
+ +
+
+
+
+
+
+
+ + + + +
+
+
+
+
+ + +
+ +
+
+

im2sim.layers.ConditionedSqueezeExcite#

+
+
+class ConditionedSqueezeExcite(channels, rank, n_cond=6, reduction=8, mode='add')[source]#
+

Bases: Module

+

Conditioned Squeeze-and-Excitation [1] (SE) layer that adaptively recalibrates channel-wise feature responses based on an additional conditioning input.

+

This operation is useful for improving the representational power of convolutional neural networks by explicitly modeling interdependencies between channels, +while also allowing for external conditioning information to influence the recalibration process.

+
+
Parameters:
+
    +
  • channels (int) – Number of input channels.

  • +
  • rank (int) – The rank of the input tensor (1 for 1D, 2 for 2D, 3 for 3D).

  • +
  • n_cond (int) – Number of conditioning channels. Default is 6.

  • +
  • reduction (int) – Reduction ratio for the hidden layer in the SE block. Default is 8.

  • +
  • mode (str) – Mode of combining the feature and conditioning information. Can be “concat” or “add”. Default is “add”.

  • +
+
+
+

References

+ +

Initialize internal Module state, shared by both nn.Module and ScriptModule.

+
+
+forward(x, cond)[source]#
+

Forward pass of the Conditioned Squeeze-and-Excitation layer.

+
+
Parameters:
+
    +
  • x (Tensor) – Input tensor of shape (batch_size, channels, *spatial_dims).

  • +
  • cond (Tensor) – Conditioning tensor of shape (batch_size, n_cond).

  • +
+
+
Returns:
+

Output tensor of shape (batch_size, channels, *spatial_dims) with channel-wise attention applied based on the conditioning input.

+
+
Return type:
+

Tensor

+
+
+
+ +
+ +
+ +
+
+
+
+
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/api_docs/im2sim/layers/DefaultGraphNorm/index.html b/tools/docs/_build/dirhtml/api_docs/im2sim/layers/DefaultGraphNorm/index.html new file mode 100644 index 0000000..8b622d8 --- /dev/null +++ b/tools/docs/_build/dirhtml/api_docs/im2sim/layers/DefaultGraphNorm/index.html @@ -0,0 +1,264 @@ + + + + + im2sim.layers.DefaultGraphNorm - IM2SIM Documentation + + + + + + + + + + + + + + + +
+
+
+ + + + im2sim + +
+ +
+
+
+
+
+
+
+ + + + +
+
+
+
+
+ + +
+ +
+
+

im2sim.layers.DefaultGraphNorm#

+
+
+class DefaultGraphNorm[source]#
+

Bases: Module

+

The default normalisation for im2sim graph blocks. +Uses torch.nn.InstanceNorm2d applied to graph data, but all channels are normalised together.

+
+
Parameters:
+

None

+
+
+

Initialize internal Module state, shared by both nn.Module and ScriptModule.

+
+
+forward(x, batch)[source]#
+
+
Parameters:
+
    +
  • x (torch.Tensor) – The input tensor of shape (N, C) where N is the number of nodes and C is the number of channels.

  • +
  • batch – The batch tensor of shape (N,) indicating the batch index for each node.

  • +
+
+
Returns:
+

The normalized tensor of shape (N, C).

+
+
Return type:
+

torch.Tensor

+

+
+
+
+ +
+ +
+ +
+
+
+
+
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/api_docs/im2sim/layers/DepthwiseConv/index.html b/tools/docs/_build/dirhtml/api_docs/im2sim/layers/DepthwiseConv/index.html new file mode 100644 index 0000000..ee9c7eb --- /dev/null +++ b/tools/docs/_build/dirhtml/api_docs/im2sim/layers/DepthwiseConv/index.html @@ -0,0 +1,278 @@ + + + + + im2sim.layers.DepthwiseConv - IM2SIM Documentation + + + + + + + + + + + + + + + +
+
+
+ + + + im2sim + +
+ +
+
+
+
+
+
+
+ + + + +
+
+
+
+
+ + +
+ +
+
+

im2sim.layers.DepthwiseConv#

+
+
+class DepthwiseConv(in_channels, out_channels, rank, kernel_size=3, stride=1, padding='same', dilation=1, bias=True)[source]#
+

Bases: Module

+

Depthwise convolution layer that applies a separate convolutional filter to each input channel.

+

This operation is useful for reducing the number of parameters and computational cost in convolutional neural networks, especially in mobile and embedded applications[1].

+
+
Parameters:
+
    +
  • in_channels (int) – Number of input channels.

  • +
  • out_channels (int) – Number of output channels (should be equal to in_channels for depthwise convolution).

  • +
  • rank (int) – The rank of the convolution (1 for 1D, 2 for 2D, 3 for 3D).

  • +
  • kernel_size (int | tuple) – Size of the convolving kernel. Default is 3.

  • +
  • stride (int | tuple) – Stride of the convolution. Default is 1.

  • +
  • padding (str or int | tuple) – Padding added to all four sides of the input. Default is “same”.

  • +
  • dilation (int | tuple) – Spacing between kernel elements. Default is 1.

  • +
  • bias (bool) – If True, adds a learnable bias to the output. Default is True.

  • +
+
+
+

References

+ +

Initialize internal Module state, shared by both nn.Module and ScriptModule.

+
+
+forward(x)[source]#
+

Forward pass of the depthwise convolution layer.

+
+
Parameters:
+

x (Tensor) – Input tensor of shape (batch_size, in_channels, *spatial_dims).

+
+
Returns:
+

Output tensor of shape (batch_size, out_channels, *spatial_dims).

+
+
Return type:
+

Tensor

+
+
+
+ +
+ +
+ +
+
+
+
+
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/api_docs/im2sim/layers/DepthwiseSeparableConv/index.html b/tools/docs/_build/dirhtml/api_docs/im2sim/layers/DepthwiseSeparableConv/index.html new file mode 100644 index 0000000..c04d9ca --- /dev/null +++ b/tools/docs/_build/dirhtml/api_docs/im2sim/layers/DepthwiseSeparableConv/index.html @@ -0,0 +1,280 @@ + + + + + im2sim.layers.DepthwiseSeparableConv - IM2SIM Documentation + + + + + + + + + + + + + + + +
+
+
+ + + + im2sim + +
+ +
+
+
+
+
+
+
+ + + + +
+
+
+
+
+ + +
+ +
+
+

im2sim.layers.DepthwiseSeparableConv#

+
+
+class DepthwiseSeparableConv(in_channels, out_channels, rank, kernel_size=3, stride=1, padding='same', dilation=1, bias=True, activation=None)[source]#
+

Bases: Module

+

Depthwise separable convolution layer that consists of a depthwise convolution followed by a pointwise convolution.

+

This operation is useful for reducing the number of parameters and computational cost in convolutional neural networks, +while retaining more representational power compared to standard depthwise convolution[1].

+
+
Parameters:
+
    +
  • in_channels (int) – Number of input channels.

  • +
  • out_channels (int) – Number of output channels.

  • +
  • rank (int) – The rank of the convolution (1 for 1D, 2 for 2D, 3 for 3D).

  • +
  • kernel_size (int | tuple) – Size of the convolving kernel. Default is 3.

  • +
  • stride (int | tuple) – Stride of the convolution. Default is 1.

  • +
  • padding (str or int | tuple) – Padding added to all four sides of the input. Default is “same”.

  • +
  • dilation (int | tuple) – Spacing between kernel elements. Default is 1.

  • +
  • bias (bool) – If True, adds a learnable bias to the output. Default is True.

  • +
  • activation (str or None) – Activation function to apply after the pointwise convolution. Default is None.

  • +
+
+
+

References

+ +

Initialize internal Module state, shared by both nn.Module and ScriptModule.

+
+
+forward(x)[source]#
+

Forward pass of the depthwise separable convolution layer.

+
+
Parameters:
+

x (Tensor) – Input tensor of shape (batch_size, in_channels, *spatial_dims).

+
+
Returns:
+

Output tensor of shape (batch_size, out_channels, *spatial_dims).

+
+
Return type:
+

Tensor

+
+
+
+ +
+ +
+ +
+
+
+
+
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/api_docs/im2sim/layers/EfficientChannelAttn/index.html b/tools/docs/_build/dirhtml/api_docs/im2sim/layers/EfficientChannelAttn/index.html new file mode 100644 index 0000000..cb7567b --- /dev/null +++ b/tools/docs/_build/dirhtml/api_docs/im2sim/layers/EfficientChannelAttn/index.html @@ -0,0 +1,272 @@ + + + + + im2sim.layers.EfficientChannelAttn - IM2SIM Documentation + + + + + + + + + + + + + + + +
+
+
+ + + + im2sim + +
+ +
+
+
+
+
+
+
+ + + + +
+
+
+
+
+ + +
+ +
+
+

im2sim.layers.EfficientChannelAttn#

+
+
+class EfficientChannelAttn(channels, rank)[source]#
+

Bases: Module

+

Efficient Channel Attention (ECA) layer that adaptively selects important channels based on global context.

+

This operation is useful for improving the representational power of convolutional neural networks by focusing on the most informative channels[1].

+
+
Parameters:
+
    +
  • channels (int) – Number of input channels.

  • +
  • rank (int) – The rank of the input tensor (1 for 1D, 2 for 2D, 3 for 3D).

  • +
+
+
+

References

+ +

Initialize internal Module state, shared by both nn.Module and ScriptModule.

+
+
+forward(x)[source]#
+

Forward pass of the Efficient Channel Attention layer.

+
+
Parameters:
+

x (Tensor) – Input tensor of shape (batch_size, channels, *spatial_dims).

+
+
Returns:
+

Output tensor of shape (batch_size, channels, *spatial_dims) with channel-wise attention applied.

+
+
Return type:
+

Tensor

+
+
+
+ +
+ +
+ +
+
+
+
+
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/api_docs/im2sim/layers/GhostConv/index.html b/tools/docs/_build/dirhtml/api_docs/im2sim/layers/GhostConv/index.html new file mode 100644 index 0000000..a8f9e59 --- /dev/null +++ b/tools/docs/_build/dirhtml/api_docs/im2sim/layers/GhostConv/index.html @@ -0,0 +1,286 @@ + + + + + im2sim.layers.GhostConv - IM2SIM Documentation + + + + + + + + + + + + + + + +
+
+
+ + + + im2sim + +
+ +
+
+
+
+
+
+
+ + + + +
+
+
+
+
+ + +
+ +
+
+

im2sim.layers.GhostConv#

+
+
+class GhostConv(in_channels, out_channels, rank, kernel_size=3, ratio=2, dw_kernel_size=3, stride=1, padding='same', separable=False, bias=True)[source]#
+

Bases: Module

+

Ghost convolution layer that generates more feature maps from cheap operations.

+

This operation is useful for reducing the number of parameters and computational cost in convolutional neural networks. +The cheap operation can either be a depthwise convolution as per the original GhostNet paper[1] or a depthwise separable convolution as in the HalfUNet paper[2

+
+
Parameters:
+
    +
  • in_channels (int) – Number of input channels.

  • +
  • out_channels (int) – Number of output channels.

  • +
  • rank (int) – The rank of the convolution (1 for 1D, 2 for 2D, 3 for 3D).

  • +
  • kernel_size (int | tuple) – Size of the convolving kernel for the primary convolution. Default is 3.

  • +
  • ratio (int) – Ratio of the number of output channels to the number of primary convolution channels. Default is 2.

  • +
  • dw_kernel_size (int | tuple) – Size of the convolving kernel for the cheap operation. Default is 3.

  • +
  • stride (int | tuple) – Stride of the primary convolution. Default is 1.

  • +
  • padding (str or int | tuple) – Padding added to all four sides of the input for the primary convolution. Default is “same”.

  • +
  • separable (bool) – If True, uses depthwise separable convolution for the cheap operation. Default is False.

  • +
  • bias (bool) – If True, adds a learnable bias to the output. Default is True.

  • +
+
+
+

References

+ +

Initialize internal Module state, shared by both nn.Module and ScriptModule.

+
+
+forward(x)[source]#
+

Forward pass of the Ghost convolution layer.

+
+
Parameters:
+

x (Tensor) – Input tensor of shape (batch_size, in_channels, *spatial_dims).

+
+
Returns:
+

Output tensor of shape (batch_size, out_channels, *spatial_dims).

+
+
Return type:
+

Tensor

+
+
+
+ +
+ +
+ +
+
+
+
+
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/api_docs/im2sim/layers/GraphConvBlock/index.html b/tools/docs/_build/dirhtml/api_docs/im2sim/layers/GraphConvBlock/index.html new file mode 100644 index 0000000..5d7f6ca --- /dev/null +++ b/tools/docs/_build/dirhtml/api_docs/im2sim/layers/GraphConvBlock/index.html @@ -0,0 +1,268 @@ + + + + + im2sim.layers.GraphConvBlock - IM2SIM Documentation + + + + + + + + + + + + + + + +
+
+
+ + + + im2sim + +
+ +
+
+
+
+
+
+
+ + + + +
+
+
+
+
+ + +
+ +
+
+

im2sim.layers.GraphConvBlock#

+
+
+class GraphConvBlock(in_channels, filters, depth=1, conv_type='GATConv', conv_kwargs=None, activation='ReLU', norm_type='defaultnorm', norm_kwargs=None)[source]#
+

Bases: Module

+

A convolutional block for graph data

+
+
Parameters:
+
    +
  • in_channels (int) – The number of channels in the input to the layer.

  • +
  • filters (int, optional) – The number of filters in each convolutional layer (default: 32)

  • +
  • depth (int, optional) – The number of successive convolutional layers (default: 2)

  • +
  • conv_type (str, optional) – The type of graph convolution to apply (default: “GATConv”, options: All PyG Convs)

  • +
  • conv_kwargs (dict, optional) – Dictionary of keyword arguments for the chosen conv_type

  • +
  • activation (str, optional) – The activation function applied after each convolution (default: “relu”, options: All torch activations)

  • +
  • norm_type (str, optional) – The normalization method to apply between convolutions (default:”defaultnorm”, options: All PyG Norms)

  • +
+
+
Returns:
+

A torch.nn.Module object.

+
+
+

Initialize internal Module state, shared by both nn.Module and ScriptModule.

+
+
+forward(in_graph)[source]#
+

Define the computation performed at every call.

+

Should be overridden by all subclasses.

+
+

Note

+

Although the recipe for forward pass needs to be defined within +this function, one should call the :class:Module instance afterwards +instead of this since the former takes care of running the +registered hooks while the latter silently ignores them.

+
+
+ +
+ +
+ +
+
+
+
+
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/api_docs/im2sim/layers/GraphConvResBlock/index.html b/tools/docs/_build/dirhtml/api_docs/im2sim/layers/GraphConvResBlock/index.html new file mode 100644 index 0000000..a8d5d32 --- /dev/null +++ b/tools/docs/_build/dirhtml/api_docs/im2sim/layers/GraphConvResBlock/index.html @@ -0,0 +1,268 @@ + + + + + im2sim.layers.GraphConvResBlock - IM2SIM Documentation + + + + + + + + + + + + + + + +
+
+
+ + + + im2sim + +
+ +
+
+
+
+
+
+
+ + + + +
+
+
+
+
+ + +
+ +
+
+

im2sim.layers.GraphConvResBlock#

+
+
+class GraphConvResBlock(in_channels, filters, depth=3, conv_type='GATConv', conv_kwargs=None, activation='ReLU', norm_type='defaultnorm', norm_kwargs=None)[source]#
+

Bases: Module

+

A convolutional block for graph data

+
+
Parameters:
+
    +
  • in_channels (int) – The number of channels in the input to the layer.

  • +
  • filters (int, optional) – The number of filters in each convolutional layer (default: 32)

  • +
  • depth (int, optional) – The number of successive convolutional layers (default: 2)

  • +
  • conv_type (str, optional) – The type of graph convolution to apply (default: “ChebConv”, options: All PyG Convs)

  • +
  • conv_kwargs (dict, optional) – Dictionary of keyword arguments for the chosen conv_type

  • +
  • activation (str, optional) – The activation function applied after each convolution (default: “relu”, options: All torch activations)

  • +
  • norm_type (str, optional) – The normalization method to apply between convolutions (default:”InstanceNorm”, options: All PyG Norms)

  • +
+
+
Returns:
+

A torch.nn.Module object.

+
+
+

Initialize internal Module state, shared by both nn.Module and ScriptModule.

+
+
+forward(in_graph)[source]#
+

Define the computation performed at every call.

+

Should be overridden by all subclasses.

+
+

Note

+

Although the recipe for forward pass needs to be defined within +this function, one should call the :class:Module instance afterwards +instead of this since the former takes care of running the +registered hooks while the latter silently ignores them.

+
+
+ +
+ +
+ +
+
+
+
+
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/api_docs/im2sim/layers/GraphResDecoderBlock/index.html b/tools/docs/_build/dirhtml/api_docs/im2sim/layers/GraphResDecoderBlock/index.html new file mode 100644 index 0000000..908f673 --- /dev/null +++ b/tools/docs/_build/dirhtml/api_docs/im2sim/layers/GraphResDecoderBlock/index.html @@ -0,0 +1,273 @@ + + + + + im2sim.layers.GraphResDecoderBlock - IM2SIM Documentation + + + + + + + + + + + + + + + +
+
+
+ + + + im2sim + +
+ +
+
+
+
+
+
+
+ + + + +
+
+
+
+
+ + +
+ +
+
+

im2sim.layers.GraphResDecoderBlock#

+
+
+class GraphResDecoderBlock(projection_channels, graph_channels, out_channels, filters, res_depth=3, n_deform_blocks=3, template_edge_index=None, conv_type='GATConv', conv_kwargs=None, activation='relu', out_activation='linear', norm_type='defaultnorm')[source]#
+

Bases: Module

+

A graph convolutional decoder block with the same structure as MeshDeformNet and Image2Flow

+
+
Parameters:
+
    +
  • encoder_channels (List[int]) – The number of channels projected from the encoder to each decoder level (len=n_decoder_levels)

  • +
  • out_channels (int) – The number of output channels including node coordinates and features

  • +
  • filters (List(List(int)), optional) – The number of convolutional filters for each level (default:[[384,288], [144,96], [64,32]])

  • +
  • res_block_depth (int, optional) – The number of successive convolutions in each residual block (default: 3)

  • +
  • n_process_blocks (int, optional) – The number of residual blocks prior to projection(default: 1)

  • +
  • n_deform_blocks (int, optional) – The number of residual blocks after projection(default: 3)

  • +
  • template_edge_index (Tensor, optional) – If template tensor is the fixed it can be passed (default: None)

  • +
  • conv_type (str, optional) – The type of graph convolution to apply (default: “ChebConv”, options: All PyG Convs)

  • +
  • conv_kwargs (dict, optional) – Dictionary of keyword arguments for the chosen conv_type

  • +
  • activation (str, optional) – The activation function applied after each convolution (default: “relu”, options: All torch activations)

  • +
  • out_activation (str, optional) – The activation function applied after each convolution (default: “linear”, options: All torch activations)

  • +
  • norm_type (str, optional) – The normalization method to apply between convolutions (default:”InstanceNorm”, options: All PyG Norms)

  • +
+
+
Returns:
+

A torch.nn.Module object.

+
+
+

Initialize internal Module state, shared by both nn.Module and ScriptModule.

+
+
+forward(in_graph, prev_results, encoder_projection)[source]#
+

Define the computation performed at every call.

+

Should be overridden by all subclasses.

+
+

Note

+

Although the recipe for forward pass needs to be defined within +this function, one should call the :class:Module instance afterwards +instead of this since the former takes care of running the +registered hooks while the latter silently ignores them.

+
+
+ +
+ +
+ +
+
+
+
+
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/api_docs/im2sim/layers/ImageConvBlock/index.html b/tools/docs/_build/dirhtml/api_docs/im2sim/layers/ImageConvBlock/index.html new file mode 100644 index 0000000..dbce75f --- /dev/null +++ b/tools/docs/_build/dirhtml/api_docs/im2sim/layers/ImageConvBlock/index.html @@ -0,0 +1,318 @@ + + + + + im2sim.layers.ImageConvBlock - IM2SIM Documentation + + + + + + + + + + + + + + + +
+
+
+ + + + im2sim + +
+ +
+
+
+
+
+
+
+ + + + +
+
+
+
+
+ + +
+ +
+
+

im2sim.layers.ImageConvBlock#

+
+
+class ImageConvBlock(in_channels, out_channels, rank, depth=2, activation=None, out_activation=None, conv_config=None, norm_config=None, attn_config=None, dropout_config=None, dropout_position=1, residual_connections=None, residual_type=ResidualConnectionType.ADD)[source]#
+

Bases: Module, ConfigurableModule

+

A configurable image convolutional block that consists of a sequence of +convolutional layers, normalization layers, dropout layers, and attention +layers. The block supports residual connections and allows for flexible +configuration of its components.

+

It is best used by creating a configuration object of type +:class:ImageConvBlockConfig and then calling the build method to create +an instance of the block.

+

Args:

+
+
in_channelsint

Number of input channels.

+
+
out_channelsint

Number of output channels.

+
+
rankint

The rank of the convolutional layers (e.g., 2 for 2D convolutions).

+
+
depthint, default=2

The number of convolutional layers in the block.

+
+
activationstr | None, default=None

Activation function applied after each convolutional layer.

+
+
out_activationstr | None, default=None

Activation function applied after the final layer.

+
+
conv_configLayerConfig | None, default=None

Configuration for convolutional layers. If None, a default configuration is used.

+
+
norm_configLayerConfig | None, default=None

Configuration for normalization layers. If None, a default configuration is used.

+
+
attn_configLayerConfig | None, default=None

Configuration for attention layers. If None, a default configuration is used.

+
+
dropout_configLayerConfig | None, default=None

Configuration for dropout layers. If None, a default configuration is used.

+
+
dropout_positionint | list[int], default=1

Position(s) of dropout layers within the block.

+
+
residual_connectionsdict[int, list[int]] | None, default=None

Specifies residual connections within the block. Keys represent target layers, +and values are lists of source layers.(e.g. {1: [0]} adds the block input to the output of layer 1.)

+
+
residual_typestr, default=ResidualConnectionType.ADD

Type of residual connection to use (e.g., “add”).

+
+
+

Example:

+

To create an ImageConvBlock with a depth of 3, ReLU activation, and softmax output activation, you can use the following code:

+
>>> cfg = ImageConvBlockConfig(depth=3, activation="ReLU", out_activation="softmax")
+>>> model = ImageConvBlock.build(
+>>>        rank=2,
+>>>        in_channels=32,
+>>>        out_channels=32,
+>>>        cfg=cfg,
+>>>    )
+
+
+

Initialize internal Module state, shared by both nn.Module and ScriptModule.

+
+
Parameters:
+
    +
  • in_channels (int)

  • +
  • out_channels (int)

  • +
  • rank (int)

  • +
  • depth (int)

  • +
  • activation (str | None)

  • +
  • out_activation (str | None)

  • +
  • conv_config (LayerConfig | None)

  • +
  • norm_config (LayerConfig | None)

  • +
  • attn_config (LayerConfig | None)

  • +
  • dropout_config (LayerConfig | None)

  • +
  • dropout_position (int | list[int])

  • +
  • residual_connections (dict[int, list[int]])

  • +
  • residual_type (str)

  • +
+
+
+
+
+forward(x)[source]#
+

Define the computation performed at every call.

+

Should be overridden by all subclasses.

+
+

Note

+

Although the recipe for forward pass needs to be defined within +this function, one should call the :class:Module instance afterwards +instead of this since the former takes care of running the +registered hooks while the latter silently ignores them.

+
+
+ +
+ +
+ +
+
+
+
+
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/api_docs/im2sim/layers/SqueezeExcite/index.html b/tools/docs/_build/dirhtml/api_docs/im2sim/layers/SqueezeExcite/index.html new file mode 100644 index 0000000..25f36b5 --- /dev/null +++ b/tools/docs/_build/dirhtml/api_docs/im2sim/layers/SqueezeExcite/index.html @@ -0,0 +1,273 @@ + + + + + im2sim.layers.SqueezeExcite - IM2SIM Documentation + + + + + + + + + + + + + + + +
+
+
+ + + + im2sim + +
+ +
+
+
+
+
+
+
+ + + + +
+
+
+
+
+ + +
+ +
+
+

im2sim.layers.SqueezeExcite#

+
+
+class SqueezeExcite(channels, rank, reduction=8)[source]#
+

Bases: Module

+

Squeeze-and-Excitation (SE) layer that adaptively recalibrates channel-wise feature responses.

+

This operation is useful for improving the representational power of convolutional neural networks by explicitly modeling interdependencies between channels[1].

+
+
Parameters:
+
    +
  • channels (int) – Number of input channels.

  • +
  • rank (int) – The rank of the input tensor (1 for 1D, 2 for 2D, 3 for 3D).

  • +
  • reduction (int) – Reduction ratio for the hidden layer in the SE block. Default is 8.

  • +
+
+
+

References

+ +

Initialize internal Module state, shared by both nn.Module and ScriptModule.

+
+
+forward(x)[source]#
+

Forward pass of the Efficient Channel Attention layer.

+
+
Parameters:
+

x (Tensor) – Input tensor of shape (batch_size, channels, *spatial_dims).

+
+
Returns:
+

Output tensor of shape (batch_size, channels, *spatial_dims) with channel-wise attention applied.

+
+
Return type:
+

Tensor

+
+
+
+ +
+ +
+ +
+
+
+
+
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/api_docs/im2sim/layers/index.html b/tools/docs/_build/dirhtml/api_docs/im2sim/layers/index.html new file mode 100644 index 0000000..8305fe3 --- /dev/null +++ b/tools/docs/_build/dirhtml/api_docs/im2sim/layers/index.html @@ -0,0 +1,276 @@ + + + + + im2sim.layers - IM2SIM Documentation + + + + + + + + + + + + + + + +
+
+
+ + + + im2sim + +
+ +
+
+
+
+
+
+
+ + + + +
+
+
+
+
+ + +
+ +
+
+

im2sim.layers#

+

Custom layers for building deep learning models.

+
+

Classes#

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

ConditionedSqueezeExcite

Conditioned Squeeze-and-Excitation [1] (SE) layer that adaptively recalibrates channel-wise feature responses based on an additional conditioning input.

DefaultGraphNorm

The default normalisation for im2sim graph blocks.

DepthwiseConv

Depthwise convolution layer that applies a separate convolutional filter to each input channel.

DepthwiseSeparableConv

Depthwise separable convolution layer that consists of a depthwise convolution followed by a pointwise convolution.

EfficientChannelAttn

Efficient Channel Attention (ECA) layer that adaptively selects important channels based on global context.

GhostConv

Ghost convolution layer that generates more feature maps from cheap operations.

GraphConvBlock

A convolutional block for graph data

GraphConvResBlock

A convolutional block for graph data

GraphResDecoderBlock

A graph convolutional decoder block with the same structure as MeshDeformNet and Image2Flow

ImageConvBlock

A configurable image convolutional block that consists of a sequence of convolutional layers, normalization layers, dropout layers, and attention layers.

SqueezeExcite

Squeeze-and-Excitation (SE) layer that adaptively recalibrates channel-wise feature responses.

+
+
+
+

Functions#

+
+ + + +
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/api_docs/im2sim/losses/index.html b/tools/docs/_build/dirhtml/api_docs/im2sim/losses/index.html new file mode 100644 index 0000000..75d7178 --- /dev/null +++ b/tools/docs/_build/dirhtml/api_docs/im2sim/losses/index.html @@ -0,0 +1,243 @@ + + + + + im2sim.losses - IM2SIM Documentation + + + + + + + + + + + + + + + +
+
+
+ + + + im2sim + +
+ +
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/api_docs/im2sim/models/HalfUNet/index.html b/tools/docs/_build/dirhtml/api_docs/im2sim/models/HalfUNet/index.html new file mode 100644 index 0000000..3cafe62 --- /dev/null +++ b/tools/docs/_build/dirhtml/api_docs/im2sim/models/HalfUNet/index.html @@ -0,0 +1,276 @@ + + + + + im2sim.models.HalfUNet - IM2SIM Documentation + + + + + + + + + + + + + + + +
+
+
+ + + + im2sim + +
+ +
+
+
+
+
+
+
+ + + + +
+
+
+
+
+ + +
+ +
+
+

im2sim.models.HalfUNet#

+
+
+class HalfUNet(in_channels, out_channels, rank, hidden_channels=64, num_downsamples=4, pool_spec=None, upsample_spec=None, block_cfg=None, blocks_per_level=2, out_activation=None, stem_block_cfg=None, encoder_block_cfg=None, out_block_cfg=None, fusion_type=ResidualConnectionType.ADD)[source]#
+

Bases: Module, ConfigurableModule

+

A Half-UNet[1] architecture for image processing tasks.

+
+
Parameters:
+
    +
  • in_channels (int) – Number of input channels.

  • +
  • out_channels (int) – Number of output channels.

  • +
  • rank (int) – Dimensionality of the input data (1 for 1D, 2 for 2D, 3 for 3D).

  • +
  • hidden_channels (int) – Number of channels in the hidden layers. Default is 64.

  • +
  • num_downsamples (int) – Number of downsampling operations in the encoder. Default is 4.

  • +
  • pool_spec (LayerConfig | list[LayerConfig]) – Specification for the pooling layers. Default is a MaxPool layer with kernel size 2 for all levels.

  • +
  • upsample_spec (LayerConfig | list[LayerConfig]) – Specification for the upsampling layers. Default is an Upsample layer with scale factor 2 and mode ‘trilinear’ for all levels.

  • +
  • block_cfg (ImageConvBlockConfig) – Configuration for the convolutional blocks. Default is a standard convolutional block with 2 layers, ReLU activation, and batch normalization.

  • +
  • blocks_per_level (int) – Number of convolutional blocks per level in the encoder. Default is 2.

  • +
  • out_activation (str | None) – Activation function for the output layer. Default is None, which means no activation is applied.

  • +
  • stem_block_cfg (ImageConvBlockConfig | None) – Configuration for the stem block. If None, it defaults to a single convolutional block with the same configuration as block_cfg.

  • +
  • encoder_block_cfg (list[ImageConvBlockConfig] | ImageConvBlockConfig | None) – Configuration for the encoder blocks. If None, it defaults to a list of block_cfg repeated for each downsampling level.

  • +
  • out_block_cfg (ImageConvBlockConfig | None) – Configuration for the output block. If None, it defaults to a single convolutional block with the same configuration as block_cfg and the specified out_activation.

  • +
  • fusion_type (ResidualConnectionType) – Type of residual connection to use in the network. It can be either ‘add’ (default), ‘concat’ or ‘average’. This determines how the encoder features are fused.

  • +
+
+
+

The best way to build a HalfUNet is to use the im2sim.configs.HalfUNetConfig class to define the configuration and then call the build method.

+

Example:

+

To create a HalfUNet model for single class segmentation, you can do the following:

+
>>> cfg = HalfUNetConfig(num_downsamples=3, hidden_channels=64)
+>>> cfg = cfg.apply_presets(["single_class_segmentation", "residual", "SE", "ghost_depthwise_separable"])
+>>> model = HalfUNet.build(in_channles=20, out_channels=1, rank=3, cfg=cfg)
+
+
+

References: +.. [1] H. Lu, Y. She, J. Tie, and S. Xu, Half-UNet: A Simplified U-Net Architecture for Medical Image Segmentation,

+
+

Front. Neuroinformatics, vol. 16, Jun. 2022, doi: 10.3389/fninf.2022.911679.

+
+
+
+forward(x)[source]#
+

Forward pass of the HalfUNet model.

+
+ +
+ +
+ +
+
+
+
+
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/api_docs/im2sim/models/index.html b/tools/docs/_build/dirhtml/api_docs/im2sim/models/index.html new file mode 100644 index 0000000..735679d --- /dev/null +++ b/tools/docs/_build/dirhtml/api_docs/im2sim/models/index.html @@ -0,0 +1,246 @@ + + + + + im2sim.models - IM2SIM Documentation + + + + + + + + + + + + + + + +
+
+
+ + + + im2sim + +
+ +
+
+
+
+
+
+
+ + + + +
+
+
+
+
+ + +
+ +
+
+

im2sim.models#

+

Predefined deep learning models for various tasks.

+
+

Classes#

+
+ + + + + + +

HalfUNet

A Half-UNet[1] architecture for image processing tasks.

+
+
+
+

Functions#

+
+ + + +
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/api_docs/im2sim/ops/index.html b/tools/docs/_build/dirhtml/api_docs/im2sim/ops/index.html new file mode 100644 index 0000000..ea7448c --- /dev/null +++ b/tools/docs/_build/dirhtml/api_docs/im2sim/ops/index.html @@ -0,0 +1,246 @@ + + + + + im2sim.ops - IM2SIM Documentation + + + + + + + + + + + + + + + +
+
+
+ + + + im2sim + +
+ +
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/api_docs/im2sim/ops/normtorange/index.html b/tools/docs/_build/dirhtml/api_docs/im2sim/ops/normtorange/index.html new file mode 100644 index 0000000..cabec93 --- /dev/null +++ b/tools/docs/_build/dirhtml/api_docs/im2sim/ops/normtorange/index.html @@ -0,0 +1,244 @@ + + + + + im2sim.ops.normtorange - IM2SIM Documentation + + + + + + + + + + + + + + + +
+
+
+ + + + im2sim + +
+ +
+
+
+
+
+
+
+ + + + +
+
+
+
+
+ + +
+ +
+
+

im2sim.ops.normtorange#

+
+
+normtorange(x, max=None, min=None, a=0, b=1)[source]#
+

Normalizes the input tensor x to a specified range [a, b].

+
+
Parameters:
+
    +
  • x (Tensor) – Input tensor to be normalized.

  • +
  • max (float, optional) – Maximum value for normalization. If None, uses the maximum of x.

  • +
  • min (float, optional) – Minimum value for normalization. If None, uses the minimum of x.

  • +
  • a (float, optional) – Lower bound of the target range. Default is 0.

  • +
  • b (float, optional) – Upper bound of the target range. Default is 1.

  • +
+
+
+
+ +
+ +
+
+
+
+
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/api_docs/im2sim/plot/index.html b/tools/docs/_build/dirhtml/api_docs/im2sim/plot/index.html new file mode 100644 index 0000000..76543e7 --- /dev/null +++ b/tools/docs/_build/dirhtml/api_docs/im2sim/plot/index.html @@ -0,0 +1,235 @@ + + + + + im2sim.plot - IM2SIM Documentation + + + + + + + + + + + + + + + +
+
+
+ + + + im2sim + +
+ +
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/api_docs/index.html b/tools/docs/_build/dirhtml/api_docs/index.html new file mode 100644 index 0000000..4d555bf --- /dev/null +++ b/tools/docs/_build/dirhtml/api_docs/index.html @@ -0,0 +1,220 @@ + + + + + IM2SIM API documentation - IM2SIM Documentation + + + + + + + + + + + + + + + +
+
+
+ + + + im2sim + +
+ +
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/genindex/index.html b/tools/docs/_build/dirhtml/genindex/index.html new file mode 100644 index 0000000..3a0bde7 --- /dev/null +++ b/tools/docs/_build/dirhtml/genindex/index.html @@ -0,0 +1,300 @@ + + + + + Index - IM2SIM Documentation + + + + + + + + + + +
+
+
+ + + + im2sim + +
+ +
+
+
+
+
+
+
+
+ + +

Index

+ +
+ C + | D + | E + | F + | G + | H + | I + | M + | N + | S + +
+

C

+ + +
+ +

D

+ + + +
+ +

E

+ + +
+ +

F

+ + +
+ +

G

+ + + +
+ +

H

+ + + +
+ +

I

+ + + +
    +
  • + im2sim + +
  • +
  • + im2sim.configs + +
  • +
  • + im2sim.data + +
  • +
  • + im2sim.layers + +
  • +
  • + im2sim.losses + +
  • +
+ +

M

+ + +
+ +

N

+ + +
+ +

S

+ + +
+ + + +
+ +
+
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/guide/contribute/index.html b/tools/docs/_build/dirhtml/guide/contribute/index.html new file mode 100644 index 0000000..bbb918c --- /dev/null +++ b/tools/docs/_build/dirhtml/guide/contribute/index.html @@ -0,0 +1,207 @@ + + + + + Contributing - IM2SIM Documentation + + + + + + + + + + + + + + + +
+
+
+ + + + im2sim + +
+ +
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/guide/faq/index.html b/tools/docs/_build/dirhtml/guide/faq/index.html new file mode 100644 index 0000000..9feb00a --- /dev/null +++ b/tools/docs/_build/dirhtml/guide/faq/index.html @@ -0,0 +1,216 @@ + + + + + Frequently Asked Questions - IM2SIM Documentation + + + + + + + + + + + + + + + +
+
+
+ + + + im2sim + +
+ +
+
+
+
+
+
+
+ + + + +
+
+
+
+
+ + +
+ +
+
+

Frequently Asked Questions#

+

When trying to install TensorFlow MRI, I get an error about OpenEXR which +includes: +``OpenEXR.cpp:36:10: fatal error: ImathBox.h: No such file or directory``. What +do I do?

+

OpenEXR is needed by TensorFlow Graphics, which is a dependency of TensorFlow +MRI. This issue can be fixed by installing the OpenEXR library. On +Debian/Ubuntu:

+
$ apt install libopenexr-dev
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/guide/index.html b/tools/docs/_build/dirhtml/guide/index.html new file mode 100644 index 0000000..31db695 --- /dev/null +++ b/tools/docs/_build/dirhtml/guide/index.html @@ -0,0 +1,206 @@ + + + + + IM2SIM guide - IM2SIM Documentation + + + + + + + + + + + + + + + +
+
+
+ + + + im2sim + +
+ +
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/guide/install/index.html b/tools/docs/_build/dirhtml/guide/install/index.html new file mode 100644 index 0000000..34de0a3 --- /dev/null +++ b/tools/docs/_build/dirhtml/guide/install/index.html @@ -0,0 +1,284 @@ + + + + + Install TensorFlow MRI - IM2SIM Documentation + + + + + + + + + + + + + + + +
+
+
+ + + + im2sim + +
+ +
+
+
+
+
+
+
+ + + + +
+
+
+
+
+ + +
+ +
+
+

Install TensorFlow MRI#

+
+

Requirements#

+

TensorFlow MRI should work in most Linux systems that meet the +requirements for TensorFlow.

+
+

Warning

+

TensorFlow MRI is not yet available for Windows or macOS. +Help us support them!.

+
+
+

TensorFlow compatibility#

+

Each TensorFlow MRI release is compiled against a specific version of +TensorFlow. To ensure compatibility, it is recommended to install matching +versions of TensorFlow and TensorFlow MRI according to the +TensorFlow compatibility table.

+
+

Warning

+

Each TensorFlow MRI version aims to target and support the latest TensorFlow +version only. A new version of TensorFlow MRI will be released shortly after +each TensorFlow release. TensorFlow MRI versions that target older versions +of TensorFlow will not generally receive any updates.

+
+
+
+
+

Set up your system#

+

You will need a working TensorFlow installation. Follow the TensorFlow +installation instructions if you do not +have one already.

+
+

Use a GPU#

+

If you need GPU support, we suggest that you use one of the +TensorFlow Docker images. +These come with a GPU-enabled TensorFlow installation and are the easiest way +to run TensorFlow and TensorFlow MRI on your system.

+
$ docker pull tensorflow/tensorflow:latest-gpu
+
+
+

Alternatively, make sure you follow +these instructions when setting up +your system.

+
+
+
+

Download from PyPI#

+

TensorFlow MRI is available on the Python package index (PyPI) and can be +installed using the pip package manager:

+
$ pip install tensorflow-mri
+
+
+
+
+

Run in Google Colab#

+

To get started without installing anything on your system, you can use +Google Colab. +Simply create a new notebook and use pip to install TensorFlow MRI.

+
!pip install tensorflow-mri
+
+
+

The Colab environment is already configured to run TensorFlow and has GPU +support.

+
+
+

TensorFlow compatibility table#

+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/guide/linalg/index.html b/tools/docs/_build/dirhtml/guide/linalg/index.html new file mode 100644 index 0000000..0fe2a79 --- /dev/null +++ b/tools/docs/_build/dirhtml/guide/linalg/index.html @@ -0,0 +1,207 @@ + + + + + Linear algebra - IM2SIM Documentation + + + + + + + + + + + + + + + +
+
+
+ + + + im2sim + +
+ +
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/guide/nufft/index.html b/tools/docs/_build/dirhtml/guide/nufft/index.html new file mode 100644 index 0000000..1a3e2b1 --- /dev/null +++ b/tools/docs/_build/dirhtml/guide/nufft/index.html @@ -0,0 +1,480 @@ + + + + + Non-uniform fast Fourier transform (NUFFT) - IM2SIM Documentation + + + + + + + + + + + + + + + +
+
+
+ + + + im2sim + +
+ +
+
+
+
+
+
+
+ + + + +
+
+
+
+
+ + +
+ +
+
+

Non-uniform fast Fourier transform (NUFFT)#

+

TensorFlow MRI provides an efficient NUFFT operator for both CPU and GPU, based +on the algorithms by the Flatiron Institute (see +this paper and +this paper +for more details). The operator is available as +tfmri.signal.nufft.

+
+

Note

+

The tfmri.signal.nufft function is an alias of the nufft function in the +TensorFlow NUFFT +stand-alone package. Please direct any issues about the NUFFT function directly +to the TensorFlow NUFFT repository.

+
+
+

Warning

+

The current NUFFT implementation uses the FFTW library, +which is released under the GNU GPL. If you are using the NUFFT for commercial +purposes, you will need to purchase a license from MIT or adapt the code to use +a different FFT library. If you do the latter, please consider +contributing +your modification so others may benefit.

+
+

The NUFFT function can be used to efficiently evaluate the Fourier transform +when either the input data or the output data does not lie on a uniform grid, +in which case the standard fast Fourier transform (FFT) algorithm cannot be +used. There are 3 transform types depending whether the input is non-uniform, +the output is non-uniform or both input and output are non-uniform.

+
    +
  • A type-1 transform evaluates the Fourier transform on a uniform grid +given a set of arbitrary points (i.e, non-uniform to uniform).

  • +
  • A type-2 transform evaluates the Fourier transform on a set of arbitrary +points given a uniform grid. (i.e., uniform to non-uniform).

  • +
  • A type-3 transform evaluates the Fourier transform on a set of arbitrary +points given a set of arbitrary points (i.e., non-uniform to non-uniform).

  • +
+
+

Tip

+

The type of the transform can be specified using the transform_type argument.

+
+
+

Warning

+

NUFFT type-3 is not currently supported or planned, but contributions will be +accepted.

+
+

The NUFFT may be forward (signal to frequency domain) or backward +(frequency to signal domain), regardless of the transform type.

+
+

Tip

+

The direction of the transform can be specified using the fft_direction +argument.

+
+
+

Guided example#

+

As an example, let’s take an image of the Shepp-Logan phantom and evaluate its +Fourier transform on a set of sampling points defining a radial k-space +trajectory, using a forward, type-2 NUFFT. Then we will see how to recover +the image from the radial k-space data, using a backward, type-1 NUFFT.

+
+
+
%pip install -q tensorflow tensorflow-mri
+
+
+
+
+
WARNING: You are using pip version 22.0.4; however, version 22.2 is available.
+You should consider upgrading via the '/usr/local/bin/python3.8 -m pip install --upgrade pip' command.
+Note: you may need to restart the kernel to use updated packages.
+
+
+
+
+

Now import both packages and create an example image using +tfmri.image.phantom:

+
+
+
import tensorflow as tf
+import tensorflow_mri as tfmri
+
+# Create
+image_shape = [256, 256]
+image = tfmri.image.phantom(shape=image_shape, dtype=tf.complex64)
+
+print("image: \n - shape: {}\n - dtype: {}".format(image.shape, image.dtype))
+
+
+
+
+
2022-07-21 17:25:43.649824: I tensorflow/core/util/util.cc:169] oneDNN custom operations are on. You may see slightly different numerical results due to floating-point round-off errors from different computation orders. To turn them off, set the environment variable `TF_ENABLE_ONEDNN_OPTS=0`.
+2022-07-21 17:25:59.264266: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:975] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
+2022-07-21 17:25:59.268928: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:975] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
+2022-07-21 17:25:59.269048: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:975] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
+2022-07-21 17:25:59.269524: I tensorflow/core/platform/cpu_feature_guard.cc:193] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operations:  AVX2 AVX512F AVX512_VNNI FMA
+To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags.
+2022-07-21 17:25:59.270142: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:975] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
+2022-07-21 17:25:59.270251: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:975] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
+2022-07-21 17:25:59.270327: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:975] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
+2022-07-21 17:25:59.612269: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:975] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
+2022-07-21 17:25:59.612401: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:975] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
+2022-07-21 17:25:59.612481: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:975] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
+2022-07-21 17:25:59.612559: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1532] Created device /job:localhost/replica:0/task:0/device:GPU:0 with 14239 MB memory:  -> device: 0, name: NVIDIA GeForce RTX 3080 Laptop GPU, pci bus id: 0000:01:00.0, compute capability: 8.6
+
+
+
image: 
+ - shape: (256, 256)
+ - dtype: <dtype: 'complex64'>
+
+
+
+
+

Let us also create a k-space trajectory. In this example we will create a +radial trajectory.

+
+
+
trajectory = tfmri.sampling.radial_trajectory(
+    base_resolution=256, views=233, flatten_encoding_dims=True)
+
+print("trajectory: \n - shape: {}\n - dtype: {}\n - range: [{}, {}]".format(
+    trajectory.shape, trajectory.dtype,
+    tf.math.reduce_min(trajectory), tf.math.reduce_max(trajectory)))
+
+
+
+
+
trajectory: 
+ - shape: (119296, 2)
+ - dtype: <dtype: 'float32'>
+ - range: [-3.1415927410125732, 3.141521453857422]
+
+
+
+
+

The trajectory should have shape [..., M, N], where M is the number of +points and N is the number of dimensions. Any additional dimensions ... will +be treated as batch dimensions.

+

Batch dimensions for image and traj, if any, will be broadcasted.

+

Spatial frequencies should be provided in radians/voxel, ie, in the range +[-pi, pi].

+

Finally, we’ll also need density compensation weights for our set of nonuniform +points. These are necessary in the adjoint transform, to compensate for the fact +that the sampling density in a radial trajectory is not uniform.

+
+
+
density = tfmri.sampling.radial_density(base_resolution=256, views=233)
+density = tf.reshape(density, [-1])
+
+print("density: \n - shape: {}\n - dtype: {}".format(
+    density.shape, density.dtype))
+
+
+
+
+
density: 
+ - shape: (119296,)
+ - dtype: <dtype: 'float32'>
+
+
+
+
+
+

Forward transform (image to k-space)#

+

Next, let’s calculate the k-space coefficients for the given image and trajectory points (image to k-space transform).

+
+
+
kspace = tfmri.signal.nufft(image, trajectory,
+                            transform_type='type_2',
+                            fft_direction='forward')
+
+print("kspace: \n - shape: {}\n - dtype: {}".format(kspace.shape, kspace.dtype))
+
+
+
+
+
kspace: 
+ - shape: (119296,)
+ - dtype: <dtype: 'complex64'>
+
+
+
+
+

We are using a type-2 transform (uniform to nonuniform) and a forward FFT +(image domain to frequency domain). These are the default values for +transform_type and fft_direction, so providing them was not necessary in +this case.

+
+
+

Adjoint transform (k-space to image)#

+

We will now perform the adjoint transform to recover the image given the +k-space data. In this case, we will use a type-1 transform (nonuniform to +uniform) and a backward FFT (frequency domain to image domain). Also note that, +prior to evaluating the NUFFT, we will compensate for the nonuniform sampling +density by simply dividing the k-space samples by the density weights. +Finally, for type-1 transforms we need to specify an additional grid_shape +argument, which should be the size of the image. If there are any batch +dimensions, grid_shape should not include them.

+
+
+
# Apply density compensation.
+kspace /= tf.cast(density, tf.complex64)
+
+recon = tfmri.signal.nufft(kspace, trajectory,
+                           grid_shape=image_shape,
+                           transform_type='type_1',
+                           fft_direction='backward')
+
+print("recon: \n - shape: {}\n - dtype: {}".format(recon.shape, recon.dtype))
+
+
+
+
+
recon: 
+ - shape: (256, 256)
+ - dtype: <dtype: 'complex64'>
+
+
+
+
+

Finally, let’s visualize the images.

+
+
+
import matplotlib.pyplot as plt
+def plot_images(image, recon):
+  _, ax = plt.subplots(1, 2, figsize=(9.6, 5.4))
+  ax[0].imshow(tf.abs(image), cmap='gray')
+  ax[0].set_title("Original image")
+  ax[1].imshow(tf.abs(recon), cmap='gray')
+  ax[1].set_title("Image after forward\nand adjoint NUFFT")
+  plt.show()
+plot_images(image, recon)
+
+
+
+
+../../_images/ed739320bcec44e952e2c94e8c30931e2f1565301f14f6e2722599cc273859a5.png +
+
+
+
+

Use the linear operator#

+

You can also use +tfmri.linalg.LinearOperatorNUFFT +to perform forward and adjoint NUFFT. This might be particularly useful when +building MRI reconstruction methods, as you can take advantage of the features +of the linear algebra framework.

+
+
+
# Create the linear operator for the specified image shape, trajectory and
+# density.
+linop_nufft = tfmri.linalg.LinearOperatorNUFFT(
+    image_shape, trajectory=trajectory, density=density)
+
+# Apply forward transform to obtain the *k*-space signal given an image.
+kspace = linop_nufft.transform(image)
+
+# Apply adjoint transform to obtain an image given a *k*-space signal.
+recon = linop_nufft.transform(kspace, adjoint=True)
+
+plot_images(image, recon)
+
+
+
+
+../../_images/ed739320bcec44e952e2c94e8c30931e2f1565301f14f6e2722599cc273859a5.png +
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/guide/optim/index.html b/tools/docs/_build/dirhtml/guide/optim/index.html new file mode 100644 index 0000000..7824121 --- /dev/null +++ b/tools/docs/_build/dirhtml/guide/optim/index.html @@ -0,0 +1,207 @@ + + + + + Optimization - IM2SIM Documentation + + + + + + + + + + + + + + + +
+
+
+ + + + im2sim + +
+ +
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/guide/recon/index.html b/tools/docs/_build/dirhtml/guide/recon/index.html new file mode 100644 index 0000000..e86de0f --- /dev/null +++ b/tools/docs/_build/dirhtml/guide/recon/index.html @@ -0,0 +1,207 @@ + + + + + MR image reconstruction - IM2SIM Documentation + + + + + + + + + + + + + + + +
+
+
+ + + + im2sim + +
+ +
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/index.html b/tools/docs/_build/dirhtml/index.html new file mode 100644 index 0000000..8e0f6c8 --- /dev/null +++ b/tools/docs/_build/dirhtml/index.html @@ -0,0 +1,218 @@ + + + + + IM2SIM Documentation + + + + + + + + + + + + + + + +
+
+
+ + + + im2sim + +
+ +
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/objects.inv b/tools/docs/_build/dirhtml/objects.inv new file mode 100644 index 0000000..3b87578 Binary files /dev/null and b/tools/docs/_build/dirhtml/objects.inv differ diff --git a/tools/docs/_build/dirhtml/py-modindex/index.html b/tools/docs/_build/dirhtml/py-modindex/index.html new file mode 100644 index 0000000..d2cb27c --- /dev/null +++ b/tools/docs/_build/dirhtml/py-modindex/index.html @@ -0,0 +1,139 @@ + + + + + Python Module Index - IM2SIM Documentation + + + + + + + + + + + + + +
+
+
+ + + + im2sim + +
+ +
+
+
+
+
+
+
+ + +
+
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/robots.txt b/tools/docs/_build/dirhtml/robots.txt new file mode 100644 index 0000000..6d0bb8a --- /dev/null +++ b/tools/docs/_build/dirhtml/robots.txt @@ -0,0 +1,3 @@ +User-agent: * + +Sitemap: https://mrphys.github.io/im2sim/sitemap.xml diff --git a/tools/docs/_build/dirhtml/search/index.html b/tools/docs/_build/dirhtml/search/index.html new file mode 100644 index 0000000..c7b9892 --- /dev/null +++ b/tools/docs/_build/dirhtml/search/index.html @@ -0,0 +1,113 @@ + + + + + Search - IM2SIM Documentation + + + + + + + + + + + + + + +
+
+
+ + + + im2sim + +
+ +
+
+
+
+
+
+
+
+ +

Search

+ + + + +

+ Searching for multiple words only shows matches that contain + all words. +

+ + +
+ + +
+

+ + +
+ + +
+ +
+
+
+
+ +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/searchindex.js b/tools/docs/_build/dirhtml/searchindex.js new file mode 100644 index 0000000..2128e03 --- /dev/null +++ b/tools/docs/_build/dirhtml/searchindex.js @@ -0,0 +1 @@ +Search.setIndex({"alltitles": {"Adjoint transform (k-space to image)": [[29, "adjoint-transform-k-space-to-image"]], "Classes": [[1, "classes"], [2, "classes"], [5, "classes"], [6, "classes"], [18, "classes"], [19, "classes"], [21, "classes"], [23, "classes"]], "Contributing": [[25, null]], "Download from PyPI": [[27, "download-from-pypi"]], "Forward transform (image to k-space)": [[29, "forward-transform-image-to-k-space"]], "Frequently Asked Questions": [[26, null]], "Functions": [[1, "functions"], [2, "functions"], [5, "functions"], [6, "functions"], [18, "functions"], [19, "functions"], [21, "functions"], [23, "functions"]], "Guided example": [[29, "guided-example"]], "IM2SIM 0.1.0": [[32, null], [33, null]], "IM2SIM API documentation": [[0, null]], "IM2SIM guide": [[24, null]], "Install TensorFlow MRI": [[27, null]], "Linear algebra": [[28, null]], "MR image reconstruction": [[31, null]], "Modules": [[1, "modules"]], "Non-uniform fast Fourier transform (NUFFT)": [[29, null]], "Optimization": [[30, null]], "Requirements": [[27, "requirements"]], "Run in Google Colab": [[27, "run-in-google-colab"]], "Set up your system": [[27, "set-up-your-system"]], "TensorFlow MRI tutorials": [[34, null]], "TensorFlow compatibility": [[27, "tensorflow-compatibility"]], "TensorFlow compatibility table": [[27, "tensorflow-compatibility-table"]], "Use a GPU": [[27, "use-a-gpu"]], "Use the linear operator": [[29, "use-the-linear-operator"]], "im2sim": [[1, null]], "im2sim.configs": [[2, null]], "im2sim.configs.HalfUNetConfig": [[3, null]], "im2sim.configs.ImageConvBlockConfig": [[4, null]], "im2sim.data": [[5, null]], "im2sim.layers": [[6, null]], "im2sim.layers.ConditionedSqueezeExcite": [[7, null]], "im2sim.layers.DefaultGraphNorm": [[8, null]], "im2sim.layers.DepthwiseConv": [[9, null]], "im2sim.layers.DepthwiseSeparableConv": [[10, null]], "im2sim.layers.EfficientChannelAttn": [[11, null]], "im2sim.layers.GhostConv": [[12, null]], "im2sim.layers.GraphConvBlock": [[13, null]], "im2sim.layers.GraphConvResBlock": [[14, null]], "im2sim.layers.GraphResDecoderBlock": [[15, null]], "im2sim.layers.ImageConvBlock": [[16, null]], "im2sim.layers.SqueezeExcite": [[17, null]], "im2sim.losses": [[18, null]], "im2sim.models": [[19, null]], "im2sim.models.HalfUNet": [[20, null]], "im2sim.ops": [[21, null]], "im2sim.ops.normtorange": [[22, null]], "im2sim.plot": [[23, null]]}, "docnames": ["api_docs", "api_docs/im2sim", "api_docs/im2sim/configs", "api_docs/im2sim/configs/HalfUNetConfig", "api_docs/im2sim/configs/ImageConvBlockConfig", "api_docs/im2sim/data", "api_docs/im2sim/layers", "api_docs/im2sim/layers/ConditionedSqueezeExcite", "api_docs/im2sim/layers/DefaultGraphNorm", "api_docs/im2sim/layers/DepthwiseConv", "api_docs/im2sim/layers/DepthwiseSeparableConv", "api_docs/im2sim/layers/EfficientChannelAttn", "api_docs/im2sim/layers/GhostConv", "api_docs/im2sim/layers/GraphConvBlock", "api_docs/im2sim/layers/GraphConvResBlock", "api_docs/im2sim/layers/GraphResDecoderBlock", "api_docs/im2sim/layers/ImageConvBlock", "api_docs/im2sim/layers/SqueezeExcite", "api_docs/im2sim/losses", "api_docs/im2sim/models", "api_docs/im2sim/models/HalfUNet", "api_docs/im2sim/ops", "api_docs/im2sim/ops/normtorange", "api_docs/im2sim/plot", "guide", "guide/contribute", "guide/faq", "guide/install", "guide/linalg", "guide/nufft", "guide/optim", "guide/recon", "index", "templates/index", "tutorials"], "envversion": {"sphinx": 64, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx.ext.intersphinx": 1}, "filenames": ["api_docs.rst", "api_docs/im2sim.rst", "api_docs/im2sim/configs.rst", "api_docs/im2sim/configs/HalfUNetConfig.rst", "api_docs/im2sim/configs/ImageConvBlockConfig.rst", "api_docs/im2sim/data.rst", "api_docs/im2sim/layers.rst", "api_docs/im2sim/layers/ConditionedSqueezeExcite.rst", "api_docs/im2sim/layers/DefaultGraphNorm.rst", "api_docs/im2sim/layers/DepthwiseConv.rst", "api_docs/im2sim/layers/DepthwiseSeparableConv.rst", "api_docs/im2sim/layers/EfficientChannelAttn.rst", "api_docs/im2sim/layers/GhostConv.rst", "api_docs/im2sim/layers/GraphConvBlock.rst", "api_docs/im2sim/layers/GraphConvResBlock.rst", "api_docs/im2sim/layers/GraphResDecoderBlock.rst", "api_docs/im2sim/layers/ImageConvBlock.rst", "api_docs/im2sim/layers/SqueezeExcite.rst", "api_docs/im2sim/losses.rst", "api_docs/im2sim/models.rst", "api_docs/im2sim/models/HalfUNet.rst", "api_docs/im2sim/ops.rst", "api_docs/im2sim/ops/normtorange.rst", "api_docs/im2sim/plot.rst", "guide.rst", "guide/contribute.ipynb", "guide/faq.rst", "guide/install.rst", "guide/linalg.ipynb", "guide/nufft.ipynb", "guide/optim.ipynb", "guide/recon.ipynb", "index.rst", "templates/index.rst", "tutorials.rst"], "indexentries": {"conditionedsqueezeexcite (class in im2sim.layers)": [[7, "im2sim.layers.ConditionedSqueezeExcite", false]], "defaultgraphnorm (class in im2sim.layers)": [[8, "im2sim.layers.DefaultGraphNorm", false]], "depthwiseconv (class in im2sim.layers)": [[9, "im2sim.layers.DepthwiseConv", false]], "depthwiseseparableconv (class in im2sim.layers)": [[10, "im2sim.layers.DepthwiseSeparableConv", false]], "efficientchannelattn (class in im2sim.layers)": [[11, "im2sim.layers.EfficientChannelAttn", false]], "forward() (conditionedsqueezeexcite method)": [[7, "im2sim.layers.ConditionedSqueezeExcite.forward", false]], "forward() (defaultgraphnorm method)": [[8, "im2sim.layers.DefaultGraphNorm.forward", false]], "forward() (depthwiseconv method)": [[9, "im2sim.layers.DepthwiseConv.forward", false]], "forward() (depthwiseseparableconv method)": [[10, "im2sim.layers.DepthwiseSeparableConv.forward", false]], "forward() (efficientchannelattn method)": [[11, "im2sim.layers.EfficientChannelAttn.forward", false]], "forward() (ghostconv method)": [[12, "im2sim.layers.GhostConv.forward", false]], "forward() (graphconvblock method)": [[13, "im2sim.layers.GraphConvBlock.forward", false]], "forward() (graphconvresblock method)": [[14, "im2sim.layers.GraphConvResBlock.forward", false]], "forward() (graphresdecoderblock method)": [[15, "im2sim.layers.GraphResDecoderBlock.forward", false]], "forward() (halfunet method)": [[20, "im2sim.models.HalfUNet.forward", false]], "forward() (imageconvblock method)": [[16, "im2sim.layers.ImageConvBlock.forward", false]], "forward() (squeezeexcite method)": [[17, "im2sim.layers.SqueezeExcite.forward", false]], "ghostconv (class in im2sim.layers)": [[12, "im2sim.layers.GhostConv", false]], "graphconvblock (class in im2sim.layers)": [[13, "im2sim.layers.GraphConvBlock", false]], "graphconvresblock (class in im2sim.layers)": [[14, "im2sim.layers.GraphConvResBlock", false]], "graphresdecoderblock (class in im2sim.layers)": [[15, "im2sim.layers.GraphResDecoderBlock", false]], "halfunet (class in im2sim.models)": [[20, "im2sim.models.HalfUNet", false]], "halfunetconfig (class in im2sim.configs)": [[3, "im2sim.configs.HalfUNetConfig", false]], "im2sim": [[1, "module-im2sim", false]], "im2sim.configs": [[2, "module-im2sim.configs", false]], "im2sim.data": [[5, "module-im2sim.data", false]], "im2sim.layers": [[6, "module-im2sim.layers", false]], "im2sim.losses": [[18, "module-im2sim.losses", false]], "im2sim.models": [[19, "module-im2sim.models", false]], "im2sim.ops": [[21, "module-im2sim.ops", false]], "im2sim.plot": [[23, "module-im2sim.plot", false]], "imageconvblock (class in im2sim.layers)": [[16, "im2sim.layers.ImageConvBlock", false]], "imageconvblockconfig (class in im2sim.configs)": [[4, "im2sim.configs.ImageConvBlockConfig", false]], "module": [[1, "module-im2sim", false], [2, "module-im2sim.configs", false], [5, "module-im2sim.data", false], [6, "module-im2sim.layers", false], [18, "module-im2sim.losses", false], [19, "module-im2sim.models", false], [21, "module-im2sim.ops", false], [23, "module-im2sim.plot", false]], "normtorange() (in module im2sim.ops)": [[22, "im2sim.ops.normtorange", false]], "squeezeexcite (class in im2sim.layers)": [[17, "im2sim.layers.SqueezeExcite", false]]}, "objects": {"": [[1, 0, 0, "-", "im2sim"]], "im2sim": [[2, 0, 0, "-", "configs"], [5, 0, 0, "-", "data"], [6, 0, 0, "-", "layers"], [18, 0, 0, "-", "losses"], [19, 0, 0, "-", "models"], [21, 0, 0, "-", "ops"], [23, 0, 0, "-", "plot"]], "im2sim.configs": [[3, 1, 1, "", "HalfUNetConfig"], [4, 1, 1, "", "ImageConvBlockConfig"]], "im2sim.layers": [[7, 1, 1, "", "ConditionedSqueezeExcite"], [8, 1, 1, "", "DefaultGraphNorm"], [9, 1, 1, "", "DepthwiseConv"], [10, 1, 1, "", "DepthwiseSeparableConv"], [11, 1, 1, "", "EfficientChannelAttn"], [12, 1, 1, "", "GhostConv"], [13, 1, 1, "", "GraphConvBlock"], [14, 1, 1, "", "GraphConvResBlock"], [15, 1, 1, "", "GraphResDecoderBlock"], [16, 1, 1, "", "ImageConvBlock"], [17, 1, 1, "", "SqueezeExcite"]], "im2sim.layers.ConditionedSqueezeExcite": [[7, 2, 1, "", "forward"]], "im2sim.layers.DefaultGraphNorm": [[8, 2, 1, "", "forward"]], "im2sim.layers.DepthwiseConv": [[9, 2, 1, "", "forward"]], "im2sim.layers.DepthwiseSeparableConv": [[10, 2, 1, "", "forward"]], "im2sim.layers.EfficientChannelAttn": [[11, 2, 1, "", "forward"]], "im2sim.layers.GhostConv": [[12, 2, 1, "", "forward"]], "im2sim.layers.GraphConvBlock": [[13, 2, 1, "", "forward"]], "im2sim.layers.GraphConvResBlock": [[14, 2, 1, "", "forward"]], "im2sim.layers.GraphResDecoderBlock": [[15, 2, 1, "", "forward"]], "im2sim.layers.ImageConvBlock": [[16, 2, 1, "", "forward"]], "im2sim.layers.SqueezeExcite": [[17, 2, 1, "", "forward"]], "im2sim.models": [[20, 1, 1, "", "HalfUNet"]], "im2sim.models.HalfUNet": [[20, 2, 1, "", "forward"]], "im2sim.ops": [[22, 3, 1, "", "normtorange"]]}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "class", "Python class"], "2": ["py", "method", "Python method"], "3": ["py", "function", "Python function"]}, "objtypes": {"0": "py:module", "1": "py:class", "2": "py:method", "3": "py:function"}, "terms": {"": [4, 12, 16, 20, 29], "0": [4, 16, 22, 29], "00": 29, "0000": 29, "01": 29, "01507": [7, 17], "02357": 10, "03151": 11, "04": [10, 11], "04861": 9, "07": 29, "0_residu": 4, "0m": 29, "0mnote": 29, "1": [3, 4, 7, 9, 10, 11, 12, 13, 15, 16, 17, 20, 22, 29], "10": [7, 9, 10, 11, 12, 17, 20, 26], "11907": 12, "119296": 29, "13": 12, "141521453857422": 29, "1415927410125732": 29, "14239": 29, "144": 15, "1532": 29, "16": [12, 20], "1610": 10, "169": 29, "17": [9, 29], "1704": 9, "1709": [7, 17], "1910": 11, "1911": 12, "193": 29, "1_residu": 4, "1d": [3, 7, 9, 10, 11, 12, 17, 20], "2": [3, 4, 7, 9, 10, 11, 12, 13, 14, 16, 17, 20, 29], "20": [3, 20], "2017": [9, 10], "2018": [7, 17], "2020": [11, 12], "2022": [12, 20, 29], "21": 29, "22": 29, "233": 29, "25": 29, "256": 29, "264266": 29, "268928": 29, "269048": 29, "269524": 29, "27": [7, 17], "270142": 29, "270251": 29, "270327": 29, "288": 15, "2d": [3, 7, 9, 10, 11, 12, 16, 17, 20], "3": [3, 4, 7, 9, 10, 11, 12, 14, 15, 16, 17, 20, 29], "3080": 29, "32": [13, 14, 15, 16], "3389": [12, 20], "33m": 29, "33mwarn": 29, "36": 26, "384": 15, "3d": [7, 9, 10, 11, 12, 17, 20], "4": [3, 20, 29], "43": 29, "48550": [7, 9, 10, 11, 12, 17], "5": 29, "59": 29, "6": [7, 29], "612269": 29, "612401": 29, "612481": 29, "612559": 29, "64": [3, 15, 20], "649824": 29, "8": [7, 17, 29], "9": 29, "911679": [12, 20], "96": 15, "975": 29, "A": [9, 12, 13, 14, 15, 16, 20, 27, 29], "As": 29, "For": 3, "If": [3, 9, 10, 12, 15, 16, 20, 22, 27, 29], "In": [29, 34], "It": [3, 16, 20], "No": 26, "On": 26, "The": [3, 4, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 20, 27, 29], "Then": 29, "There": 29, "These": [27, 29], "To": [3, 4, 16, 20, 27, 29], "_": 29, "ab": 29, "about": [26, 29], "accept": 29, "access": 34, "accord": 27, "activ": [3, 4, 10, 13, 14, 15, 16, 20], "ad": [3, 4, 9, 10, 12], "adapt": [7, 11, 17, 29], "add": [3, 4, 7, 9, 10, 12, 16, 20], "addit": [4, 7, 29, 34], "advantag": 29, "affin": 4, "after": [4, 10, 13, 14, 15, 16, 27, 29], "afterward": [13, 14, 15, 16], "against": 27, "aim": 27, "al": 9, "algebra": 29, "algorithm": 29, "alia": 29, "all": [3, 8, 9, 10, 12, 13, 14, 15, 16, 20, 34], "allow": [7, 16], "alon": 29, "alreadi": 27, "also": [3, 4, 7, 29, 34], "altern": [27, 34], "although": [13, 14, 15, 16], "an": [3, 4, 7, 16, 20, 26, 29], "ani": [27, 29], "anyth": 27, "appli": [3, 4, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17, 20, 29], "applic": 9, "apply_preset": [3, 4, 20], "appropri": 29, "apr": [9, 10], "apt": 26, "ar": [3, 4, 8, 16, 20, 27, 29, 34], "arbitrari": 29, "architectur": [3, 12, 20], "arg": 16, "argument": [13, 14, 15, 29], "arxiv": [7, 9, 10, 11, 12, 17], "attent": [3, 4, 7, 11, 16, 17], "attn_config": [4, 16], "attribut": [3, 4], "automat": 3, "avail": [27, 29], "averag": [3, 20], "avx2": 29, "avx512_vnni": 29, "avx512f": 29, "ax": 29, "b": [11, 22], "back": 4, "backward": 29, "base": [3, 4, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 20, 29], "base_resolut": 29, "batch": [3, 8, 20, 29], "batch_siz": [7, 9, 10, 11, 12, 17], "befor": 3, "begin": 34, "being": 3, "below": 3, "benefit": 29, "best": [16, 20], "between": [7, 9, 10, 13, 14, 15, 17], "bia": [9, 10, 12], "bilinear": 3, "bin": 29, "binari": [3, 29], "blcok": 4, "block": [3, 4, 7, 8, 13, 14, 15, 16, 17, 20], "block_cfg": [3, 20], "blocks_per_level": [3, 20], "bool": [9, 10, 12], "both": [7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 29], "bottleneck": 3, "bound": 22, "broadcast": 29, "bu": 29, "build": [3, 6, 16, 20, 29], "button": 34, "c": [8, 12], "calcul": 29, "call": [13, 14, 15, 16, 20], "can": [3, 4, 7, 12, 15, 16, 20, 26, 27, 29, 34], "cannot": 29, "capabl": 29, "care": [13, 14, 15, 16], "case": [3, 4, 29], "cast": 29, "cc": 29, "cfg": [3, 4, 16, 20], "chang": 3, "channel": [3, 4, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 20], "cheap": 12, "chebconv": [14, 15], "chollet": 10, "chosen": [13, 14, 15], "class": [3, 4, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 20], "click": 34, "cmap": 29, "code": [4, 16, 29], "coeffici": 29, "colab": 34, "combin": 7, "come": [25, 27, 28, 30, 31], "command": 29, "commerci": 29, "common": [3, 4], "common_runtim": 29, "compar": 10, "compens": 29, "compil": [27, 29], "complex64": 29, "compon": 16, "comput": [9, 10, 12, 13, 14, 15, 16, 29], "concat": [3, 7, 20], "concat_residu": 4, "concaten": 4, "cond": 7, "condit": 7, "config": 20, "configur": [2, 3, 4, 16, 20, 27], "configurablemodul": [16, 20], "connect": [3, 4, 16, 20], "consid": 29, "consist": [10, 16], "context": 11, "continu": 3, "contribut": 29, "conv": [13, 14, 15], "conv_config": [4, 16], "conv_kwarg": [13, 14, 15], "conv_typ": [13, 14, 15], "convert": 4, "convolut": [3, 4, 7, 9, 10, 11, 12, 13, 14, 15, 16, 17, 20], "convolv": [9, 10, 12], "coordin": 15, "core": 29, "cost": [9, 10, 12], "cpp": 26, "cpu": 29, "cpu_feature_guard": 29, "creat": [3, 4, 16, 20, 27, 29], "critic": 29, "cuda": 29, "cuda_gpu_executor": 29, "current": 29, "custom": [6, 18, 21, 29], "data": [3, 8, 13, 14, 20, 23, 29], "debian": 26, "decod": 15, "deep": [6, 10, 11, 18, 19, 21, 29], "def": 29, "default": [3, 4, 7, 8, 9, 10, 12, 13, 14, 15, 16, 17, 20, 22, 29], "defaultnorm": [13, 14, 15], "defin": [3, 4, 13, 14, 15, 16, 20, 29], "densiti": 29, "depend": [26, 29], "depth": [4, 13, 14, 16], "depthwis": [3, 4, 9, 10, 12], "depthwise_separ": [3, 4], "depthwiseseparableconv": [3, 4], "detail": 29, "determin": [3, 20], "dev": 26, "devic": 29, "dict": [4, 13, 14, 15, 16], "dictionari": [13, 14, 15], "differ": [3, 4, 29], "dilat": [3, 4, 9, 10], "dilated_bottleneck": 3, "dilated_conv": 4, "dimens": 29, "dimension": 20, "direct": 29, "directli": [3, 4, 29, 34], "directori": 26, "divid": 29, "do": [3, 20, 26, 27, 29], "docker": 27, "doe": 29, "doi": [7, 9, 10, 11, 12, 17, 20], "domain": 29, "download": 34, "downsampl": [3, 20], "dropout": [4, 16], "dropout_config": [4, 16], "dropout_posit": [4, 16], "dtype": 29, "due": 29, "dw_kernel_s": 12, "e": [3, 4, 16, 29], "each": [3, 4, 8, 9, 13, 14, 15, 16, 20, 27], "easiest": 27, "eca": [3, 4, 11], "effici": [3, 4, 9, 11, 17, 29], "efficientchannelattn": 3, "either": [3, 4, 12, 20, 29], "element": [9, 10], "embed": 9, "enabl": [27, 29], "encod": [3, 15, 20], "encoder_block_cfg": [3, 20], "encoder_channel": 15, "encoder_project": 15, "ensur": 27, "environ": [27, 29, 34], "equal": 9, "error": [26, 29], "especi": 9, "et": 9, "evalu": 29, "everi": [13, 14, 15, 16], "exampl": [3, 4, 16, 20], "excit": [3, 4, 7, 17], "expect": 3, "explicitli": [7, 17], "extern": 7, "f": 10, "fact": 29, "factor": [3, 20], "factori": [3, 4], "fals": 12, "fatal": 26, "featur": [3, 7, 12, 15, 17, 20, 29], "fft": 29, "fft_direct": 29, "fftw": 29, "field": 3, "figsiz": 29, "file": [3, 4, 26], "filter": [9, 13, 14, 15], "final": [4, 16, 29], "first": 4, "fix": [15, 26], "flag": 29, "flatiron": 29, "flatten_encoding_dim": 29, "flexibl": 16, "float": [22, 29], "float32": 29, "flow": 3, "fma": 29, "fninf": [12, 20], "focus": 11, "follow": [3, 4, 10, 16, 20, 27, 29], "format": 29, "former": [13, 14, 15, 16], "forward": [7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 20], "four": [9, 10, 12], "framework": 29, "free": 34, "frequenc": 29, "from": [3, 4, 12, 15, 29], "front": [12, 20], "function": [3, 4, 10, 13, 14, 15, 16, 20, 29], "fuse": [3, 20], "fusion_typ": [3, 20], "g": [3, 4, 7, 9, 16, 17], "gatconv": [13, 14, 15], "geforc": 29, "gener": [12, 27], "get": [26, 27], "ghost": [3, 4, 12], "ghost_depthwis": [3, 4], "ghost_depthwise_separ": [3, 4, 20], "ghostconv": 3, "ghostnet": 12, "given": 29, "global": 11, "gnu": 29, "googl": 34, "gpl": 29, "gpu": [29, 34], "gpu_devic": 29, "gradient": 3, "grai": 29, "graph": [8, 13, 14, 15], "graph_channel": 15, "graphic": 26, "grid": 29, "grid_shap": 29, "guo": 12, "h": [12, 20, 26], "ha": 27, "had": 29, "half": [3, 12, 20], "halfunet": [3, 12], "halfunetconfig": 20, "han": 12, "have": [4, 27, 29], "help": [3, 27], "hidden": [3, 7, 17, 20], "hidden_channel": [3, 20], "hook": [13, 14, 15, 16], "host": 34, "how": [3, 20, 29], "howard": 9, "howev": 29, "hu": [7, 11, 17], "i": [3, 4, 7, 8, 9, 10, 11, 12, 15, 16, 17, 20, 22, 26, 27, 29], "id": 29, "ie": 29, "ignor": [13, 14, 15, 16], "imag": [3, 4, 12, 16, 20, 27], "image2flow": 15, "image_shap": 29, "imageconvblockconfig": [3, 16, 20], "imathbox": 26, "implement": 29, "import": [11, 29], "improv": [3, 7, 11, 17], "imshow": 29, "in_channel": [9, 10, 12, 13, 14, 16, 20], "in_channl": [3, 20], "in_graph": [13, 14, 15], "includ": [4, 15, 26, 29], "increas": 3, "index": [8, 27], "indic": 8, "influenc": 7, "inform": [7, 11], "initi": [7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17], "input": [3, 4, 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 20, 22, 29], "instal": [26, 29], "instanc": [3, 4, 13, 14, 15, 16], "instancenorm": [4, 14, 15], "instancenorm2d": 8, "instead": [4, 13, 14, 15, 16], "institut": 29, "instruct": [27, 29], "int": [3, 4, 7, 9, 10, 11, 12, 13, 14, 15, 16, 17, 20], "intens": 3, "interdepend": [7, 17], "intern": [7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17], "issu": [26, 29], "its": [3, 16, 29], "j": [7, 12, 17, 20], "job": 29, "jun": [12, 20], "jupyt": 34, "k": 12, "keep": 4, "kei": [4, 16], "kernel": [3, 4, 9, 10, 12, 20, 29], "kernel_s": [9, 10, 12], "keyword": [13, 14, 15], "kspace": 29, "l": [7, 17], "laptop": 29, "last": [3, 4], "later": [3, 4], "latest": 27, "latter": [13, 14, 15, 16, 29], "layer": [3, 4, 20], "layerconfig": [3, 4, 16, 20], "learn": [6, 10, 18, 19, 21], "learnabl": [9, 10, 12], "least": 29, "len": 15, "let": 29, "level": [3, 15, 20], "li": 11, "libopenexr": 26, "librari": [3, 4, 26, 29], "licens": 29, "lie": 29, "linalg": 29, "linear": 15, "linearoperatornufft": 29, "linop_nufft": 29, "linux": 27, "list": [3, 4, 15, 16, 20], "ll": 29, "load": [3, 4, 5], "loaded_cfg": 4, "local": 29, "localhost": 29, "logan": 29, "lower": 22, "lowest": 3, "lu": [12, 20], "m": 29, "machin": 34, "maco": 27, "mai": 29, "make": 27, "manag": 27, "map": [3, 12], "mar": [7, 11, 12, 17], "match": 27, "math": 29, "matplotlib": 29, "max": 22, "maximum": 22, "maxpool": [3, 20], "mb": 29, "mean": [3, 4, 20], "medic": [12, 20], "meet": 27, "memori": 29, "meshdeformnet": 15, "method": [13, 14, 15, 16, 20, 29], "might": 29, "min": 22, "minimum": 22, "mit": 29, "mobil": 9, "mobilenet": 9, "mode": [3, 7, 20], "model": [2, 3, 6, 7, 16, 17, 18, 21, 23], "modif": 29, "modifi": [3, 4], "modul": [7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 20], "more": [3, 10, 12, 29], "most": [11, 27], "mri": [26, 29], "multi": 3, "multiclass_segment": 3, "multipl": 3, "must": 29, "n": [8, 29], "n_cond": 7, "n_decoder_level": 15, "n_deform_block": 15, "n_process_block": 15, "name": 29, "nand": 29, "nearest": 3, "necessari": 29, "need": [13, 14, 15, 16, 26, 27, 29], "neg": 29, "net": [3, 11, 12, 20], "network": [3, 7, 9, 10, 11, 12, 17, 20, 29], "neural": [7, 9, 10, 11, 12, 17, 29], "neuroinformat": [12, 20], "new": 27, "next": [3, 29], "nn": [7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17], "node": [8, 15, 29], "none": [3, 4, 8, 10, 13, 14, 15, 16, 20, 22], "nonuniform": 29, "norm": [13, 14, 15], "norm_config": [4, 16], "norm_kwarg": [13, 14], "norm_typ": [13, 14, 15], "normal": [3, 4, 8, 13, 14, 15, 16, 20, 22], "normalis": 8, "note": 29, "notebook": [27, 34], "now": 29, "num_downsampl": [3, 20], "numa": 29, "number": [3, 4, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 20, 29], "numer": 29, "nvidia": 29, "object": [13, 14, 15, 16], "obtain": 29, "off": 29, "older": 27, "one": [13, 14, 15, 16, 27, 29], "oneapi": 29, "onednn": 29, "onli": 27, "openexr": 26, "oper": [3, 7, 9, 10, 11, 12, 17, 20, 21], "optim": 29, "option": [13, 14, 15, 22], "order": 29, "origin": [12, 29], "other": 29, "our": 29, "out_activ": [3, 4, 15, 16, 20], "out_block_cfg": [3, 20], "out_channel": [3, 9, 10, 12, 15, 16, 20], "output": [3, 4, 7, 9, 10, 11, 12, 15, 16, 17, 20, 23, 29], "overridden": [13, 14, 15, 16], "p": 11, "packag": [27, 29], "pad": [4, 9, 10, 12], "paper": [12, 29], "paramet": [3, 4, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 20, 22], "particularli": 29, "pass": [3, 7, 9, 10, 11, 12, 13, 14, 15, 16, 17, 20], "pci": 29, "per": [3, 12, 20], "perform": [13, 14, 15, 16, 29], "phantom": 29, "pi": 29, "pip": [27, 29], "pixel": 3, "plan": 29, "platform": 29, "pleas": 29, "plot_imag": 29, "plt": 29, "point": 29, "pointwis": 10, "pool": [3, 20], "pool_spec": [3, 20], "posit": [4, 16], "power": [7, 10, 11, 17], "predefin": 19, "preprocess": 5, "preset": [3, 4], "prev_result": 15, "primari": 12, "print": 29, "prior": [15, 29], "probabl": 3, "process": [7, 20], "project": 15, "projection_channel": 15, "provid": 29, "pull": 27, "purchas": 29, "purpos": 29, "pyg": [13, 14, 15], "pyplot": 29, "python": 27, "python3": 29, "q": [11, 12, 29], "quickli": [3, 4], "radial": 29, "radial_dens": 29, "radial_trajectori": 29, "radian": 29, "rang": [22, 29], "rank": [3, 7, 9, 10, 11, 12, 16, 17, 20], "ratio": [7, 12, 17], "read": 29, "rebuild": 29, "recalibr": [7, 17], "receiv": 27, "recept": 3, "recip": [13, 14, 15, 16], "recommend": 27, "recon": [3, 4, 29], "reconstruct": [3, 4, 29], "recov": 29, "reduc": [9, 10, 12], "reduce_max": 29, "reduce_min": 29, "reduct": [7, 17], "refer": [7, 9, 10, 11, 12, 17, 20], "regardless": 29, "regist": [13, 14, 15, 16], "releas": [27, 29], "relu": [3, 4, 13, 14, 15, 16, 20], "remov": 4, "repeat": [3, 20], "replica": 29, "repositori": 29, "repres": [4, 16], "represent": [7, 10, 11, 17], "res_block_depth": 15, "res_depth": 15, "reshap": 29, "residu": [3, 4, 15, 16, 20], "residual_connect": [4, 16], "residual_typ": [4, 16], "residualconnectiontyp": [3, 4, 16, 20], "resolut": 3, "respons": [7, 17], "restart": 29, "result": 29, "retain": 10, "return": [7, 8, 9, 10, 11, 12, 13, 14, 15, 17, 29], "round": 29, "rtx": 29, "run": [13, 14, 15, 16, 34], "same": [3, 9, 10, 12, 15, 20], "sampl": 29, "save": [3, 4], "scale": [3, 20], "scriptmodul": [7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17], "se": [3, 4, 7, 17, 20], "see": [3, 4, 29], "segment": [3, 4, 12, 20], "select": 11, "separ": [3, 4, 9, 10, 12], "sequenc": 16, "set": [2, 3, 4, 29], "set_titl": 29, "setup": 34, "shape": [7, 8, 9, 10, 11, 12, 17, 29], "share": [7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17], "she": [12, 20], "shen": [7, 17], "shepp": 29, "shortli": 27, "should": [9, 13, 14, 15, 16, 27, 29], "show": 29, "side": [9, 10, 12], "signal": 29, "silent": [13, 14, 15, 16], "simpli": [27, 29], "simplifi": [12, 20], "sinc": [13, 14, 15, 16], "singl": [3, 4, 20], "single_block": 4, "single_class_segment": [3, 20], "single_conv": 4, "size": [3, 4, 9, 10, 12, 20, 29], "slightli": 29, "so": 29, "softmax": [4, 16], "soon": [25, 28, 30, 31], "sourc": [3, 4, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 20, 22], "space": [9, 10], "spatial": 29, "spatial_dim": [7, 9, 10, 11, 12, 17], "specif": [3, 20, 27], "specifi": [3, 4, 16, 20, 22, 29], "squeez": [3, 4, 7, 17], "squeezeexcit": 3, "stabil": 3, "stand": 29, "standard": [3, 4, 10, 20, 29], "start": 27, "state": [7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17], "stem": [3, 20], "stem_block_cfg": [3, 20], "str": [3, 4, 7, 9, 10, 12, 13, 14, 15, 16, 20], "stream_executor": 29, "stride": [9, 10, 12], "structur": 15, "subclass": [13, 14, 15, 16], "subplot": 29, "success": [13, 14, 15, 29], "suggest": 27, "sun": [7, 17], "superresolut": 3, "support": [16, 27, 29], "sure": 27, "sysf": 29, "take": [13, 14, 15, 16, 29], "target": [4, 16, 22, 27], "task": [3, 4, 19, 20, 29], "templat": 15, "template_edge_index": 15, "tensor": [7, 8, 9, 10, 11, 12, 15, 17, 22], "tensorflow": [26, 29], "tensorflow_mri": 29, "tf": 29, "tf_enable_onednn_opt": 29, "tfmri": 29, "them": [13, 14, 15, 16, 27, 29, 34], "thi": [3, 7, 9, 10, 11, 12, 13, 14, 15, 16, 17, 20, 26, 29, 34], "tian": 12, "tie": [12, 20], "togeth": 8, "torch": [8, 13, 14, 15], "train": [2, 3, 18], "trainabl": 4, "traj": 29, "trajectori": 29, "transform_typ": 29, "treat": 29, "trilinear": [3, 20], "true": [4, 9, 10, 12, 29], "try": 26, "tupl": [9, 10, 12], "turn": 29, "type": [3, 4, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 20, 29], "type_1": 29, "type_2": 29, "typic": 3, "u": [3, 12, 20, 27, 29], "ubuntu": 26, "under": 29, "unet": [12, 20], "up": [3, 4], "updat": [27, 29], "upgrad": 29, "upper": 22, "upsampl": [3, 20], "upsample_spec": [3, 20], "us": [3, 4, 7, 8, 9, 10, 11, 12, 16, 17, 20, 22], "usr": 29, "util": [5, 23, 29], "valu": [3, 4, 16, 22, 29], "variabl": 29, "variou": 19, "version": [27, 29], "via": 29, "view": [29, 34], "vision": 9, "visual": [23, 29], "vol": [12, 20], "voxel": 29, "w": 11, "wa": 29, "wai": [20, 27], "wang": [11, 12], "we": [27, 29], "websit": 34, "weight": 29, "what": 26, "when": [3, 4, 26, 27, 29], "where": [3, 8, 29], "whether": 29, "which": [3, 20, 26, 29], "while": [7, 10, 13, 14, 15, 16], "window": 27, "wise": [7, 11, 17], "within": [4, 13, 14, 15, 16], "without": [3, 4, 27], "work": 27, "written": 34, "wu": 11, "x": [7, 8, 9, 10, 11, 12, 16, 17, 20, 22], "xception": 10, "xu": [12, 20], "y": [12, 20], "yaml": [3, 4], "yet": 27, "you": [3, 4, 16, 20, 27, 29, 34], "your": [29, 34], "zero": 29, "zhu": 11, "zuo": 11}, "titles": ["IM2SIM API documentation", "im2sim", "im2sim.configs", "im2sim.configs.HalfUNetConfig", "im2sim.configs.ImageConvBlockConfig", "im2sim.data", "im2sim.layers", "im2sim.layers.ConditionedSqueezeExcite", "im2sim.layers.DefaultGraphNorm", "im2sim.layers.DepthwiseConv", "im2sim.layers.DepthwiseSeparableConv", "im2sim.layers.EfficientChannelAttn", "im2sim.layers.GhostConv", "im2sim.layers.GraphConvBlock", "im2sim.layers.GraphConvResBlock", "im2sim.layers.GraphResDecoderBlock", "im2sim.layers.ImageConvBlock", "im2sim.layers.SqueezeExcite", "im2sim.losses", "im2sim.models", "im2sim.models.HalfUNet", "im2sim.ops", "im2sim.ops.normtorange", "im2sim.plot", "IM2SIM guide", "Contributing", "Frequently Asked Questions", "Install TensorFlow MRI", "Linear algebra", "Non-uniform fast Fourier transform (NUFFT)", "Optimization", "MR image reconstruction", "IM2SIM 0.1.0", "IM2SIM 0.1.0", "TensorFlow MRI tutorials"], "titleterms": {"0": [32, 33], "1": [32, 33], "adjoint": 29, "algebra": 28, "api": 0, "ask": 26, "class": [1, 2, 5, 6, 18, 19, 21, 23], "colab": 27, "compat": 27, "conditionedsqueezeexcit": 7, "config": [2, 3, 4], "contribut": 25, "data": 5, "defaultgraphnorm": 8, "depthwiseconv": 9, "depthwiseseparableconv": 10, "document": 0, "download": 27, "efficientchannelattn": 11, "exampl": 29, "fast": 29, "forward": 29, "fourier": 29, "frequent": 26, "from": 27, "function": [1, 2, 5, 6, 18, 19, 21, 23], "ghostconv": 12, "googl": 27, "gpu": 27, "graphconvblock": 13, "graphconvresblock": 14, "graphresdecoderblock": 15, "guid": [24, 29], "halfunet": 20, "halfunetconfig": 3, "im2sim": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 32, 33], "imag": [29, 31], "imageconvblock": 16, "imageconvblockconfig": 4, "instal": 27, "k": 29, "layer": [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17], "linear": [28, 29], "loss": 18, "model": [19, 20], "modul": 1, "mr": 31, "mri": [27, 34], "non": 29, "normtorang": 22, "nufft": 29, "op": [21, 22], "oper": 29, "optim": 30, "plot": 23, "pypi": 27, "question": 26, "reconstruct": 31, "requir": 27, "run": 27, "set": 27, "space": 29, "squeezeexcit": 17, "system": 27, "tabl": 27, "tensorflow": [27, 34], "transform": 29, "tutori": 34, "uniform": 29, "up": 27, "us": [27, 29], "your": 27}}) \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/sitemap.xml b/tools/docs/_build/dirhtml/sitemap.xml new file mode 100644 index 0000000..44e9ecf --- /dev/null +++ b/tools/docs/_build/dirhtml/sitemap.xml @@ -0,0 +1,2 @@ + +https://mrphys.github.io/im2sim/api_docs/https://mrphys.github.io/im2sim/api_docs/im2sim/https://mrphys.github.io/im2sim/api_docs/im2sim/configs/https://mrphys.github.io/im2sim/api_docs/im2sim/configs/HalfUNetConfig/https://mrphys.github.io/im2sim/api_docs/im2sim/configs/ImageConvBlockConfig/https://mrphys.github.io/im2sim/api_docs/im2sim/data/https://mrphys.github.io/im2sim/api_docs/im2sim/layers/https://mrphys.github.io/im2sim/api_docs/im2sim/layers/ConditionedSqueezeExcite/https://mrphys.github.io/im2sim/api_docs/im2sim/layers/DefaultGraphNorm/https://mrphys.github.io/im2sim/api_docs/im2sim/layers/DepthwiseConv/https://mrphys.github.io/im2sim/api_docs/im2sim/layers/DepthwiseSeparableConv/https://mrphys.github.io/im2sim/api_docs/im2sim/layers/EfficientChannelAttn/https://mrphys.github.io/im2sim/api_docs/im2sim/layers/GhostConv/https://mrphys.github.io/im2sim/api_docs/im2sim/layers/GraphConvBlock/https://mrphys.github.io/im2sim/api_docs/im2sim/layers/GraphConvResBlock/https://mrphys.github.io/im2sim/api_docs/im2sim/layers/GraphResDecoderBlock/https://mrphys.github.io/im2sim/api_docs/im2sim/layers/ImageConvBlock/https://mrphys.github.io/im2sim/api_docs/im2sim/layers/SqueezeExcite/https://mrphys.github.io/im2sim/api_docs/im2sim/losses/https://mrphys.github.io/im2sim/api_docs/im2sim/models/https://mrphys.github.io/im2sim/api_docs/im2sim/models/HalfUNet/https://mrphys.github.io/im2sim/api_docs/im2sim/ops/https://mrphys.github.io/im2sim/api_docs/im2sim/ops/normtorange/https://mrphys.github.io/im2sim/api_docs/im2sim/plot/https://mrphys.github.io/im2sim/guide/https://mrphys.github.io/im2sim/guide/contribute/https://mrphys.github.io/im2sim/guide/faq/https://mrphys.github.io/im2sim/guide/install/https://mrphys.github.io/im2sim/guide/linalg/https://mrphys.github.io/im2sim/guide/nufft/https://mrphys.github.io/im2sim/guide/optim/https://mrphys.github.io/im2sim/guide/recon/https://mrphys.github.io/im2sim/https://mrphys.github.io/im2sim/templates/https://mrphys.github.io/im2sim/tutorials/https://mrphys.github.io/im2sim/genindex/https://mrphys.github.io/im2sim/py-modindex/https://mrphys.github.io/im2sim/search/ \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/templates/index.html b/tools/docs/_build/dirhtml/templates/index.html new file mode 100644 index 0000000..713fcb5 --- /dev/null +++ b/tools/docs/_build/dirhtml/templates/index.html @@ -0,0 +1,210 @@ + + + + + IM2SIM 0.1.0 - IM2SIM Documentation + + + + + + + + + + + + + + + +
+
+
+ + + + im2sim + +
+ +
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/tools/docs/_build/dirhtml/tutorials/index.html b/tools/docs/_build/dirhtml/tutorials/index.html new file mode 100644 index 0000000..7b32fa5 --- /dev/null +++ b/tools/docs/_build/dirhtml/tutorials/index.html @@ -0,0 +1,211 @@ + + + + + TensorFlow MRI tutorials - IM2SIM Documentation + + + + + + + + + + + + + + + +
+
+
+ + + + im2sim + +
+ +
+
+
+
+
+
+
+ + + + +
+
+
+
+
+ + +
+ +
+
+

TensorFlow MRI tutorials#

+

All TensorFlow MRI tutorials are written as Jupyter notebooks.

+

In addition to viewing them on this website, you can run them directly in +Google Colab, a hosted notebook environment with no setup and free access to +GPUs. Click on the Run in Colab button to begin.

+

Alternatively, you can also download the notebooks to run on your machine.

+
+ +
+
+
+
+
+
+
+ +
+
+
+
+ + + + + \ No newline at end of file diff --git a/tools/docs/_build/doctrees/api_docs.doctree b/tools/docs/_build/doctrees/api_docs.doctree new file mode 100644 index 0000000..e17ca3c Binary files /dev/null and b/tools/docs/_build/doctrees/api_docs.doctree differ diff --git a/tools/docs/_build/doctrees/api_docs/im2sim.doctree b/tools/docs/_build/doctrees/api_docs/im2sim.doctree new file mode 100644 index 0000000..793ffc7 Binary files /dev/null and b/tools/docs/_build/doctrees/api_docs/im2sim.doctree differ diff --git a/tools/docs/_build/doctrees/api_docs/im2sim/configs.doctree b/tools/docs/_build/doctrees/api_docs/im2sim/configs.doctree new file mode 100644 index 0000000..a9d1e5e Binary files /dev/null and b/tools/docs/_build/doctrees/api_docs/im2sim/configs.doctree differ diff --git a/tools/docs/_build/doctrees/api_docs/im2sim/configs/HalfUNetConfig.doctree b/tools/docs/_build/doctrees/api_docs/im2sim/configs/HalfUNetConfig.doctree new file mode 100644 index 0000000..15cca7e Binary files /dev/null and b/tools/docs/_build/doctrees/api_docs/im2sim/configs/HalfUNetConfig.doctree differ diff --git a/tools/docs/_build/doctrees/api_docs/im2sim/configs/ImageConvBlockConfig.doctree b/tools/docs/_build/doctrees/api_docs/im2sim/configs/ImageConvBlockConfig.doctree new file mode 100644 index 0000000..08223ce Binary files /dev/null and b/tools/docs/_build/doctrees/api_docs/im2sim/configs/ImageConvBlockConfig.doctree differ diff --git a/tools/docs/_build/doctrees/api_docs/im2sim/data.doctree b/tools/docs/_build/doctrees/api_docs/im2sim/data.doctree new file mode 100644 index 0000000..57e7f0a Binary files /dev/null and b/tools/docs/_build/doctrees/api_docs/im2sim/data.doctree differ diff --git a/tools/docs/_build/doctrees/api_docs/im2sim/layers.doctree b/tools/docs/_build/doctrees/api_docs/im2sim/layers.doctree new file mode 100644 index 0000000..6a2824c Binary files /dev/null and b/tools/docs/_build/doctrees/api_docs/im2sim/layers.doctree differ diff --git a/tools/docs/_build/doctrees/api_docs/im2sim/layers/ConditionedSqueezeExcite.doctree b/tools/docs/_build/doctrees/api_docs/im2sim/layers/ConditionedSqueezeExcite.doctree new file mode 100644 index 0000000..16e2a5c Binary files /dev/null and b/tools/docs/_build/doctrees/api_docs/im2sim/layers/ConditionedSqueezeExcite.doctree differ diff --git a/tools/docs/_build/doctrees/api_docs/im2sim/layers/DefaultGraphNorm.doctree b/tools/docs/_build/doctrees/api_docs/im2sim/layers/DefaultGraphNorm.doctree new file mode 100644 index 0000000..9ef3724 Binary files /dev/null and b/tools/docs/_build/doctrees/api_docs/im2sim/layers/DefaultGraphNorm.doctree differ diff --git a/tools/docs/_build/doctrees/api_docs/im2sim/layers/DepthwiseConv.doctree b/tools/docs/_build/doctrees/api_docs/im2sim/layers/DepthwiseConv.doctree new file mode 100644 index 0000000..8f4085a Binary files /dev/null and b/tools/docs/_build/doctrees/api_docs/im2sim/layers/DepthwiseConv.doctree differ diff --git a/tools/docs/_build/doctrees/api_docs/im2sim/layers/DepthwiseSeparableConv.doctree b/tools/docs/_build/doctrees/api_docs/im2sim/layers/DepthwiseSeparableConv.doctree new file mode 100644 index 0000000..d27ac13 Binary files /dev/null and b/tools/docs/_build/doctrees/api_docs/im2sim/layers/DepthwiseSeparableConv.doctree differ diff --git a/tools/docs/_build/doctrees/api_docs/im2sim/layers/EfficientChannelAttn.doctree b/tools/docs/_build/doctrees/api_docs/im2sim/layers/EfficientChannelAttn.doctree new file mode 100644 index 0000000..dd6b3b1 Binary files /dev/null and b/tools/docs/_build/doctrees/api_docs/im2sim/layers/EfficientChannelAttn.doctree differ diff --git a/tools/docs/_build/doctrees/api_docs/im2sim/layers/GhostConv.doctree b/tools/docs/_build/doctrees/api_docs/im2sim/layers/GhostConv.doctree new file mode 100644 index 0000000..fab945d Binary files /dev/null and b/tools/docs/_build/doctrees/api_docs/im2sim/layers/GhostConv.doctree differ diff --git a/tools/docs/_build/doctrees/api_docs/im2sim/layers/GraphConvBlock.doctree b/tools/docs/_build/doctrees/api_docs/im2sim/layers/GraphConvBlock.doctree new file mode 100644 index 0000000..7c52e96 Binary files /dev/null and b/tools/docs/_build/doctrees/api_docs/im2sim/layers/GraphConvBlock.doctree differ diff --git a/tools/docs/_build/doctrees/api_docs/im2sim/layers/GraphConvResBlock.doctree b/tools/docs/_build/doctrees/api_docs/im2sim/layers/GraphConvResBlock.doctree new file mode 100644 index 0000000..1ffc13f Binary files /dev/null and b/tools/docs/_build/doctrees/api_docs/im2sim/layers/GraphConvResBlock.doctree differ diff --git a/tools/docs/_build/doctrees/api_docs/im2sim/layers/GraphResDecoderBlock.doctree b/tools/docs/_build/doctrees/api_docs/im2sim/layers/GraphResDecoderBlock.doctree new file mode 100644 index 0000000..c73cdd1 Binary files /dev/null and b/tools/docs/_build/doctrees/api_docs/im2sim/layers/GraphResDecoderBlock.doctree differ diff --git a/tools/docs/_build/doctrees/api_docs/im2sim/layers/ImageConvBlock.doctree b/tools/docs/_build/doctrees/api_docs/im2sim/layers/ImageConvBlock.doctree new file mode 100644 index 0000000..f9957c9 Binary files /dev/null and b/tools/docs/_build/doctrees/api_docs/im2sim/layers/ImageConvBlock.doctree differ diff --git a/tools/docs/_build/doctrees/api_docs/im2sim/layers/SqueezeExcite.doctree b/tools/docs/_build/doctrees/api_docs/im2sim/layers/SqueezeExcite.doctree new file mode 100644 index 0000000..cae03a0 Binary files /dev/null and b/tools/docs/_build/doctrees/api_docs/im2sim/layers/SqueezeExcite.doctree differ diff --git a/tools/docs/_build/doctrees/api_docs/im2sim/losses.doctree b/tools/docs/_build/doctrees/api_docs/im2sim/losses.doctree new file mode 100644 index 0000000..6f3e571 Binary files /dev/null and b/tools/docs/_build/doctrees/api_docs/im2sim/losses.doctree differ diff --git a/tools/docs/_build/doctrees/api_docs/im2sim/models.doctree b/tools/docs/_build/doctrees/api_docs/im2sim/models.doctree new file mode 100644 index 0000000..3170dcf Binary files /dev/null and b/tools/docs/_build/doctrees/api_docs/im2sim/models.doctree differ diff --git a/tools/docs/_build/doctrees/api_docs/im2sim/models/HalfUNet.doctree b/tools/docs/_build/doctrees/api_docs/im2sim/models/HalfUNet.doctree new file mode 100644 index 0000000..b24ce1f Binary files /dev/null and b/tools/docs/_build/doctrees/api_docs/im2sim/models/HalfUNet.doctree differ diff --git a/tools/docs/_build/doctrees/api_docs/im2sim/ops.doctree b/tools/docs/_build/doctrees/api_docs/im2sim/ops.doctree new file mode 100644 index 0000000..f50f482 Binary files /dev/null and b/tools/docs/_build/doctrees/api_docs/im2sim/ops.doctree differ diff --git a/tools/docs/_build/doctrees/api_docs/im2sim/ops/normtorange.doctree b/tools/docs/_build/doctrees/api_docs/im2sim/ops/normtorange.doctree new file mode 100644 index 0000000..dec2b4e Binary files /dev/null and b/tools/docs/_build/doctrees/api_docs/im2sim/ops/normtorange.doctree differ diff --git a/tools/docs/_build/doctrees/api_docs/im2sim/plot.doctree b/tools/docs/_build/doctrees/api_docs/im2sim/plot.doctree new file mode 100644 index 0000000..36b6d29 Binary files /dev/null and b/tools/docs/_build/doctrees/api_docs/im2sim/plot.doctree differ diff --git a/tools/docs/_build/doctrees/environment.pickle b/tools/docs/_build/doctrees/environment.pickle new file mode 100644 index 0000000..3c9c524 Binary files /dev/null and b/tools/docs/_build/doctrees/environment.pickle differ diff --git a/tools/docs/_build/doctrees/guide.doctree b/tools/docs/_build/doctrees/guide.doctree new file mode 100644 index 0000000..c7b943f Binary files /dev/null and b/tools/docs/_build/doctrees/guide.doctree differ diff --git a/tools/docs/_build/doctrees/guide/contribute.doctree b/tools/docs/_build/doctrees/guide/contribute.doctree new file mode 100644 index 0000000..2427054 Binary files /dev/null and b/tools/docs/_build/doctrees/guide/contribute.doctree differ diff --git a/tools/docs/_build/doctrees/guide/faq.doctree b/tools/docs/_build/doctrees/guide/faq.doctree new file mode 100644 index 0000000..bf37c97 Binary files /dev/null and b/tools/docs/_build/doctrees/guide/faq.doctree differ diff --git a/tools/docs/_build/doctrees/guide/install.doctree b/tools/docs/_build/doctrees/guide/install.doctree new file mode 100644 index 0000000..47cc506 Binary files /dev/null and b/tools/docs/_build/doctrees/guide/install.doctree differ diff --git a/tools/docs/_build/doctrees/guide/linalg.doctree b/tools/docs/_build/doctrees/guide/linalg.doctree new file mode 100644 index 0000000..9940d2a Binary files /dev/null and b/tools/docs/_build/doctrees/guide/linalg.doctree differ diff --git a/tools/docs/_build/doctrees/guide/nufft.doctree b/tools/docs/_build/doctrees/guide/nufft.doctree new file mode 100644 index 0000000..63908d5 Binary files /dev/null and b/tools/docs/_build/doctrees/guide/nufft.doctree differ diff --git a/tools/docs/_build/doctrees/guide/optim.doctree b/tools/docs/_build/doctrees/guide/optim.doctree new file mode 100644 index 0000000..a22ccec Binary files /dev/null and b/tools/docs/_build/doctrees/guide/optim.doctree differ diff --git a/tools/docs/_build/doctrees/guide/recon.doctree b/tools/docs/_build/doctrees/guide/recon.doctree new file mode 100644 index 0000000..15dd0f0 Binary files /dev/null and b/tools/docs/_build/doctrees/guide/recon.doctree differ diff --git a/tools/docs/_build/doctrees/index.doctree b/tools/docs/_build/doctrees/index.doctree new file mode 100644 index 0000000..fb90f33 Binary files /dev/null and b/tools/docs/_build/doctrees/index.doctree differ diff --git a/tools/docs/_build/doctrees/templates/index.doctree b/tools/docs/_build/doctrees/templates/index.doctree new file mode 100644 index 0000000..1b0ac13 Binary files /dev/null and b/tools/docs/_build/doctrees/templates/index.doctree differ diff --git a/tools/docs/_build/doctrees/tutorials.doctree b/tools/docs/_build/doctrees/tutorials.doctree new file mode 100644 index 0000000..ae8037a Binary files /dev/null and b/tools/docs/_build/doctrees/tutorials.doctree differ diff --git a/tools/docs/_build/jupyter_execute/ed739320bcec44e952e2c94e8c30931e2f1565301f14f6e2722599cc273859a5.png b/tools/docs/_build/jupyter_execute/ed739320bcec44e952e2c94e8c30931e2f1565301f14f6e2722599cc273859a5.png new file mode 100644 index 0000000..b74b45d Binary files /dev/null and b/tools/docs/_build/jupyter_execute/ed739320bcec44e952e2c94e8c30931e2f1565301f14f6e2722599cc273859a5.png differ diff --git a/tools/docs/_templates/configs/class.rst b/tools/docs/_templates/configs/class.rst new file mode 100644 index 0000000..b4ba172 --- /dev/null +++ b/tools/docs/_templates/configs/class.rst @@ -0,0 +1,7 @@ +im2sim.configs.{{ objname | escape | underline }}=============== + +.. currentmodule:: {{ module }} + +.. auto{{ objtype }}:: {{ objname }} + :members: + :show-inheritance: diff --git a/tools/docs/_templates/configs/function.rst b/tools/docs/_templates/configs/function.rst new file mode 100644 index 0000000..9eb5080 --- /dev/null +++ b/tools/docs/_templates/configs/function.rst @@ -0,0 +1,5 @@ +im2sim.configs.{{ objname | escape | underline }}=============== + +.. currentmodule:: {{ module }} + +.. auto{{ objtype }}:: {{ objname }} diff --git a/tools/docs/_templates/data/class.rst b/tools/docs/_templates/data/class.rst new file mode 100644 index 0000000..46628a0 --- /dev/null +++ b/tools/docs/_templates/data/class.rst @@ -0,0 +1,7 @@ +im2sim.data.{{ objname | escape | underline }}============ + +.. currentmodule:: {{ module }} + +.. auto{{ objtype }}:: {{ objname }} + :members: + :show-inheritance: diff --git a/tools/docs/_templates/data/function.rst b/tools/docs/_templates/data/function.rst new file mode 100644 index 0000000..d79d52b --- /dev/null +++ b/tools/docs/_templates/data/function.rst @@ -0,0 +1,5 @@ +im2sim.data.{{ objname | escape | underline }}============ + +.. currentmodule:: {{ module }} + +.. auto{{ objtype }}:: {{ objname }} diff --git a/tools/docs/_templates/layers/class.rst b/tools/docs/_templates/layers/class.rst new file mode 100644 index 0000000..64e6496 --- /dev/null +++ b/tools/docs/_templates/layers/class.rst @@ -0,0 +1,7 @@ +im2sim.layers.{{ objname | escape | underline }}============== + +.. currentmodule:: {{ module }} + +.. auto{{ objtype }}:: {{ objname }} + :members: + :show-inheritance: diff --git a/tools/docs/_templates/layers/function.rst b/tools/docs/_templates/layers/function.rst new file mode 100644 index 0000000..7b5ea39 --- /dev/null +++ b/tools/docs/_templates/layers/function.rst @@ -0,0 +1,5 @@ +im2sim.layers.{{ objname | escape | underline }}============== + +.. currentmodule:: {{ module }} + +.. auto{{ objtype }}:: {{ objname }} diff --git a/tools/docs/_templates/losses/class.rst b/tools/docs/_templates/losses/class.rst new file mode 100644 index 0000000..5fc24e1 --- /dev/null +++ b/tools/docs/_templates/losses/class.rst @@ -0,0 +1,7 @@ +im2sim.losses.{{ objname | escape | underline }}============== + +.. currentmodule:: {{ module }} + +.. auto{{ objtype }}:: {{ objname }} + :members: + :show-inheritance: diff --git a/tools/docs/_templates/losses/function.rst b/tools/docs/_templates/losses/function.rst new file mode 100644 index 0000000..d9de777 --- /dev/null +++ b/tools/docs/_templates/losses/function.rst @@ -0,0 +1,5 @@ +im2sim.losses.{{ objname | escape | underline }}============== + +.. currentmodule:: {{ module }} + +.. auto{{ objtype }}:: {{ objname }} diff --git a/tools/docs/_templates/models/class.rst b/tools/docs/_templates/models/class.rst new file mode 100644 index 0000000..cc42e22 --- /dev/null +++ b/tools/docs/_templates/models/class.rst @@ -0,0 +1,7 @@ +im2sim.models.{{ objname | escape | underline }}============== + +.. currentmodule:: {{ module }} + +.. auto{{ objtype }}:: {{ objname }} + :members: + :show-inheritance: diff --git a/tools/docs/_templates/models/function.rst b/tools/docs/_templates/models/function.rst new file mode 100644 index 0000000..558e06d --- /dev/null +++ b/tools/docs/_templates/models/function.rst @@ -0,0 +1,5 @@ +im2sim.models.{{ objname | escape | underline }}============== + +.. currentmodule:: {{ module }} + +.. auto{{ objtype }}:: {{ objname }} diff --git a/tools/docs/_templates/ops/class.rst b/tools/docs/_templates/ops/class.rst new file mode 100644 index 0000000..437f7e2 --- /dev/null +++ b/tools/docs/_templates/ops/class.rst @@ -0,0 +1,7 @@ +im2sim.ops.{{ objname | escape | underline }}=========== + +.. currentmodule:: {{ module }} + +.. auto{{ objtype }}:: {{ objname }} + :members: + :show-inheritance: diff --git a/tools/docs/_templates/ops/function.rst b/tools/docs/_templates/ops/function.rst new file mode 100644 index 0000000..8e12b17 --- /dev/null +++ b/tools/docs/_templates/ops/function.rst @@ -0,0 +1,5 @@ +im2sim.ops.{{ objname | escape | underline }}=========== + +.. currentmodule:: {{ module }} + +.. auto{{ objtype }}:: {{ objname }} diff --git a/tools/docs/_templates/plot/class.rst b/tools/docs/_templates/plot/class.rst new file mode 100644 index 0000000..6dc7556 --- /dev/null +++ b/tools/docs/_templates/plot/class.rst @@ -0,0 +1,7 @@ +im2sim.plot.{{ objname | escape | underline }}============ + +.. currentmodule:: {{ module }} + +.. auto{{ objtype }}:: {{ objname }} + :members: + :show-inheritance: diff --git a/tools/docs/_templates/plot/function.rst b/tools/docs/_templates/plot/function.rst new file mode 100644 index 0000000..819ca00 --- /dev/null +++ b/tools/docs/_templates/plot/function.rst @@ -0,0 +1,5 @@ +im2sim.plot.{{ objname | escape | underline }}============ + +.. currentmodule:: {{ module }} + +.. auto{{ objtype }}:: {{ objname }} diff --git a/tools/docs/api_docs.rst b/tools/docs/api_docs.rst new file mode 100644 index 0000000..c4fd190 --- /dev/null +++ b/tools/docs/api_docs.rst @@ -0,0 +1,2 @@ +IM2SIM API documentation +================================ diff --git a/tools/docs/api_docs/im2sim.rst b/tools/docs/api_docs/im2sim.rst new file mode 100644 index 0000000..d2e1761 --- /dev/null +++ b/tools/docs/api_docs/im2sim.rst @@ -0,0 +1,37 @@ +im2sim +===== + +.. automodule:: im2sim + +Modules +------- + +.. autosummary:: + :nosignatures: + + configs + data + layers + losses + models + ops + plot + + +Classes +------- + +.. autosummary:: + :toctree: im2sim + :template: ops/class.rst + :nosignatures: + + + +Functions +--------- + +.. autosummary:: + :toctree: im2sim + :template: ops/function.rst + :nosignatures: diff --git a/tools/docs/api_docs/im2sim/configs.rst b/tools/docs/api_docs/im2sim/configs.rst new file mode 100644 index 0000000..7bde386 --- /dev/null +++ b/tools/docs/api_docs/im2sim/configs.rst @@ -0,0 +1,25 @@ +im2sim.configs +============= + +.. automodule:: im2sim.configs + +Classes +------- + +.. autosummary:: + :toctree: configs + :template: configs/class.rst + :nosignatures: + + HalfUNetConfig + ImageConvBlockConfig + +Functions +--------- + +.. autosummary:: + :toctree: configs + :template: configs/function.rst + :nosignatures: + + diff --git a/tools/docs/api_docs/im2sim/configs/HalfUNetConfig.rst b/tools/docs/api_docs/im2sim/configs/HalfUNetConfig.rst new file mode 100644 index 0000000..93a1af0 --- /dev/null +++ b/tools/docs/api_docs/im2sim/configs/HalfUNetConfig.rst @@ -0,0 +1,8 @@ +im2sim.configs.HalfUNetConfig +============================= + +.. currentmodule:: im2sim.configs + +.. autoclass:: HalfUNetConfig + :members: + :show-inheritance: \ No newline at end of file diff --git a/tools/docs/api_docs/im2sim/configs/ImageConvBlockConfig.rst b/tools/docs/api_docs/im2sim/configs/ImageConvBlockConfig.rst new file mode 100644 index 0000000..52efb4c --- /dev/null +++ b/tools/docs/api_docs/im2sim/configs/ImageConvBlockConfig.rst @@ -0,0 +1,8 @@ +im2sim.configs.ImageConvBlockConfig +=================================== + +.. currentmodule:: im2sim.configs + +.. autoclass:: ImageConvBlockConfig + :members: + :show-inheritance: \ No newline at end of file diff --git a/tools/docs/api_docs/im2sim/data.rst b/tools/docs/api_docs/im2sim/data.rst new file mode 100644 index 0000000..2500ac8 --- /dev/null +++ b/tools/docs/api_docs/im2sim/data.rst @@ -0,0 +1,24 @@ +im2sim.data +========== + +.. automodule:: im2sim.data + +Classes +------- + +.. autosummary:: + :toctree: data + :template: data/class.rst + :nosignatures: + + + +Functions +--------- + +.. autosummary:: + :toctree: data + :template: data/function.rst + :nosignatures: + + diff --git a/tools/docs/api_docs/im2sim/layers.rst b/tools/docs/api_docs/im2sim/layers.rst new file mode 100644 index 0000000..1ae1873 --- /dev/null +++ b/tools/docs/api_docs/im2sim/layers.rst @@ -0,0 +1,34 @@ +im2sim.layers +============ + +.. automodule:: im2sim.layers + +Classes +------- + +.. autosummary:: + :toctree: layers + :template: layers/class.rst + :nosignatures: + + ConditionedSqueezeExcite + DefaultGraphNorm + DepthwiseConv + DepthwiseSeparableConv + EfficientChannelAttn + GhostConv + GraphConvBlock + GraphConvResBlock + GraphResDecoderBlock + ImageConvBlock + SqueezeExcite + +Functions +--------- + +.. autosummary:: + :toctree: layers + :template: layers/function.rst + :nosignatures: + + diff --git a/tools/docs/api_docs/im2sim/layers/ConditionedSqueezeExcite.rst b/tools/docs/api_docs/im2sim/layers/ConditionedSqueezeExcite.rst new file mode 100644 index 0000000..2f8cd36 --- /dev/null +++ b/tools/docs/api_docs/im2sim/layers/ConditionedSqueezeExcite.rst @@ -0,0 +1,8 @@ +im2sim.layers.ConditionedSqueezeExcite +====================================== + +.. currentmodule:: im2sim.layers + +.. autoclass:: ConditionedSqueezeExcite + :members: + :show-inheritance: \ No newline at end of file diff --git a/tools/docs/api_docs/im2sim/layers/DefaultGraphNorm.rst b/tools/docs/api_docs/im2sim/layers/DefaultGraphNorm.rst new file mode 100644 index 0000000..6827f3d --- /dev/null +++ b/tools/docs/api_docs/im2sim/layers/DefaultGraphNorm.rst @@ -0,0 +1,8 @@ +im2sim.layers.DefaultGraphNorm +============================== + +.. currentmodule:: im2sim.layers + +.. autoclass:: DefaultGraphNorm + :members: + :show-inheritance: \ No newline at end of file diff --git a/tools/docs/api_docs/im2sim/layers/DepthwiseConv.rst b/tools/docs/api_docs/im2sim/layers/DepthwiseConv.rst new file mode 100644 index 0000000..c22f48f --- /dev/null +++ b/tools/docs/api_docs/im2sim/layers/DepthwiseConv.rst @@ -0,0 +1,8 @@ +im2sim.layers.DepthwiseConv +=========================== + +.. currentmodule:: im2sim.layers + +.. autoclass:: DepthwiseConv + :members: + :show-inheritance: \ No newline at end of file diff --git a/tools/docs/api_docs/im2sim/layers/DepthwiseSeparableConv.rst b/tools/docs/api_docs/im2sim/layers/DepthwiseSeparableConv.rst new file mode 100644 index 0000000..c00bb8d --- /dev/null +++ b/tools/docs/api_docs/im2sim/layers/DepthwiseSeparableConv.rst @@ -0,0 +1,8 @@ +im2sim.layers.DepthwiseSeparableConv +==================================== + +.. currentmodule:: im2sim.layers + +.. autoclass:: DepthwiseSeparableConv + :members: + :show-inheritance: \ No newline at end of file diff --git a/tools/docs/api_docs/im2sim/layers/EfficientChannelAttn.rst b/tools/docs/api_docs/im2sim/layers/EfficientChannelAttn.rst new file mode 100644 index 0000000..19563af --- /dev/null +++ b/tools/docs/api_docs/im2sim/layers/EfficientChannelAttn.rst @@ -0,0 +1,8 @@ +im2sim.layers.EfficientChannelAttn +================================== + +.. currentmodule:: im2sim.layers + +.. autoclass:: EfficientChannelAttn + :members: + :show-inheritance: \ No newline at end of file diff --git a/tools/docs/api_docs/im2sim/layers/GhostConv.rst b/tools/docs/api_docs/im2sim/layers/GhostConv.rst new file mode 100644 index 0000000..8ead535 --- /dev/null +++ b/tools/docs/api_docs/im2sim/layers/GhostConv.rst @@ -0,0 +1,8 @@ +im2sim.layers.GhostConv +======================= + +.. currentmodule:: im2sim.layers + +.. autoclass:: GhostConv + :members: + :show-inheritance: \ No newline at end of file diff --git a/tools/docs/api_docs/im2sim/layers/GraphConvBlock.rst b/tools/docs/api_docs/im2sim/layers/GraphConvBlock.rst new file mode 100644 index 0000000..81846f5 --- /dev/null +++ b/tools/docs/api_docs/im2sim/layers/GraphConvBlock.rst @@ -0,0 +1,8 @@ +im2sim.layers.GraphConvBlock +============================ + +.. currentmodule:: im2sim.layers + +.. autoclass:: GraphConvBlock + :members: + :show-inheritance: \ No newline at end of file diff --git a/tools/docs/api_docs/im2sim/layers/GraphConvResBlock.rst b/tools/docs/api_docs/im2sim/layers/GraphConvResBlock.rst new file mode 100644 index 0000000..5d0e180 --- /dev/null +++ b/tools/docs/api_docs/im2sim/layers/GraphConvResBlock.rst @@ -0,0 +1,8 @@ +im2sim.layers.GraphConvResBlock +=============================== + +.. currentmodule:: im2sim.layers + +.. autoclass:: GraphConvResBlock + :members: + :show-inheritance: \ No newline at end of file diff --git a/tools/docs/api_docs/im2sim/layers/GraphResDecoderBlock.rst b/tools/docs/api_docs/im2sim/layers/GraphResDecoderBlock.rst new file mode 100644 index 0000000..ffaadf0 --- /dev/null +++ b/tools/docs/api_docs/im2sim/layers/GraphResDecoderBlock.rst @@ -0,0 +1,8 @@ +im2sim.layers.GraphResDecoderBlock +================================== + +.. currentmodule:: im2sim.layers + +.. autoclass:: GraphResDecoderBlock + :members: + :show-inheritance: \ No newline at end of file diff --git a/tools/docs/api_docs/im2sim/layers/ImageConvBlock.rst b/tools/docs/api_docs/im2sim/layers/ImageConvBlock.rst new file mode 100644 index 0000000..4bebbe3 --- /dev/null +++ b/tools/docs/api_docs/im2sim/layers/ImageConvBlock.rst @@ -0,0 +1,8 @@ +im2sim.layers.ImageConvBlock +============================ + +.. currentmodule:: im2sim.layers + +.. autoclass:: ImageConvBlock + :members: + :show-inheritance: \ No newline at end of file diff --git a/tools/docs/api_docs/im2sim/layers/SqueezeExcite.rst b/tools/docs/api_docs/im2sim/layers/SqueezeExcite.rst new file mode 100644 index 0000000..a67e99b --- /dev/null +++ b/tools/docs/api_docs/im2sim/layers/SqueezeExcite.rst @@ -0,0 +1,8 @@ +im2sim.layers.SqueezeExcite +=========================== + +.. currentmodule:: im2sim.layers + +.. autoclass:: SqueezeExcite + :members: + :show-inheritance: \ No newline at end of file diff --git a/tools/docs/api_docs/im2sim/losses.rst b/tools/docs/api_docs/im2sim/losses.rst new file mode 100644 index 0000000..a0ad274 --- /dev/null +++ b/tools/docs/api_docs/im2sim/losses.rst @@ -0,0 +1,24 @@ +im2sim.losses +============ + +.. automodule:: im2sim.losses + +Classes +------- + +.. autosummary:: + :toctree: losses + :template: losses/class.rst + :nosignatures: + + + +Functions +--------- + +.. autosummary:: + :toctree: losses + :template: losses/function.rst + :nosignatures: + + diff --git a/tools/docs/api_docs/im2sim/models.rst b/tools/docs/api_docs/im2sim/models.rst new file mode 100644 index 0000000..f7e36b9 --- /dev/null +++ b/tools/docs/api_docs/im2sim/models.rst @@ -0,0 +1,24 @@ +im2sim.models +============ + +.. automodule:: im2sim.models + +Classes +------- + +.. autosummary:: + :toctree: models + :template: models/class.rst + :nosignatures: + + HalfUNet + +Functions +--------- + +.. autosummary:: + :toctree: models + :template: models/function.rst + :nosignatures: + + diff --git a/tools/docs/api_docs/im2sim/models/HalfUNet.rst b/tools/docs/api_docs/im2sim/models/HalfUNet.rst new file mode 100644 index 0000000..ef0a032 --- /dev/null +++ b/tools/docs/api_docs/im2sim/models/HalfUNet.rst @@ -0,0 +1,8 @@ +im2sim.models.HalfUNet +====================== + +.. currentmodule:: im2sim.models + +.. autoclass:: HalfUNet + :members: + :show-inheritance: \ No newline at end of file diff --git a/tools/docs/api_docs/im2sim/ops.rst b/tools/docs/api_docs/im2sim/ops.rst new file mode 100644 index 0000000..fa21c8d --- /dev/null +++ b/tools/docs/api_docs/im2sim/ops.rst @@ -0,0 +1,24 @@ +im2sim.ops +========= + +.. automodule:: im2sim.ops + +Classes +------- + +.. autosummary:: + :toctree: ops + :template: ops/class.rst + :nosignatures: + + + +Functions +--------- + +.. autosummary:: + :toctree: ops + :template: ops/function.rst + :nosignatures: + + normtorange diff --git a/tools/docs/api_docs/im2sim/ops/normtorange.rst b/tools/docs/api_docs/im2sim/ops/normtorange.rst new file mode 100644 index 0000000..62e9378 --- /dev/null +++ b/tools/docs/api_docs/im2sim/ops/normtorange.rst @@ -0,0 +1,6 @@ +im2sim.ops.normtorange +====================== + +.. currentmodule:: im2sim.ops + +.. autofunction:: normtorange \ No newline at end of file diff --git a/tools/docs/api_docs/im2sim/plot.rst b/tools/docs/api_docs/im2sim/plot.rst new file mode 100644 index 0000000..ebd4801 --- /dev/null +++ b/tools/docs/api_docs/im2sim/plot.rst @@ -0,0 +1,24 @@ +im2sim.plot +========== + +.. automodule:: im2sim.plot + +Classes +------- + +.. autosummary:: + :toctree: plot + :template: plot/class.rst + :nosignatures: + + + +Functions +--------- + +.. autosummary:: + :toctree: plot + :template: plot/function.rst + :nosignatures: + + diff --git a/tools/docs/conf.py b/tools/docs/conf.py new file mode 100644 index 0000000..eae56de --- /dev/null +++ b/tools/docs/conf.py @@ -0,0 +1,401 @@ +# Copyright 2026 University College London. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Configuration file for the Sphinx documentation builder. + +This file only contains a selection of the most common options. For a full +list see the documentation: +https://www.sphinx-doc.org/en/master/usage/configuration.html +""" + +from os import path +import inspect +import operator +import packaging.version +import re +import sys +import types + +import conf_helper + + + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +sys.path.insert(0, path.abspath('../..')) + + +# -- Project information ----------------------------------------------------- + +ROOT = path.abspath(path.join(path.dirname(__file__), '../..')) + +ABOUT = {} +with open(path.join(ROOT, "im2sim/__about__.py")) as f: + exec(f.read(), ABOUT) +_version = packaging.version.Version(ABOUT['__version__']) + +project = ABOUT['__title__'] +copyright = ABOUT['__copyright__'] +author = ABOUT['__author__'] +release = _version.public +version = '.'.join(map(str, (_version.major, _version.minor))) + + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.napoleon', + 'sphinx.ext.autosummary', + "sphinx.ext.intersphinx", + 'sphinx.ext.linkcode', + 'sphinx.ext.autosectionlabel', + 'myst_nb', + 'sphinx_sitemap' +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +autosectionlabel_prefix_document = True + +autodoc_typehints = "description" + +# Make Sphinx resolve types automatically +python_use_unqualified_type_names = True + +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), + "torch": ("https://pytorch.org/docs/stable/", None), + "numpy": ("https://numpy.org/doc/stable/", None), +} + + +# Add the reference to the bibliography file. + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# Do not add full qualification to objects' signatures. +add_module_names = False + +# For classes, list the documentation of both the class and the `__init__` +# method. +autoclass_content = 'both' + +# -- Options for HTML output ------------------------------------------------- + +html_title = 'IM2SIM Documentation' +html_logo = '../assets/im2sim_logo.png' +html_favicon = '../assets/im2sim_logo.png' + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'shibuya' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['../assets'] + +# https://sphinx-book-theme.readthedocs.io/en/latest/tutorials/get-started.html +html_theme_options = { + 'repository_url': 'https://github.com/mrphys/im2sim', + 'use_repository_button': True, + 'logo_only': True, + 'launch_buttons': { + 'colab_url': "https://colab.research.google.com/" + }, + 'path_to_docs': 'docs' +} + +html_css_files = [ + 'https://fonts.googleapis.com/css?family=Roboto|Roboto+Mono', +] + +# Additional files to copy to output directory. +html_extra_path = ['robots.txt'] + +# For sitemap generation. +html_baseurl = 'https://mrphys.github.io/im2sim/' +sitemap_url_scheme = '{link}' + +# For autosummary generation. +autosummary_filename_map = conf_helper.AutosummaryFilenameMap() + +# -- Options for MyST ---------------------------------------------------------- +# https://myst-nb.readthedocs.io/en/latest/authoring/jupyter-notebooks.html +myst_enable_extensions = [ + "amsmath", + "colon_fence", + "deflist", + "dollarmath", + "html_image", +] + +# https://myst-nb.readthedocs.io/en/latest/authoring/basics.html +source_suffix = [ + '.rst', + '.md', + '.ipynb' +] + +# Do not execute notebooks. +# https://myst-nb.readthedocs.io/en/latest/computation/execute.html +nb_execution_mode = "off" + + +import im2sim + + +def linkcode_resolve(domain, info): + """Find the GitHub URL where an object is defined. + + Args: + domain: The language domain. This is always `py`. + info: A `dict` with keys `module` and `fullname`. + + Returns: + The GitHub URL to the object, or `None` if not relevant. + """ + + # Obtain fully-qualified name of object. + qualname = info['module'] + '.' + info['fullname'] + # Remove the `im2sim` bit. + qualname = qualname.split('.', maxsplit=1)[-1] + + # Get the object. + # obj = operator.attrgetter(qualname)(im2sim) + try: + obj = operator.attrgetter(qualname)(im2sim) + except AttributeError: + return None + # We only add links to classes (type `type`) and functions + # (type `types.FunctionType`). + if not isinstance(obj, (type, types.FunctionType)): + return None + + # Get the file name of the current object. + file = inspect.getsourcefile(obj) + # If no file, we're done. This happens for C++ ops. + if file is None: + return None + # When using TF's deprecation decorators, `getsourcefile` returns the + # `deprecation.py` file where the decorators are defined instead of the + # file where the object is defined. This should probably be fixed on the + # decorators themselves. For now, we just don't add the link for deprecated + # objects. + if 'deprecation' in file: + return None + # Crop anything before `im2sim\src`. This path is system + # dependent and we don't care about it. + index = file.index('im2sim/src') + file = file[index:] + + # Base URL. + url = 'https://github.com/mrphys/im2sim' + # Add version blob. + url += '/blob/v' + release + # Add file. + url += '/' + file + + # Try to add line numbers. This will not work when the class is defined + # dynamically. In that case we point to the file, but no line number. + try: + lines, start = inspect.getsourcelines(obj) + stop = start + len(lines) - 1 + except OSError: + # Could not get source lines. + return url + + # Add line numbers. + url += '#L' + str(start) + '-L' + str(stop) + + return url + + +# -- Hyperlinks -------------------------------------------------------------- +# Common types and constants in the API docs are enriched with hyperlinks to +# their corresponding docs. + +# The following dictionary specifies type names and the corresponding links. +# The link is only added if the name has inline code format, e.g. ``foo``. +COMMON_TYPES_LINKS = { + # Python standard types. + 'int': 'https://docs.python.org/3/library/stdtypes.html#numeric-types-int-float-complex', + 'float': 'https://docs.python.org/3/library/stdtypes.html#numeric-types-int-float-complex', + 'complex': 'https://docs.python.org/3/library/stdtypes.html#numeric-types-int-float-complex', + 'str': 'https://docs.python.org/3/library/stdtypes.html#text-sequence-type-str', + 'boolean': 'https://docs.python.org/3/library/stdtypes.html#boolean-values', + 'tuple': 'https://docs.python.org/3/library/stdtypes.html#tuples', + 'list': 'https://docs.python.org/3/library/stdtypes.html#lists', + 'dict': 'https://docs.python.org/3/library/stdtypes.html#mapping-types-dict', + 'namedtuple': 'https://docs.python.org/3/library/collections.html#namedtuple-factory-function-for-tuples-with-named-fields', + 'callable': 'https://docs.python.org/3/library/functions.html#callable', + 'dataclass': 'https://docs.python.org/3/library/dataclasses.html', + # Python constants. + 'False': 'https://docs.python.org/3/library/constants.html#False', + 'True': 'https://docs.python.org/3/library/constants.html#True', + 'None': 'https://docs.python.org/3/library/constants.html#None', + # NumPy types. + 'np.ndarray': 'https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html', + 'np.inf': 'https://numpy.org/doc/stable/reference/constants.html#numpy.inf', + 'np.nan': 'https://numpy.org/doc/stable/reference/constants.html#numpy.nan', + # PyTorch types. + 'torch.Tensor': 'https://pytorch.org/docs/stable/tensors.html', + 'torch.Size': 'https://pytorch.org/docs/stable/size.html', + 'torch.dtype': 'https://pytorch.org/docs/stable/tensor_attributes.html#torch.dtype', + 'torch.device': 'https://pytorch.org/docs/stable/tensor_attributes.html#torch.device', + # TorchGeometric types. + 'torch_geometric.data.Data': 'https://pytorch-geometric.readthedocs.io/en/latest/modules/data.html#torch_geometric.data.Data', +} + +IM2SIM_OBJECTS_PATTERN = re.compile( + r"``(?Pim2sim\.[a-zA-Z0-9_.]+)``" +) + +COMMON_TYPES_PATTERNS = { + k: re.compile(rf"``{k}``")for k in COMMON_TYPES_LINKS} + +COMMON_TYPES_REPLACEMENTS = { + k: rf"`{k} <{v}>`_" for k, v in COMMON_TYPES_LINKS.items()} + +CODE_LETTER_PATTERN = re.compile(r"``(?P\w+)``(?P[a-zA-Z])") +CODE_LETTER_REPL = r"``\g``\ \g" + +LINK_PATTERN = re.compile(r"``(?P[\w\.]+)``_") +LINK_REPL = r"`\g`_" + +import inspect + + +def process_docstring_text(text): + """Process a docstring and convert it to Sphinx RST.""" + # Replace Note: and Warning: by RST equivalents. + rst_lines = [] + admonition_lines = None + + for line in text.splitlines(): + if admonition_lines is None: + # We are not in an admonition right now. Check if this line will start + # one. + if (line.strip().startswith('Warning:') or + line.strip().startswith('Note:')): + label_position = line.index(':') + admonition_type = line[:label_position].strip().lower() + admonition_content = line[label_position + 1:].strip() + leading_whitespace = ' ' * (len(line) - len(line.lstrip())) + extra_indentation = ' ' + + admonition_lines = [ + f"{leading_whitespace}.. {admonition_type}::", + leading_whitespace + extra_indentation + admonition_content, + ] + else: + rst_lines.append(line) + else: + # Check if this is the end of the admonition. + if line.strip() == '': + rst_lines.extend(admonition_lines) + admonition_lines = None + else: + admonition_lines.append(extra_indentation + line) + + # If we reached the end and are still in an admonition, add it. + if admonition_lines is not None: + rst_lines.extend(admonition_lines) + + # Replace markdown literal markers (`) by ReST literal markers (``). + text = '\n'.join(rst_lines) + text = text.replace('`', '``') + text = text.replace(':math:``', ':math:`') + + # Correct inline code followed by word characters. + text = CODE_LETTER_PATTERN.sub(CODE_LETTER_REPL, text) + + # Add links to common types. + for k in COMMON_TYPES_LINKS: + text = COMMON_TYPES_PATTERNS[k].sub( + COMMON_TYPES_REPLACEMENTS[k], + text, + ) + + + # Add links to im2sim objects. + for match in IM2SIM_OBJECTS_PATTERN.finditer(text): + object_name = match.group('name') + url = get_doc_url(object_name) + + pattern = rf"``{object_name}``" + repl = rf"`{object_name} <{url}>`_" + text = text.replace(pattern, repl) + + # Correct double quotes. + text = LINK_PATTERN.sub(LINK_REPL, text) + + return text + + +def process_docstring( + app, what, name, obj, options, lines +): # pylint: disable=missing-param-doc,unused-argument + """Process autodoc docstrings.""" + text = process_docstring_text('\n'.join(lines)) + lines[:] = text.splitlines() + + if what != 'class': + return + + presets = getattr(obj, '_presets', None) + if not presets: + return + + lines.append('') + lines.append('.. rubric:: Preset Library') + lines.append('') + + for preset_name, fn in presets.items(): + doc = inspect.getdoc(fn) or 'No description provided.' + + lines.append(f'**{preset_name}**') + lines.append('') + + # Process preset documentation using exactly the same rules + # as the class documentation. + preset_text = process_docstring_text(doc) + + lines.extend(preset_text.splitlines()) + lines.append('') + + +def get_doc_url(name): + """Get doc URL for the given im2sim name.""" + url = 'https://mrphys.github.io/im2sim/api_docs/' + url += name.replace('.', '/') + return url + + +def setup(app): + app.connect('autodoc-process-docstring', process_docstring) diff --git a/tools/docs/conf_helper.py b/tools/docs/conf_helper.py new file mode 100644 index 0000000..a226687 --- /dev/null +++ b/tools/docs/conf_helper.py @@ -0,0 +1,3 @@ +class AutosummaryFilenameMap(dict): + def get(self, k, default=None): + return k.split('.')[-1] diff --git a/tools/docs/create_documents.py b/tools/docs/create_documents.py new file mode 100644 index 0000000..b4b1063 --- /dev/null +++ b/tools/docs/create_documents.py @@ -0,0 +1,146 @@ +# Copyright 2026 University College London. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""RST document generator.""" + +import dataclasses +import inspect +import os +import string +import sys +import typing + +DOCS_PATH = os.path.dirname(os.path.realpath(__file__)) +ROOT_PATH = os.path.join(DOCS_PATH, '..', '..') +TEMPLATES_PATH = os.path.join(DOCS_PATH, 'templates') +API_DOCS_PATH = os.path.join(DOCS_PATH, 'api_docs') + +sys.path.insert(0, ROOT_PATH) + +from im2sim.src.utils import api_util + +# Create API docs directory. +os.makedirs(os.path.join(API_DOCS_PATH, 'im2sim'), exist_ok=True) + +# Read the index template. +with open(os.path.join(TEMPLATES_PATH, 'index.rst'), 'r') as f: + INDEX_TEMPLATE = string.Template(f.read()) + +im2sim_DOC_TEMPLATE = string.Template( +"""im2sim +===== + +.. automodule:: im2sim + +Modules +------- + +.. autosummary:: + :nosignatures: + + ${namespaces} + + +Classes +------- + +.. autosummary:: + :toctree: im2sim + :template: ops/class.rst + :nosignatures: + + + +Functions +--------- + +.. autosummary:: + :toctree: im2sim + :template: ops/function.rst + :nosignatures: +""") + +MODULE_DOC_TEMPLATE = string.Template( +"""im2sim.${module} +======${underline} + +.. automodule:: im2sim.${module} + +Classes +------- + +.. autosummary:: + :toctree: ${module} + :template: ${module}/class.rst + :nosignatures: + + ${classes} + +Functions +--------- + +.. autosummary:: + :toctree: ${module} + :template: ${module}/function.rst + :nosignatures: + + ${functions} +""") + + +@dataclasses.dataclass +class Module: + """A module.""" + classes: typing.List[str] = dataclasses.field(default_factory=list) + functions: typing.List[str] = dataclasses.field(default_factory=list) + +modules = {namespace: Module() for namespace in api_util.get_submodule_names()} + + +for name, symbol in api_util.get_api_symbols().items(): + name = api_util.get_canonical_name_for_symbol(symbol) + namespace, name = name.split('.', maxsplit=1) + + if inspect.isclass(symbol): + modules[namespace].classes.append(name) + elif inspect.isfunction(symbol): + modules[namespace].functions.append(name) + +# Write namespace templates. +for name, module in modules.items(): + classes = '\n '.join(sorted(set(module.classes))) + functions = '\n '.join(sorted(set(module.functions))) + + filename = os.path.join(API_DOCS_PATH, f'im2sim/{name}.rst') + with open(filename, 'w') as f: + f.write(MODULE_DOC_TEMPLATE.substitute( + module=name, + underline='=' * len(name), + classes=classes, + functions=functions)) + +# Write top-level API doc im2sim.rst. +filename = os.path.join(API_DOCS_PATH, 'im2sim.rst') +with open(filename, 'w') as f: + namespaces = api_util.get_submodule_names() + f.write(im2sim_DOC_TEMPLATE.substitute( + namespaces='\n '.join(sorted(namespaces)))) + +# Write index.rst. +filename = os.path.join(DOCS_PATH, 'index.rst') +with open(filename, 'w') as f: + namespaces = api_util.get_submodule_names() + namespaces = ['api_docs/im2sim/' + namespace for namespace in namespaces] + f.write(INDEX_TEMPLATE.substitute( + namespaces='\n '.join(sorted(namespaces)))) diff --git a/tools/docs/create_templates.py b/tools/docs/create_templates.py new file mode 100644 index 0000000..0ee484d --- /dev/null +++ b/tools/docs/create_templates.py @@ -0,0 +1,70 @@ +# Copyright 2026 University College London. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Autodoc template generator.""" + +import os +import string +import sys + +DOCS_PATH = os.path.dirname(os.path.realpath(__file__)) +ROOT_PATH = os.path.join(DOCS_PATH, '..', '..') + +sys.path.insert(0, ROOT_PATH) + +from im2sim.src.utils import api_util + + +CLASS_TEMPLATE = string.Template( +"""${module}.{{ objname | escape | underline }}${underline} + +.. currentmodule:: {{ module }} + +.. auto{{ objtype }}:: {{ objname }} + :members: + :show-inheritance: +""") + +FUNCTION_TEMPLATE = string.Template( +"""${module}.{{ objname | escape | underline }}${underline} + +.. currentmodule:: {{ module }} + +.. auto{{ objtype }}:: {{ objname }} +""") + +NAMESPACES = api_util.get_submodule_names() + +TEMPLATE_PATH = os.path.join( + os.path.dirname(os.path.realpath(__file__)), '_templates') + +for namespace in NAMESPACES: + # Create directory for this namespace. + os.makedirs(os.path.join(TEMPLATE_PATH, namespace), exist_ok=True) + + # Special treatment for namespace `ops`, which maps to the `im2sim` parent + # module. + module = f'im2sim.{namespace}' + + # Substitute the templates for this module. + class_template = CLASS_TEMPLATE.substitute( + module=module, underline='=' * (len(module) + 1)) + function_template = FUNCTION_TEMPLATE.substitute( + module=module, underline='=' * (len(module) + 1)) + + # Write template files. + with open(os.path.join(TEMPLATE_PATH, namespace, 'class.rst'), 'w') as f: + f.write(class_template) + with open(os.path.join(TEMPLATE_PATH, namespace, 'function.rst'), 'w') as f: + f.write(function_template) diff --git a/tools/docs/guide.rst b/tools/docs/guide.rst new file mode 100644 index 0000000..ac46b84 --- /dev/null +++ b/tools/docs/guide.rst @@ -0,0 +1,2 @@ +IM2SIM guide +==================== diff --git a/tools/docs/guide/faq.rst b/tools/docs/guide/faq.rst new file mode 100644 index 0000000..a699674 --- /dev/null +++ b/tools/docs/guide/faq.rst @@ -0,0 +1,15 @@ +Frequently Asked Questions +========================== + +**When trying to install TensorFlow MRI, I get an error about OpenEXR which +includes: +``OpenEXR.cpp:36:10: fatal error: ImathBox.h: No such file or directory``. What +do I do?** + +OpenEXR is needed by TensorFlow Graphics, which is a dependency of TensorFlow +MRI. This issue can be fixed by installing the OpenEXR library. On +Debian/Ubuntu: + +.. code-block:: console + + $ apt install libopenexr-dev diff --git a/tools/docs/guide/install.rst b/tools/docs/guide/install.rst new file mode 100644 index 0000000..404c4a7 --- /dev/null +++ b/tools/docs/guide/install.rst @@ -0,0 +1,89 @@ +Install TensorFlow MRI +====================== + +Requirements +------------ + +TensorFlow MRI should work in most Linux systems that meet the +`requirements for TensorFlow `_. + +.. warning:: + + TensorFlow MRI is not yet available for Windows or macOS. + `Help us support them! `_. + + +TensorFlow compatibility +~~~~~~~~~~~~~~~~~~~~~~~~ + +Each TensorFlow MRI release is compiled against a specific version of +TensorFlow. To ensure compatibility, it is recommended to install matching +versions of TensorFlow and TensorFlow MRI according to the +:ref:`TensorFlow compatibility table`. + +.. warning:: + + Each TensorFlow MRI version aims to target and support the latest TensorFlow + version only. A new version of TensorFlow MRI will be released shortly after + each TensorFlow release. TensorFlow MRI versions that target older versions + of TensorFlow will not generally receive any updates. + + +Set up your system +------------------ + +You will need a working TensorFlow installation. Follow the `TensorFlow +installation instructions `_ if you do not +have one already. + + +Use a GPU +~~~~~~~~~ + +If you need GPU support, we suggest that you use one of the +`TensorFlow Docker images `_. +These come with a GPU-enabled TensorFlow installation and are the easiest way +to run TensorFlow and TensorFlow MRI on your system. + +.. code-block:: console + + $ docker pull tensorflow/tensorflow:latest-gpu + +Alternatively, make sure you follow +`these instructions `_ when setting up +your system. + + +Download from PyPI +------------------ + +TensorFlow MRI is available on the Python package index (PyPI) and can be +installed using the ``pip`` package manager: + +.. code-block:: console + + $ pip install tensorflow-mri + + +Run in Google Colab +------------------- + +To get started without installing anything on your system, you can use +`Google Colab `_. +Simply create a new notebook and use ``pip`` to install TensorFlow MRI. + +.. code:: python + + !pip install tensorflow-mri + + +The Colab environment is already configured to run TensorFlow and has GPU +support. + + +TensorFlow compatibility table +------------------------------ + +.. include:: ../../../README.rst + :start-after: start-compatibility-table + :end-before: end-compatibility-table diff --git a/tools/docs/index.rst b/tools/docs/index.rst new file mode 100644 index 0000000..a8aa51c --- /dev/null +++ b/tools/docs/index.rst @@ -0,0 +1,29 @@ +IM2SIM |release| +======================== + +.. image:: https://img.shields.io/badge/-View%20on%20GitHub-128091?logo=github&labelColor=grey + :target: https://github.com/mrphys/im2sim + :alt: View on GitHub + +.. include:: ../../README.rst + :start-after: start-intro + :end-before: end-intro + + +.. toctree:: + :caption: API Documentation + :hidden: + + API documentation + api_docs/im2sim + api_docs/im2sim/configs + api_docs/im2sim/data + api_docs/im2sim/layers + api_docs/im2sim/losses + api_docs/im2sim/models + api_docs/im2sim/ops + api_docs/im2sim/plot + + +.. meta:: + :google-site-verification: 8PySedj6KJ0kc5qC1CbO6_9blFB9Nho3SgXvbRzyVOU diff --git a/tools/docs/make.bat b/tools/docs/make.bat new file mode 100644 index 0000000..2119f51 --- /dev/null +++ b/tools/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/tools/docs/requirements.txt b/tools/docs/requirements.txt new file mode 100644 index 0000000..a95ae18 --- /dev/null +++ b/tools/docs/requirements.txt @@ -0,0 +1 @@ +furo diff --git a/tools/docs/robots.txt b/tools/docs/robots.txt new file mode 100644 index 0000000..6d0bb8a --- /dev/null +++ b/tools/docs/robots.txt @@ -0,0 +1,3 @@ +User-agent: * + +Sitemap: https://mrphys.github.io/im2sim/sitemap.xml diff --git a/tools/docs/templates/index.rst b/tools/docs/templates/index.rst new file mode 100644 index 0000000..aef3f8f --- /dev/null +++ b/tools/docs/templates/index.rst @@ -0,0 +1,23 @@ +IM2SIM |release| +======================== + +.. image:: https://img.shields.io/badge/-View%20on%20GitHub-128091?logo=github&labelColor=grey + :target: https://github.com/mrphys/im2sim + :alt: View on GitHub + +.. include:: ../../README.rst + :start-after: start-intro + :end-before: end-intro + + +.. toctree:: + :caption: API Documentation + :hidden: + + API documentation + api_docs/im2sim + ${namespaces} + + +.. meta:: + :google-site-verification: 8PySedj6KJ0kc5qC1CbO6_9blFB9Nho3SgXvbRzyVOU diff --git a/tools/docs/test_docs.py b/tools/docs/test_docs.py new file mode 100644 index 0000000..139e6b2 --- /dev/null +++ b/tools/docs/test_docs.py @@ -0,0 +1,13 @@ +# import doctest +# import pathlib +# import sys +# wdir = pathlib.Path().absolute() +# sys.path.insert(0, str(wdir)) + +# from tensorflow_mri.python.ops import array_ops +# from tensorflow_mri.python.ops import wavelet_ops + +# kwargs = dict(raise_on_error=True) + +# doctest.testmod(array_ops, **kwargs) +# doctest.testmod(wavelet_ops, **kwargs) diff --git a/tools/docs/tutorials.rst b/tools/docs/tutorials.rst new file mode 100644 index 0000000..9c52220 --- /dev/null +++ b/tools/docs/tutorials.rst @@ -0,0 +1,10 @@ +TensorFlow MRI tutorials +======================== + +All TensorFlow MRI tutorials are written as Jupyter notebooks. + +In addition to viewing them on this website, you can run them directly in +Google Colab, a hosted notebook environment with no setup and free access to +GPUs. Click on the **Run in Colab** button to begin. + +Alternatively, you can also download the notebooks to run on your machine.