Compare commits

...

21 Commits

Author SHA1 Message Date
mbsantiago
586e78814f refactor: remove ambiguous training precision option 2026-08-08 12:34:07 +01:00
mbsantiago
c8f0f2bee1 docs: document runtime compilation options 2026-08-08 12:18:55 +01:00
mbsantiago
bad5d4f4fc test: cover compile idempotency paths 2026-08-08 12:04:19 +01:00
mbsantiago
8166db8f9c feat: add runtime model compilation options 2026-08-08 12:02:00 +01:00
mbsantiago
9f22fd097f Merge branch 'feat/compile-model' of github.com:macaodha/batdetect2 into feat/compile-model 2026-08-08 11:04:52 +01:00
Santiago Martinez Balvanera
bcc9cb6d00 Expand the cosine annealing config 2026-08-08 11:01:38 +01:00
Santiago Martinez Balvanera
ead0adf284 Allow passing the logger object directly to the train workflow 2026-08-08 11:01:38 +01:00
Santiago Martinez Balvanera
7492280218 Add setting for compiling the model before train 2026-08-08 11:01:38 +01:00
Santiago Martinez Balvanera
d7896c6d75
Merge pull request #74 from macaodha/fix/raw-format-stack
Some checks are pending
CI / Checks (push) Waiting to run
CI / Tests (Python ${{ matrix.python-version }}) (3.10) (push) Waiting to run
CI / Tests (Python ${{ matrix.python-version }}) (3.11) (push) Waiting to run
CI / Tests (Python ${{ matrix.python-version }}) (3.12) (push) Waiting to run
Docs Pages / Build Docs (push) Waiting to run
Docs Pages / Deploy Docs (push) Blocked by required conditions
Fix raw formatter loading for empty detections and large outputs
2026-08-08 10:59:47 +01:00
mbsantiago
23ccf7f5d6 fix: validate raw recording metadata on load 2026-08-08 10:54:56 +01:00
mbsantiago
8f01a914d7 test: cover raw formatter round trips 2026-08-07 19:59:32 +01:00
mbsantiago
01b0166346 Merge branch 'fix/raw-format-stack' of github.com:macaodha/batdetect2 into fix/raw-format-stack 2026-08-07 19:54:18 +01:00
Santiago Martinez Balvanera
149c5b85ef Add progress reporting and multiprocessing 2026-08-07 19:49:19 +01:00
Santiago Martinez Balvanera
b8f23d8b4b Avoid np stack error for empty arrays 2026-08-07 19:49:19 +01:00
Santiago Martinez Balvanera
ffa37ebb43
Merge pull request #73 from macaodha/enhancement/smaller-bottleneck
Some checks are pending
CI / Checks (push) Waiting to run
CI / Tests (Python ${{ matrix.python-version }}) (3.10) (push) Waiting to run
CI / Tests (Python ${{ matrix.python-version }}) (3.11) (push) Waiting to run
CI / Tests (Python ${{ matrix.python-version }}) (3.12) (push) Waiting to run
Docs Pages / Build Docs (push) Waiting to run
Docs Pages / Deploy Docs (push) Blocked by required conditions
Add configurable bottleneck blocks and aggregation strategies
2026-08-07 19:44:21 +01:00
mbsantiago
ac805be169 fix: address bottleneck config review feedback 2026-08-07 19:39:50 +01:00
mbsantiago
cf49c6e3da fix: align bottleneck aggregation configuration 2026-08-07 18:33:27 +01:00
Santiago Martinez Balvanera
8475b09527 Add progress reporting and multiprocessing 2026-08-04 10:41:57 +01:00
Santiago Martinez Balvanera
12ccfc4ed4 Avoid np stack error for empty arrays 2026-08-04 08:49:08 +01:00
Santiago Martinez Balvanera
906f97df33 Add efficient self attention layers 2026-08-04 08:43:08 +01:00
Santiago Martinez Balvanera
575e0b3d28 Add vertical mean block 2026-07-15 16:55:29 +01:00
23 changed files with 1164 additions and 295 deletions

View File

@ -15,6 +15,9 @@ Defined in `batdetect2.api_v2`.
- `BatDetect2API.from_config(model_config=..., targets_config=..., ...)` - `BatDetect2API.from_config(model_config=..., targets_config=..., ...)`
- build a full model stack from config objects. - build a full model stack from config objects.
Both constructors accept `compile_model=True` to compile the detector after the
API is built.
## Common tasks ## Common tasks
- Load a checkpoint and run prediction on one file. - Load a checkpoint and run prediction on one file.
@ -22,6 +25,8 @@ Defined in `batdetect2.api_v2`.
- Save predictions in one of the supported output formats. - Save predictions in one of the supported output formats.
- Evaluate a model on labelled data. - Evaluate a model on labelled data.
- Fine-tune an existing checkpoint on new targets. - Fine-tune an existing checkpoint on new targets.
- Compile the detector explicitly with `BatDetect2API.compile()` when you want
to opt into PyTorch runtime compilation from Python.
## Generated reference ## Generated reference

View File

@ -7,6 +7,8 @@ Defined in `batdetect2.inference.config`.
## Top-level fields ## Top-level fields
- `compile_model`
- compile the detector before batch prediction. This is off by default.
- `loader` - `loader`
- data-loader settings for inference. - data-loader settings for inference.
- `clipping` - `clipping`
@ -34,8 +36,19 @@ Override `InferenceConfig` when:
- long recordings need different clipping behavior, - long recordings need different clipping behavior,
- you want to tune batch size for your hardware, - you want to tune batch size for your hardware,
- you want to opt into runtime model compilation for repeated predictions,
- you need reproducible prediction settings across runs. - you need reproducible prediction settings across runs.
## Runtime compilation
Set `compile_model: true` to compile the detector before batch inference. This
can help when you run repeated predictions with stable input shapes. For a
single short run, the compile step can cost more time than it saves.
In Python, you can also compile explicitly with `BatDetect2API.compile()` or by
passing `compile_model=True` to `BatDetect2API.from_checkpoint(...)` or
`BatDetect2API.from_config(...)`.
## Related pages ## Related pages
- Tune inference clipping: - Tune inference clipping:

View File

@ -7,6 +7,8 @@ Defined in `batdetect2.train.config`.
## Top-level fields ## Top-level fields
- `compile_model`
- compile the detector before training starts. This is off by default.
- `train_loader` - `train_loader`
- training data loading and clipping settings. - training data loading and clipping settings.
- `val_loader` - `val_loader`
@ -33,10 +35,18 @@ Use `TrainingConfig` when you want to change things like:
- batch size, - batch size,
- augmentation, - augmentation,
- optimiser and scheduler settings, - optimiser and scheduler settings,
- runtime options such as model compilation,
- number of epochs, - number of epochs,
- validation frequency, - validation frequency,
- checkpoint behaviour. - checkpoint behaviour.
## Runtime options
Use `compile_model: true` to call `torch.compile` on the detector used during
training. This can help on longer runs with stable tensor shapes, but it may be
slower for short CPU-only experiments because PyTorch has to compile the graph
before it can reuse it.
Example files live under `example_data/configs/`, including Example files live under `example_data/configs/`, including
`example_data/configs/training.yaml`. `example_data/configs/training.yaml`.

View File

@ -136,7 +136,7 @@ clean: clean-build clean-pyc clean-test clean-docs
# Train on example data. # Train on example data.
example-train OPTIONS="": example-train OPTIONS="":
uv run batdetect2 train \ uv run batdetect2 -v train \
--val-dataset example_data/dataset.yaml \ --val-dataset example_data/dataset.yaml \
--base-dir . \ --base-dir . \
--targets example_data/targets.yaml \ --targets example_data/targets.yaml \

View File

@ -25,8 +25,8 @@ dependencies = [
"soundevent[audio,geometry,plot]>=2.10.0", "soundevent[audio,geometry,plot]>=2.10.0",
"soundfile>=0.12.1", "soundfile>=0.12.1",
"tensorboard>=2.16.2", "tensorboard>=2.16.2",
"torch>=1.13.1", "torch>=2.0.0",
"torchaudio>=1.13.1", "torchaudio>=2.0.0",
"xarray>=2024.0.0", "xarray>=2024.0.0",
] ]
requires-python = ">=3.10,<3.14" requires-python = ">=3.10,<3.14"

View File

@ -1,5 +1,4 @@
from __future__ import annotations from __future__ import annotations
from lightning.pytorch.loggers import Logger
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Literal from typing import TYPE_CHECKING, Literal
@ -9,6 +8,7 @@ if TYPE_CHECKING:
import numpy as np import numpy as np
import torch import torch
from lightning.pytorch.loggers import Logger
from soundevent import data from soundevent import data
from batdetect2.audio import AudioConfig, AudioLoader from batdetect2.audio import AudioConfig, AudioLoader
@ -153,6 +153,19 @@ class BatDetect2API:
self.model.eval() self.model.eval()
def compile(self) -> "BatDetect2API":
"""Compile the detector path used by inference.
Returns
-------
BatDetect2API
This API instance with the detector compiled.
"""
from batdetect2.models import compile_model
compile_model(self.model)
return self
def load_annotations( def load_annotations(
self, self,
path: data.PathLike, path: data.PathLike,
@ -228,6 +241,9 @@ class BatDetect2API:
Training logger config override. Training logger config override.
logging_callbacks : Sequence[LoggingCallback[TrainLoggingContext]], optional logging_callbacks : Sequence[LoggingCallback[TrainLoggingContext]], optional
Extra logging callbacks to run during training setup. Extra logging callbacks to run during training setup.
train_logger : Logger | None, optional
Pre-built Lightning logger to use for training. If omitted, one is
built from ``logger_config``.
Returns Returns
------- -------
@ -982,6 +998,7 @@ class BatDetect2API:
inference_config: InferenceConfig | None = None, inference_config: InferenceConfig | None = None,
outputs_config: OutputsConfig | None = None, outputs_config: OutputsConfig | None = None,
logging_config: AppLoggingConfig | None = None, logging_config: AppLoggingConfig | None = None,
compile_model: bool = False,
) -> "BatDetect2API": ) -> "BatDetect2API":
"""Build an API instance from config objects. """Build an API instance from config objects.
@ -1007,6 +1024,8 @@ class BatDetect2API:
Output config. If omitted, the default outputs config is used. Output config. If omitted, the default outputs config is used.
logging_config : AppLoggingConfig | None, optional logging_config : AppLoggingConfig | None, optional
Logging config. If omitted, the default logging config is used. Logging config. If omitted, the default logging config is used.
compile_model : bool, optional
If ``True``, compile the detector path after building the API.
Returns Returns
------- -------
@ -1089,7 +1108,7 @@ class BatDetect2API:
), ),
) )
return cls( api = cls(
model_config=model_config, model_config=model_config,
audio_config=audio_config, audio_config=audio_config,
train_config=train_config, train_config=train_config,
@ -1108,6 +1127,11 @@ class BatDetect2API:
output_transform=output_transform, output_transform=output_transform,
) )
if compile_model:
api.compile()
return api
@classmethod @classmethod
def from_checkpoint( def from_checkpoint(
cls, cls,
@ -1118,6 +1142,7 @@ class BatDetect2API:
inference_config: InferenceConfig | None = None, inference_config: InferenceConfig | None = None,
outputs_config: OutputsConfig | None = None, outputs_config: OutputsConfig | None = None,
logging_config: AppLoggingConfig | None = None, logging_config: AppLoggingConfig | None = None,
compile_model: bool = False,
) -> "BatDetect2API": ) -> "BatDetect2API":
"""Build an API instance from a saved checkpoint. """Build an API instance from a saved checkpoint.
@ -1138,6 +1163,8 @@ class BatDetect2API:
Output config override. Output config override.
logging_config : AppLoggingConfig | None, optional logging_config : AppLoggingConfig | None, optional
Logging config override. Logging config override.
compile_model : bool, optional
If ``True``, compile the detector path after building the API.
Returns Returns
------- -------
@ -1221,7 +1248,7 @@ class BatDetect2API:
transform=output_transform, transform=output_transform,
) )
return cls( api = cls(
model_config=model_config, model_config=model_config,
audio_config=audio_config, audio_config=audio_config,
train_config=train_config, train_config=train_config,
@ -1240,6 +1267,11 @@ class BatDetect2API:
output_transform=output_transform, output_transform=output_transform,
) )
if compile_model:
api.compile()
return api
def _set_trainable_parameters( def _set_trainable_parameters(
self, self,
trainable: Literal["all", "heads", "classifier_head", "size_head"], trainable: Literal["all", "heads", "classifier_head", "size_head"],

View File

@ -10,6 +10,7 @@ from batdetect2.inference.clips import get_clips_from_files
from batdetect2.inference.config import InferenceConfig from batdetect2.inference.config import InferenceConfig
from batdetect2.inference.dataset import build_inference_loader from batdetect2.inference.dataset import build_inference_loader
from batdetect2.inference.lightning import InferenceModule from batdetect2.inference.lightning import InferenceModule
from batdetect2.models import compile_model
from batdetect2.models.types import ModelProtocol from batdetect2.models.types import ModelProtocol
from batdetect2.outputs import ( from batdetect2.outputs import (
OutputsConfig, OutputsConfig,
@ -71,6 +72,9 @@ def run_batch_inference(
batch_size=batch_size, batch_size=batch_size,
) )
if inference_config.compile_model:
compile_model(model)
module = InferenceModule( module = InferenceModule(
model, model,
output_transform=output_transform, output_transform=output_transform,

View File

@ -15,6 +15,7 @@ class ClipingConfig(BaseConfig):
class InferenceConfig(BaseConfig): class InferenceConfig(BaseConfig):
compile_model: bool = False
loader: InferenceLoaderConfig = Field( loader: InferenceLoaderConfig = Field(
default_factory=InferenceLoaderConfig default_factory=InferenceLoaderConfig
) )

View File

@ -100,6 +100,7 @@ __all__ = [
"ModelConfig", "ModelConfig",
"build_model", "build_model",
"build_model_with_new_targets", "build_model_with_new_targets",
"compile_model",
] ]
@ -112,9 +113,6 @@ class ModelConfig(BaseConfig):
Attributes Attributes
---------- ----------
compile : bool
If ``True``, compile the model before training. Defaults to
``False``.
samplerate : int samplerate : int
Expected input audio sample rate in Hz. Audio must be resampled Expected input audio sample rate in Hz. Audio must be resampled
to this rate before being passed to the model. Defaults to to this rate before being passed to the model. Defaults to
@ -132,7 +130,6 @@ class ModelConfig(BaseConfig):
``PostprocessConfig()``. ``PostprocessConfig()``.
""" """
compile: bool = False
samplerate: int = Field(default=TARGET_SAMPLERATE_HZ, gt=0) samplerate: int = Field(default=TARGET_SAMPLERATE_HZ, gt=0)
architecture: BackboneConfig = Field(default_factory=UNetBackboneConfig) architecture: BackboneConfig = Field(default_factory=UNetBackboneConfig)
preprocess: PreprocessingConfig = Field( preprocess: PreprocessingConfig = Field(
@ -323,3 +320,15 @@ def build_model_with_new_targets(
dimension_names=roi_mapper.dimension_names, dimension_names=roi_mapper.dimension_names,
config=model.get_config(), config=model.get_config(),
) )
def compile_model(model: ModelProtocol) -> ModelProtocol:
"""Compile the detector path used by training and inference."""
if not isinstance(model.detector, torch.nn.Module):
raise TypeError("Detector must be a torch.nn.Module to compile.")
if getattr(model.detector, "_compiled_call_impl", None) is not None:
return model
model.detector.compile()
return model

View File

@ -70,11 +70,16 @@ __all__ = [
"FreqCoordConvUpBlock", "FreqCoordConvUpBlock",
"StandardConvUpBlock", "StandardConvUpBlock",
"SelfAttention", "SelfAttention",
"EfficientSelfAttention",
"VerticalMean",
"ConvConfig", "ConvConfig",
"EfficientSelfAttentionConfig",
"FreqCoordConvDownConfig", "FreqCoordConvDownConfig",
"StandardConvDownConfig", "StandardConvDownConfig",
"FreqCoordConvUpConfig", "FreqCoordConvUpConfig",
"StandardConvUpConfig", "StandardConvUpConfig",
"VerticalConvConfig",
"VerticalMeanConfig",
"LayerConfig", "LayerConfig",
"build_layer", "build_layer",
] ]
@ -145,8 +150,8 @@ class SelfAttentionConfig(BaseConfig):
attention_channels : int attention_channels : int
Dimensionality of the query, key, and value projections. Dimensionality of the query, key, and value projections.
temperature : float temperature : float
Scaling factor applied to the weighted values before the final Divisor applied together with ``attention_channels`` when scaling
linear projection. Defaults to ``1``. dot-product attention logits. Defaults to ``1``.
""" """
name: Literal["SelfAttention"] = "SelfAttention" name: Literal["SelfAttention"] = "SelfAttention"
@ -306,6 +311,126 @@ class SelfAttention(Block):
) )
class EfficientSelfAttentionConfig(BaseConfig):
"""Configuration for an ``EfficientSelfAttention`` block.
Attributes
----------
name : str
Discriminator field; always ``"EfficientSelfAttention"``.
attention_channels : int
Dimensionality of the query, key, and value projections.
temperature : float
Divisor applied together with ``attention_channels`` when scaling
dot-product attention logits. Defaults to ``1``.
"""
name: Literal["EfficientSelfAttention"] = "EfficientSelfAttention"
attention_channels: int
temperature: float = 1.0
class EfficientSelfAttention(Block):
"""An optimized self-attention block operating along the time axis.
Applies a scaled dot-product self-attention mechanism across the time
steps of an input feature map. This version uses a fused QKV linear
projection and PyTorch's native scaled dot-product attention (SDPA)
for optimal memory usage and execution speed.
Parameters
----------
in_channels : int
Number of input channels (features per time step).
attention_channels : int
Dimensionality of the query, key, and value projections.
temperature : float, default=1.0
Divisor applied together with ``attention_channels`` when scaling
the dot-product scores before softmax.
Attributes
----------
qkv_proj : nn.Linear
Fused linear projection for queries, keys, and values.
pro_fun : nn.Linear
Final linear projection applied to the attended values.
temperature : float
Scaling divisor used when computing attention scores.
att_dim : int
Dimensionality of the attention space (``attention_channels``).
"""
def __init__(
self,
in_channels: int,
attention_channels: int,
temperature: float = 1.0,
):
super().__init__()
self.in_channels = in_channels
self.out_channels = in_channels
self.temperature = temperature
self.att_dim = attention_channels
self.output_channels = in_channels
self.qkv_proj = nn.Linear(in_channels, 3 * attention_channels)
self.pro_fun = nn.Linear(attention_channels, in_channels)
self.scale_factor = 1.0 / (self.temperature * self.att_dim)
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""Apply self-attention along the time dimension."""
x = x.squeeze(2).permute(0, 2, 1)
# Single projection pass
qkv = self.qkv_proj(x)
# Split along the last dimension into Q, K, V
query, key, value = torch.chunk(qkv, 3, dim=-1)
att = F.scaled_dot_product_attention(
query,
key,
value,
attn_mask=None,
dropout_p=0.0,
is_causal=False,
scale=self.scale_factor,
)
op = self.pro_fun(att)
return op.permute(0, 2, 1).unsqueeze(2)
def compute_attention_weights(self, x: torch.Tensor) -> torch.Tensor:
"""Return the softmax attention weight matrix.
Useful for visualising which time steps attend to which others.
"""
x = x.squeeze(2).permute(0, 2, 1)
qkv = self.qkv_proj(x)
query, key, _ = torch.chunk(qkv, 3, dim=-1)
kk_qq = torch.bmm(key, query.permute(0, 2, 1)) * self.scale_factor
att_weights = F.softmax(kk_qq, dim=1)
return att_weights
@block_registry.register(EfficientSelfAttentionConfig)
@staticmethod
def from_config(
config: EfficientSelfAttentionConfig,
input_channels: int,
input_height: int,
) -> "EfficientSelfAttention":
return EfficientSelfAttention(
in_channels=input_channels,
attention_channels=config.attention_channels,
temperature=config.temperature,
)
class ConvConfig(BaseConfig): class ConvConfig(BaseConfig):
"""Configuration for a basic ConvBlock.""" """Configuration for a basic ConvBlock."""
@ -460,6 +585,10 @@ class VerticalConv(Block):
""" """
return F.relu_(self.bn(self.conv(x))) return F.relu_(self.bn(self.conv(x)))
def get_output_height(self, input_height: int) -> int:
"""Return the collapsed output height."""
return 1
@block_registry.register(VerticalConvConfig) @block_registry.register(VerticalConvConfig)
@staticmethod @staticmethod
def from_config( def from_config(
@ -474,6 +603,94 @@ class VerticalConv(Block):
) )
class VerticalMeanConfig(BaseConfig):
"""Configuration for a ``VerticalMean`` block.
Attributes
----------
name : str
Discriminator field; always ``"VerticalMean"``.
"""
name: Literal["VerticalMean"] = "VerticalMean"
"""Discriminator field indicating the block type."""
channels: int
"""Number of output channels."""
class VerticalMean(Block):
"""Mean pooling block operating along the height dimension.
Applies a 2D mean pooling operation, followed by a 2D convolution,
followed by a batch normalization and ReLU activation.
Sequence: Mean Pool -> Conv -> BN -> ReLU.
Parameters
----------
in_channels : int
Number of channels in the input tensor.
out_channels : int
Number of output channels after the mean pooling.
input_height : int
The height (H dimension) of the input tensor. The convolutional kernel
will be sized `(1, 1)`.
"""
def __init__(
self,
in_channels: int,
out_channels: int,
input_height: int,
):
super().__init__()
self.in_channels = in_channels
self.out_channels = out_channels
self.input_height = input_height
self.conv = nn.Conv2d(
in_channels,
out_channels,
kernel_size=(1, 1),
padding=0,
)
self.batch_norm = nn.BatchNorm2d(out_channels)
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""Apply avg pooling -> Conv -> BN -> ReLU.
Parameters
----------
x : torch.Tensor
Input tensor, shape `(B, C_in, H, W)`.
Returns
-------
torch.Tensor
Output tensor, shape `(B, C_out, 1, W)`.
"""
x = x.mean(dim=2, keepdim=True)
x = self.conv(x)
return F.relu(self.batch_norm(x), inplace=True)
def get_output_height(self, input_height: int) -> int:
"""Return the collapsed output height."""
return 1
@block_registry.register(VerticalMeanConfig)
@staticmethod
def from_config(
config: VerticalMeanConfig,
input_channels: int,
input_height: int,
):
return VerticalMean(
in_channels=input_channels,
out_channels=config.channels,
input_height=input_height,
)
class FreqCoordConvDownConfig(BaseConfig): class FreqCoordConvDownConfig(BaseConfig):
"""Configuration for a FreqCoordConvDownBlock.""" """Configuration for a FreqCoordConvDownBlock."""
@ -951,13 +1168,16 @@ LayerConfig = Annotated[
| FreqCoordConvUpConfig | FreqCoordConvUpConfig
| StandardConvUpConfig | StandardConvUpConfig
| SelfAttentionConfig | SelfAttentionConfig
| EfficientSelfAttentionConfig
| VerticalConvConfig
| VerticalMeanConfig
| LayerGroupConfig, | LayerGroupConfig,
Field(discriminator="name"), Field(discriminator="name"),
] ]
"""Type alias for the discriminated union of block configuration models.""" """Type alias for the discriminated union of block configuration models."""
class LayerGroup(nn.Module): class LayerGroup(Block):
"""Sequential chain of blocks that acts as a single composite block. """Sequential chain of blocks that acts as a single composite block.
Wraps multiple ``Block`` instances in an ``nn.Sequential`` container, Wraps multiple ``Block`` instances in an ``nn.Sequential`` container,

View File

@ -21,14 +21,18 @@ This module provides:
from typing import Annotated, List from typing import Annotated, List
import torch import torch
from pydantic import Field from pydantic import Field, model_validator
from torch import nn from torch import nn
from batdetect2.core.configs import BaseConfig from batdetect2.core.configs import BaseConfig
from batdetect2.models.blocks import ( from batdetect2.models.blocks import (
Block, Block,
EfficientSelfAttentionConfig,
SelfAttentionConfig, SelfAttentionConfig,
VerticalConv, VerticalConv,
VerticalConvConfig,
VerticalMean,
VerticalMeanConfig,
build_layer, build_layer,
) )
from batdetect2.models.types import BottleneckProtocol from batdetect2.models.types import BottleneckProtocol
@ -94,6 +98,7 @@ class Bottleneck(Block):
in_channels: int, in_channels: int,
out_channels: int, out_channels: int,
bottleneck_channels: int | None = None, bottleneck_channels: int | None = None,
frequency_aggregator: Block | None = None,
layers: List[torch.nn.Module] | None = None, layers: List[torch.nn.Module] | None = None,
) -> None: ) -> None:
"""Initialise the Bottleneck layer. """Initialise the Bottleneck layer.
@ -125,12 +130,15 @@ class Bottleneck(Block):
) )
self.layers = nn.ModuleList(layers or []) self.layers = nn.ModuleList(layers or [])
self.conv_vert = VerticalConv( if frequency_aggregator is None:
frequency_aggregator = VerticalConv(
in_channels=in_channels, in_channels=in_channels,
out_channels=self.bottleneck_channels, out_channels=self.bottleneck_channels,
input_height=input_height, input_height=input_height,
) )
self.conv_vert = frequency_aggregator
def forward(self, x: torch.Tensor) -> torch.Tensor: def forward(self, x: torch.Tensor) -> torch.Tensor:
"""Process the encoder's bottleneck features. """Process the encoder's bottleneck features.
@ -160,12 +168,19 @@ class Bottleneck(Block):
BottleneckLayerConfig = Annotated[ BottleneckLayerConfig = Annotated[
SelfAttentionConfig, SelfAttentionConfig | EfficientSelfAttentionConfig,
Field(discriminator="name"), Field(discriminator="name"),
] ]
"""Type alias for the discriminated union of block configs usable in the Bottleneck.""" """Type alias for the discriminated union of block configs usable in the Bottleneck."""
FrequencyAggregationLayerConfig = Annotated[
(VerticalConvConfig | VerticalMeanConfig),
Field(discriminator="name"),
]
"""Type alias for the discriminated union of block configs usable in the FrequencyAggregation."""
class BottleneckConfig(BaseConfig): class BottleneckConfig(BaseConfig):
"""Configuration for the bottleneck component. """Configuration for the bottleneck component.
@ -182,17 +197,79 @@ class BottleneckConfig(BaseConfig):
""" """
channels: int channels: int
frequency_aggregation: FrequencyAggregationLayerConfig | None = None
layers: List[BottleneckLayerConfig] = Field(default_factory=list) layers: List[BottleneckLayerConfig] = Field(default_factory=list)
@model_validator(mode="after")
def set_default_frequency_aggregation(self) -> "BottleneckConfig":
"""Default frequency aggregation to the bottleneck channel count."""
if self.frequency_aggregation is None:
self.frequency_aggregation = VerticalConvConfig(
channels=self.channels
)
return self
DEFAULT_BOTTLENECK_CONFIG: BottleneckConfig = BottleneckConfig( DEFAULT_BOTTLENECK_CONFIG: BottleneckConfig = BottleneckConfig(
channels=256, channels=256,
frequency_aggregation=VerticalConvConfig(channels=256),
layers=[ layers=[
SelfAttentionConfig(attention_channels=256), SelfAttentionConfig(attention_channels=256),
], ],
) )
def build_frequency_aggregation(
input_height: int,
in_channels: int,
config: FrequencyAggregationLayerConfig,
) -> Block:
"""Build a block for aggregating frequency information.
Parameters
----------
input_height : int
Height (number of frequency bins) of the input tensor from the
encoder. Must be positive.
in_channels : int
Number of channels in the input tensor from the encoder. Must be
positive.
config : FrequencyAggregationLayerConfig, optional
Configuration specifying the output channel count and any
additional layers. Uses ``VerticalConvConfig`` if ``None``.
Returns
-------
Block
An initialised ``VerticalConv`` module.
Raises
------
AssertionError
If any configured layer changes the height of the feature map
(bottleneck layers must preserve height so that it can be restored
by repetition).
"""
if config.name == "VerticalConv":
return VerticalConv(
in_channels=in_channels,
out_channels=config.channels,
input_height=input_height,
)
if config.name == "VerticalMean":
return VerticalMean(
in_channels=in_channels,
out_channels=config.channels,
input_height=input_height,
)
raise NotImplementedError(
f"Unknown frequency aggregation layer: {config.name}"
)
def build_bottleneck( def build_bottleneck(
input_height: int, input_height: int,
in_channels: int, in_channels: int,
@ -231,13 +308,26 @@ def build_bottleneck(
by repetition). by repetition).
""" """
config = config or DEFAULT_BOTTLENECK_CONFIG config = config or DEFAULT_BOTTLENECK_CONFIG
frequency_aggregation = config.frequency_aggregation
if frequency_aggregation is None:
raise ValueError("frequency_aggregation must be configured.")
current_channels = in_channels frequency_aggregator = build_frequency_aggregation(
current_height = input_height input_height=input_height,
in_channels=in_channels,
config=frequency_aggregation,
)
current_channels = frequency_aggregator.out_channels
current_height = frequency_aggregator.get_output_height(input_height)
assert current_height == 1, (
"Bottleneck frequency aggregation should collapse spectrogram height"
)
layers = [] layers = []
for layer_config in config.layers: for layer_config in config.layers:
previous_height = current_height
layer = build_layer( layer = build_layer(
input_height=current_height, input_height=current_height,
in_channels=current_channels, in_channels=current_channels,
@ -245,7 +335,7 @@ def build_bottleneck(
) )
current_height = layer.get_output_height(current_height) current_height = layer.get_output_height(current_height)
current_channels = layer.out_channels current_channels = layer.out_channels
assert current_height == input_height, ( assert current_height == previous_height, (
"Bottleneck layers should not change the spectrogram height" "Bottleneck layers should not change the spectrogram height"
) )
layers.append(layer) layers.append(layer)
@ -253,6 +343,7 @@ def build_bottleneck(
return Bottleneck( return Bottleneck(
input_height=input_height, input_height=input_height,
in_channels=in_channels, in_channels=in_channels,
out_channels=config.channels, out_channels=current_channels,
frequency_aggregator=frequency_aggregator,
layers=layers, layers=layers,
) )

View File

@ -1,4 +1,6 @@
import json
from collections import defaultdict from collections import defaultdict
from multiprocessing import Pool
from pathlib import Path from pathlib import Path
from typing import List, Literal, Sequence from typing import List, Literal, Sequence
from uuid import UUID, uuid4 from uuid import UUID, uuid4
@ -8,6 +10,7 @@ import xarray as xr
from loguru import logger from loguru import logger
from soundevent import data from soundevent import data
from soundevent.geometry import compute_bounds from soundevent.geometry import compute_bounds
from tqdm import tqdm
from batdetect2.core import BaseConfig from batdetect2.core import BaseConfig
from batdetect2.outputs.formats.base import ( from batdetect2.outputs.formats.base import (
@ -25,6 +28,8 @@ class RawOutputConfig(BaseConfig):
include_class_scores: bool = True include_class_scores: bool = True
include_features: bool = True include_features: bool = True
include_geometry: bool = True include_geometry: bool = True
n_jobs: int = 1
show_progress: bool = False
class RawFormatter(OutputFormatterProtocol[ClipDetections]): class RawFormatter(OutputFormatterProtocol[ClipDetections]):
@ -35,12 +40,19 @@ class RawFormatter(OutputFormatterProtocol[ClipDetections]):
include_features: bool = True, include_features: bool = True,
include_geometry: bool = True, include_geometry: bool = True,
parse_full_geometry: bool = False, parse_full_geometry: bool = False,
n_jobs: int = 1,
show_progress: bool = False,
): ):
self.targets = targets self.targets = targets
self.include_class_scores = include_class_scores self.include_class_scores = include_class_scores
self.include_features = include_features self.include_features = include_features
self.include_geometry = include_geometry self.include_geometry = include_geometry
self.parse_full_geometry = parse_full_geometry self.parse_full_geometry = parse_full_geometry
self.n_jobs = n_jobs
self.show_progress = show_progress
if n_jobs < 1:
raise ValueError("n_jobs must be >= 1")
def format( def format(
self, self,
@ -68,15 +80,40 @@ class RawFormatter(OutputFormatterProtocol[ClipDetections]):
def load(self, path: data.PathLike) -> List[ClipDetections]: def load(self, path: data.PathLike) -> List[ClipDetections]:
path = Path(path) path = Path(path)
files = list(path.glob("*.nc")) files = list(path.glob("*.nc"))
predictions: List[ClipDetections] = []
for filepath in files: if self.n_jobs == 1:
return self._load_sequential(files)
return self._load_parallel(files)
def _load_sequential(
self, files: Sequence[data.PathLike]
) -> List[ClipDetections]:
iterable = files
if self.show_progress:
iterable = tqdm(files, total=len(files))
return [self.load_single_file(filepath) for filepath in iterable]
def _load_parallel(
self, files: Sequence[data.PathLike]
) -> List[ClipDetections]:
with Pool(self.n_jobs) as pool:
if not self.show_progress:
return pool.map(self.load_single_file, files)
return list(
tqdm(
pool.imap(self.load_single_file, files),
total=len(files),
)
)
def load_single_file(self, filepath: data.PathLike) -> ClipDetections:
logger.debug(f"Loading clip predictions {filepath}") logger.debug(f"Loading clip predictions {filepath}")
clip_data = xr.load_dataset(filepath) clip_data = xr.load_dataset(filepath)
prediction = self.pred_from_xr(clip_data) return self.pred_from_xr(clip_data)
predictions.append(prediction)
return predictions
def pred_to_xr( def pred_to_xr(
self, self,
@ -140,7 +177,7 @@ class RawFormatter(OutputFormatterProtocol[ClipDetections]):
"clip_id": str(clip.uuid), "clip_id": str(clip.uuid),
} }
if self.include_class_scores: if self.include_class_scores and values["class_scores"]:
class_scores = np.stack(values["class_scores"], axis=0) class_scores = np.stack(values["class_scores"], axis=0)
data_vars["class_scores"] = ( data_vars["class_scores"] = (
["detection", "classes"], ["detection", "classes"],
@ -148,7 +185,7 @@ class RawFormatter(OutputFormatterProtocol[ClipDetections]):
) )
coords["classes"] = ("classes", self.targets.class_names) coords["classes"] = ("classes", self.targets.class_names)
if self.include_features: if self.include_features and values["features"]:
features = np.stack(values["features"], axis=0) features = np.stack(values["features"], axis=0)
data_vars["features"] = (["detection", "feature"], features) data_vars["features"] = (["detection", "feature"], features)
coords["feature"] = ("feature", np.arange(num_features)) coords["feature"] = ("feature", np.arange(num_features))
@ -167,59 +204,81 @@ class RawFormatter(OutputFormatterProtocol[ClipDetections]):
def pred_from_xr(self, dataset: xr.Dataset) -> ClipDetections: def pred_from_xr(self, dataset: xr.Dataset) -> ClipDetections:
clip_data = dataset clip_data = dataset
recording = data.Recording.model_validate_json( recording = data.Recording.model_validate(
clip_data.attrs["recording"] json.loads(clip_data.attrs["recording"])
) )
clip_id = clip_data.clip_id.item() clip_id = clip_data.clip_id.item()
clip = data.Clip( clip = data.Clip.model_construct(
recording=recording, recording=recording,
uuid=UUID(clip_id), uuid=UUID(clip_id),
start_time=clip_data.clip_start, start_time=float(clip_data.clip_start),
end_time=clip_data.clip_end, end_time=float(clip_data.clip_end),
) )
sound_events = [] sound_events = []
for detection in clip_data.coords["detection"]: num_detections = len(clip_data.coords["detection"])
detection_data = clip_data.sel(detection=detection)
score = detection_data.score.item()
if "geometry" in clip_data and self.parse_full_geometry: scores = clip_data.score.data
geometry = data.geometry_validate( start_times = clip_data.start_time.data
detection_data.geometry.item() end_times = clip_data.end_time.data
low_freqs = clip_data.low_freq.data
high_freqs = clip_data.high_freq.data
top_class_scores = clip_data.top_class_score.data
top_class = clip_data.top_class.data
num_classes = len(self.targets.class_names)
class_map = dict(
zip(
self.targets.class_names,
range(num_classes),
strict=True,
) )
)
geometries = None
if self.parse_full_geometry and "geometry" in clip_data:
geometries = clip_data.geometry.data
class_scores = None
if "class_scores" in clip_data:
class_scores = clip_data.class_scores.data
features = None
if "features" in clip_data:
features = clip_data.features.data
for index in range(num_detections):
score = scores[index]
if geometries is not None:
geometry = data.geometry_validate(geometries[index])
else: else:
start_time = detection_data.start_time.item() start_time = start_times[index]
end_time = detection_data.end_time.item() end_time = end_times[index]
low_freq = detection_data.low_freq.item() low_freq = low_freqs[index]
high_freq = detection_data.high_freq.item() high_freq = high_freqs[index]
geometry = data.BoundingBox.model_construct( geometry = data.BoundingBox.model_construct(
coordinates=[start_time, low_freq, end_time, high_freq] coordinates=[start_time, low_freq, end_time, high_freq]
) )
if "class_scores" in detection_data: if class_scores is not None:
class_scores = detection_data.class_scores.data class_score = class_scores[index]
else: else:
class_scores = np.zeros(len(self.targets.class_names)) class_score = np.zeros(num_classes)
class_index = self.targets.class_names.index( class_index = class_map[top_class[index]]
detection_data.top_class.item() class_score[class_index] = top_class_scores[index]
)
class_scores[class_index] = (
detection_data.top_class_score.item()
)
if "features" in detection_data: feats = features[index] if features is not None else np.zeros(0)
features = detection_data.features.data
else:
features = np.zeros(0)
sound_events.append( sound_events.append(
Detection( Detection(
geometry=geometry, geometry=geometry,
detection_score=score, detection_score=score,
class_scores=class_scores, class_scores=class_score,
features=features, features=feats,
) )
) )
@ -236,4 +295,6 @@ class RawFormatter(OutputFormatterProtocol[ClipDetections]):
include_class_scores=config.include_class_scores, include_class_scores=config.include_class_scores,
include_features=config.include_features, include_features=config.include_features,
include_geometry=config.include_geometry, include_geometry=config.include_geometry,
n_jobs=config.n_jobs,
show_progress=config.show_progress,
) )

View File

@ -1,5 +1,3 @@
from typing import Literal
from pydantic import Field from pydantic import Field
from batdetect2.core.configs import BaseConfig from batdetect2.core.configs import BaseConfig
@ -41,7 +39,7 @@ class PLTrainerConfig(BaseConfig):
class TrainingConfig(BaseConfig): class TrainingConfig(BaseConfig):
precision: Literal["medium", "high"] | None = None compile_model: bool = False
train_loader: TrainLoaderConfig = Field(default_factory=TrainLoaderConfig) train_loader: TrainLoaderConfig = Field(default_factory=TrainLoaderConfig)
val_loader: ValLoaderConfig = Field(default_factory=ValLoaderConfig) val_loader: ValLoaderConfig = Field(default_factory=ValLoaderConfig)
optimizer: OptimizerConfig = Field(default_factory=AdamOptimizerConfig) optimizer: OptimizerConfig = Field(default_factory=AdamOptimizerConfig)

View File

@ -2,7 +2,6 @@ from collections.abc import Sequence
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
import torch
from lightning import Trainer, seed_everything from lightning import Trainer, seed_everything
from lightning.pytorch.loggers import Logger from lightning.pytorch.loggers import Logger
from loguru import logger from loguru import logger
@ -16,7 +15,7 @@ from batdetect2.logging import (
LoggingCallback, LoggingCallback,
build_logger, build_logger,
) )
from batdetect2.models import ModelConfig, build_model from batdetect2.models import ModelConfig, build_model, compile_model
from batdetect2.models.types import ModelProtocol from batdetect2.models.types import ModelProtocol
from batdetect2.preprocess import PreprocessorProtocol, build_preprocessor from batdetect2.preprocess import PreprocessorProtocol, build_preprocessor
from batdetect2.targets import ( from batdetect2.targets import (
@ -208,16 +207,9 @@ def run_train(
run_name=run_name, run_name=run_name,
) )
if model_config.compile: if train_config.compile_model:
logger.info("Compiling model...") logger.info("Compiling detector...")
module.compile() compile_model(module.model)
if train_config.precision is not None:
logger.info(
"Setting precision float precision to {}",
train_config.precision,
)
torch.set_float32_matmul_precision(train_config.precision)
logger.info("Starting main training loop...") logger.info("Starting main training loop...")
trainer.fit( trainer.fit(

View File

@ -1,6 +1,7 @@
import uuid import uuid
from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Callable, List, Optional from typing import Any, Callable, List, Optional, cast
from uuid import uuid4 from uuid import uuid4
import lightning as L import lightning as L
@ -15,6 +16,7 @@ from batdetect2.audio.clips import build_clipper
from batdetect2.audio.types import AudioLoader, ClipperProtocol from batdetect2.audio.types import AudioLoader, ClipperProtocol
from batdetect2.data import DatasetConfig, load_dataset from batdetect2.data import DatasetConfig, load_dataset
from batdetect2.data.annotations.batdetect2 import BatDetect2FilesAnnotations from batdetect2.data.annotations.batdetect2 import BatDetect2FilesAnnotations
from batdetect2.models.types import ModelProtocol
from batdetect2.preprocess import build_preprocessor from batdetect2.preprocess import build_preprocessor
from batdetect2.preprocess.types import PreprocessorProtocol from batdetect2.preprocess.types import PreprocessorProtocol
from batdetect2.targets import ( from batdetect2.targets import (
@ -31,6 +33,12 @@ from batdetect2.train.lightning import build_training_module
from batdetect2.train.types import ClipLabeller from batdetect2.train.types import ClipLabeller
@dataclass
class DetectorCompileRecorder:
compile_count: int = 0
call_count: int = 0
@pytest.fixture @pytest.fixture
def example_data_dir() -> Path: def example_data_dir() -> Path:
pkg_dir = Path(__file__).parent.parent pkg_dir = Path(__file__).parent.parent
@ -156,12 +164,15 @@ def generate_whistle(tmp_path: Path):
offset = int((time - duration / 2) * samplerate) offset = int((time - duration / 2) * samplerate)
t = np.linspace(-duration / 2, duration / 2, frames, endpoint=False) t = np.linspace(-duration / 2, duration / 2, frames, endpoint=False)
data = signal.gausspulse( pulse = np.asarray(
signal.gausspulse(
t, t,
fc=frequency, fc=frequency,
bw=2 / (frequency * whistle_duration), bw=2 / (frequency * whistle_duration),
),
dtype=np.float64,
) )
wave = (np.roll(data, offset) * np.iinfo(np.int16).max).astype( wave = (np.roll(pulse, offset) * np.iinfo(np.int16).max).astype(
np.int16 np.int16
) )
sf.write(str(path), wave, samplerate, subtype="PCM_16") sf.write(str(path), wave, samplerate, subtype="PCM_16")
@ -363,6 +374,30 @@ def sample_audio_loader() -> AudioLoader:
return build_audio_loader() return build_audio_loader()
@pytest.fixture
def record_detector_compilation(
monkeypatch: pytest.MonkeyPatch,
) -> Callable[[ModelProtocol], DetectorCompileRecorder]:
def factory(model: ModelProtocol) -> DetectorCompileRecorder:
recorder = DetectorCompileRecorder()
detector = cast(Any, model.detector)
original_call_impl = detector._call_impl
def compile_detector() -> None:
recorder.compile_count += 1
def compiled_call(*args, **kwargs):
recorder.call_count += 1
return original_call_impl(*args, **kwargs)
detector._compiled_call_impl = compiled_call
monkeypatch.setattr(detector, "compile", compile_detector)
return recorder
return factory
@pytest.fixture @pytest.fixture
def bat_tag() -> data.Tag: def bat_tag() -> data.Tag:
return data.Tag(key="class", value="bat") return data.Tag(key="class", value="bat")

View File

@ -153,6 +153,61 @@ def test_process_spectrogram_rejects_batched_input(
api_v2.process_spectrogram(spec) api_v2.process_spectrogram(spec)
def test_user_can_compile_api_detector(
api_v2: BatDetect2API,
example_audio_files: list[Path],
record_detector_compilation,
) -> None:
recorder = record_detector_compilation(api_v2.model)
audio = api_v2.load_audio(example_audio_files[0])
spec = api_v2.generate_spectrogram(audio)
api_v2.compile()
api_v2.compile()
api_v2.process_spectrogram(spec)
assert recorder.compile_count == 1
assert recorder.call_count == 1
def test_api_from_config_compiles_detector_when_requested(
monkeypatch: pytest.MonkeyPatch,
) -> None:
compiled_models = []
def compile_model(model):
compiled_models.append(model)
return model
monkeypatch.setattr("batdetect2.models.compile_model", compile_model)
api = BatDetect2API.from_config(
compile_model=True,
)
assert compiled_models == [api.model]
def test_api_from_checkpoint_compiles_detector_when_requested(
tiny_checkpoint_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
compiled_models = []
def compile_model(model):
compiled_models.append(model)
return model
monkeypatch.setattr("batdetect2.models.compile_model", compile_model)
api = BatDetect2API.from_checkpoint(
tiny_checkpoint_path,
compile_model=True,
)
assert compiled_models == [api.model]
def test_user_can_read_top_class_and_other_class_scores( def test_user_can_read_top_class_and_other_class_scores(
api_v2: BatDetect2API, api_v2: BatDetect2API,
example_audio_files: list[Path], example_audio_files: list[Path],

View File

@ -61,3 +61,102 @@ def test_roundtrip(
).all() ).all()
assert (recovered_prediction.features == detection.features).all() assert (recovered_prediction.features == detection.features).all()
assert recovered_prediction.geometry == detection.geometry assert recovered_prediction.geometry == detection.geometry
def test_roundtrip_recovers_recording_metadata(
sample_formatter,
create_recording,
create_clip,
sample_targets: TargetProtocol,
tmp_path: Path,
):
recording = create_recording(
tags=[data.Tag(key="source", value="test-recorder")],
duration=2,
samplerate=384_000,
time_expansion=10,
)
clip = create_clip(recording=recording, start_time=0.25, end_time=0.75)
detection = Detection(
geometry=data.BoundingBox(
coordinates=[0.3, 45_000, 0.4, 70_000],
),
detection_score=0.5,
class_scores=np.ones(len(sample_targets.class_names)),
features=np.ones(32),
)
prediction = ClipDetections(clip=clip, detections=[detection])
path = tmp_path / "predictions"
sample_formatter.save(predictions=[prediction], path=path)
recovered = sample_formatter.load(path=path)
assert len(recovered) == 1
assert recovered[0].clip.recording.model_dump(mode="json") == (
recording.model_dump(mode="json")
)
def test_roundtrip_empty_detections(
sample_formatter,
clip: data.Clip,
tmp_path: Path,
):
prediction = ClipDetections(clip=clip, detections=[])
path = tmp_path / "predictions"
sample_formatter.save(predictions=[prediction], path=path)
recovered = sample_formatter.load(path=path)
assert len(recovered) == 1
assert recovered[0].detections == []
assert recovered[0].clip.uuid == prediction.clip.uuid
assert recovered[0].clip.start_time == prediction.clip.start_time
assert recovered[0].clip.end_time == prediction.clip.end_time
def test_roundtrip_loads_with_multiprocessing(
clip: data.Clip,
sample_targets: TargetProtocol,
tmp_path: Path,
):
save_formatter = build_output_formatter(
config=RawOutputConfig(),
targets=sample_targets,
)
load_formatter = build_output_formatter(
config=RawOutputConfig(n_jobs=2),
targets=sample_targets,
)
predictions = [
ClipDetections(
clip=data.Clip(
recording=clip.recording,
start_time=index,
end_time=index + 0.5,
),
detections=[
Detection(
geometry=data.BoundingBox(
coordinates=[index, 45_000, index + 0.1, 70_000],
),
detection_score=0.5,
class_scores=np.ones(len(sample_targets.class_names)),
features=np.ones(32),
)
],
)
for index in range(2)
]
path = tmp_path / "predictions"
save_formatter.save(predictions=predictions, path=path)
recovered = load_formatter.load(path=path)
assert len(recovered) == len(predictions)
assert {item.clip.uuid for item in recovered} == {
item.clip.uuid for item in predictions
}

View File

@ -3,6 +3,8 @@ from pathlib import Path
import pytest import pytest
from soundevent import data from soundevent import data
from batdetect2.api_v2 import BatDetect2API
from batdetect2.inference import InferenceConfig
from batdetect2.inference.batch import run_batch_inference from batdetect2.inference.batch import run_batch_inference
from batdetect2.targets import build_roi_mapping, build_targets from batdetect2.targets import build_roi_mapping, build_targets
from batdetect2.train import load_model_from_checkpoint from batdetect2.train import load_model_from_checkpoint
@ -53,3 +55,54 @@ def test_run_batch_inference_matches_single_clip_inference(
strict=True, strict=True,
): ):
assert_clip_detections_equal(batched, single) assert_clip_detections_equal(batched, single)
def test_run_batch_inference_compiles_detector_when_config_requests_compile(
example_annotations: list[data.ClipAnnotation],
record_detector_compilation,
) -> None:
api = BatDetect2API.from_config()
recorder = record_detector_compilation(api.model)
predictions = run_batch_inference(
api.model,
[example_annotations[0].clip],
targets=api.targets,
roi_mapper=api.roi_mapper,
audio_loader=api.audio_loader,
preprocessor=api.preprocessor,
output_transform=api.output_transform,
inference_config=InferenceConfig(compile_model=True),
batch_size=1,
num_workers=0,
)
assert predictions
assert recorder.compile_count == 1
assert recorder.call_count > 0
def test_run_batch_inference_does_not_recompile_compiled_detector(
example_annotations: list[data.ClipAnnotation],
record_detector_compilation,
) -> None:
api = BatDetect2API.from_config()
recorder = record_detector_compilation(api.model)
api.compile()
predictions = run_batch_inference(
api.model,
[example_annotations[0].clip],
targets=api.targets,
roi_mapper=api.roi_mapper,
audio_loader=api.audio_loader,
preprocessor=api.preprocessor,
output_transform=api.output_transform,
inference_config=InferenceConfig(compile_model=True),
batch_size=1,
num_workers=0,
)
assert predictions
assert recorder.compile_count == 1
assert recorder.call_count > 0

View File

@ -10,6 +10,8 @@ from hypothesis import strategies as st
from batdetect2 import api from batdetect2 import api
from batdetect2.detector import parameters from batdetect2.detector import parameters
from batdetect2.models.backbones import UNetBackboneConfig
from batdetect2.train import load_model_from_checkpoint
@settings(deadline=None, max_examples=5) @settings(deadline=None, max_examples=5)
@ -72,3 +74,16 @@ def test_can_import_model_without_pickle_on_test_data(
model=model_with_pickle, model=model_with_pickle,
) )
assert predictions_without_pickle == predictions_with_pickle assert predictions_without_pickle == predictions_with_pickle
def test_bundled_checkpoint_loads_with_current_config_schema() -> None:
"""Bundled checkpoints remain loadable after config schema changes."""
model, configs = load_model_from_checkpoint()
assert model.class_names
assert isinstance(configs.model.architecture, UNetBackboneConfig)
frequency_aggregation = (
configs.model.architecture.bottleneck.frequency_aggregation
)
assert frequency_aggregation is not None
assert frequency_aggregation.name

View File

@ -4,6 +4,8 @@ import torch
from batdetect2.models.blocks import ( from batdetect2.models.blocks import (
ConvBlock, ConvBlock,
ConvConfig, ConvConfig,
EfficientSelfAttention,
EfficientSelfAttentionConfig,
FreqCoordConvDownBlock, FreqCoordConvDownBlock,
FreqCoordConvDownConfig, FreqCoordConvDownConfig,
FreqCoordConvUpBlock, FreqCoordConvUpBlock,
@ -18,6 +20,8 @@ from batdetect2.models.blocks import (
StandardConvUpConfig, StandardConvUpConfig,
VerticalConv, VerticalConv,
VerticalConvConfig, VerticalConvConfig,
VerticalMean,
VerticalMeanConfig,
build_layer, build_layer,
) )
@ -99,6 +103,20 @@ def test_vertical_conv_forward_shape(dummy_input):
assert block.out_channels == out_channels assert block.out_channels == out_channels
def test_vertical_mean_forward_shape(dummy_input):
"""Test that VerticalMean collapses the height dimension to 1."""
in_channels = dummy_input.size(1)
input_height = dummy_input.size(2)
out_channels = 32
block = VerticalMean(in_channels, out_channels, input_height)
output = block(dummy_input)
assert output.shape == (2, out_channels, 1, 32)
assert block.out_channels == out_channels
assert block.get_output_height(input_height) == 1
def test_self_attention_forward_shape(dummy_bottleneck_input): def test_self_attention_forward_shape(dummy_bottleneck_input):
"""Test that SelfAttention maintains the exact shape.""" """Test that SelfAttention maintains the exact shape."""
in_channels = dummy_bottleneck_input.size(1) in_channels = dummy_bottleneck_input.size(1)
@ -113,6 +131,20 @@ def test_self_attention_forward_shape(dummy_bottleneck_input):
assert block.out_channels == in_channels assert block.out_channels == in_channels
def test_efficient_self_attention_forward_shape(dummy_bottleneck_input):
"""Test that EfficientSelfAttention maintains the exact shape."""
in_channels = dummy_bottleneck_input.size(1)
attention_channels = 32
block = EfficientSelfAttention(
in_channels=in_channels, attention_channels=attention_channels
)
output = block(dummy_bottleneck_input)
assert output.shape == dummy_bottleneck_input.shape
assert block.out_channels == in_channels
def test_self_attention_weights(dummy_bottleneck_input): def test_self_attention_weights(dummy_bottleneck_input):
"""Test that attention weights sum to 1 over the time sequence.""" """Test that attention weights sum to 1 over the time sequence."""
in_channels = dummy_bottleneck_input.size(1) in_channels = dummy_bottleneck_input.size(1)
@ -131,6 +163,51 @@ def test_self_attention_weights(dummy_bottleneck_input):
assert torch.allclose(sum_weights, torch.ones_like(sum_weights), atol=1e-5) assert torch.allclose(sum_weights, torch.ones_like(sum_weights), atol=1e-5)
def test_efficient_self_attention_matches_self_attention_with_copied_weights(
dummy_bottleneck_input,
):
"""Temporarily compare efficient and original attention outputs."""
in_channels = dummy_bottleneck_input.size(1)
attention_channels = 32
block = SelfAttention(
in_channels=in_channels,
attention_channels=attention_channels,
)
efficient_block = EfficientSelfAttention(
in_channels=in_channels,
attention_channels=attention_channels,
)
# Match the fused QKV projection to the original separate projections.
with torch.no_grad():
efficient_block.qkv_proj.weight[:attention_channels].copy_(
block.query_fun.weight
)
efficient_block.qkv_proj.bias[:attention_channels].copy_(
block.query_fun.bias
)
efficient_block.qkv_proj.weight[
attention_channels : 2 * attention_channels
].copy_(block.key_fun.weight)
efficient_block.qkv_proj.bias[
attention_channels : 2 * attention_channels
].copy_(block.key_fun.bias)
efficient_block.qkv_proj.weight[2 * attention_channels :].copy_(
block.value_fun.weight
)
efficient_block.qkv_proj.bias[2 * attention_channels :].copy_(
block.value_fun.bias
)
efficient_block.pro_fun.weight.copy_(block.pro_fun.weight)
efficient_block.pro_fun.bias.copy_(block.pro_fun.bias)
output = block(dummy_bottleneck_input)
efficient_output = efficient_block(dummy_bottleneck_input)
torch.testing.assert_close(output, efficient_output)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"layer_config, expected_type", "layer_config, expected_type",
[ [
@ -140,7 +217,12 @@ def test_self_attention_weights(dummy_bottleneck_input):
(FreqCoordConvDownConfig(out_channels=32), FreqCoordConvDownBlock), (FreqCoordConvDownConfig(out_channels=32), FreqCoordConvDownBlock),
(FreqCoordConvUpConfig(out_channels=32), FreqCoordConvUpBlock), (FreqCoordConvUpConfig(out_channels=32), FreqCoordConvUpBlock),
(SelfAttentionConfig(attention_channels=32), SelfAttention), (SelfAttentionConfig(attention_channels=32), SelfAttention),
(
EfficientSelfAttentionConfig(attention_channels=32),
EfficientSelfAttention,
),
(VerticalConvConfig(channels=32), VerticalConv), (VerticalConvConfig(channels=32), VerticalConv),
(VerticalMeanConfig(channels=32), VerticalMean),
], ],
) )
def test_build_layer_factory(layer_config, expected_type): def test_build_layer_factory(layer_config, expected_type):

View File

@ -0,0 +1,55 @@
import torch
from batdetect2.models.blocks import (
SelfAttention,
SelfAttentionConfig,
VerticalMeanConfig,
)
from batdetect2.models.bottleneck import (
Bottleneck,
BottleneckConfig,
build_bottleneck,
)
def test_bottleneck_layers_use_frequency_aggregation_channels() -> None:
"""Layers after frequency aggregation are built for aggregated channels."""
config = BottleneckConfig(
channels=128,
frequency_aggregation=VerticalMeanConfig(channels=128),
layers=[SelfAttentionConfig(attention_channels=32)],
)
bottleneck = build_bottleneck(
input_height=8,
in_channels=64,
config=config,
)
assert isinstance(bottleneck, Bottleneck)
attention = bottleneck.layers[0]
assert isinstance(attention, SelfAttention)
assert attention.in_channels == 128
output = bottleneck(torch.randn(2, 64, 8, 10))
assert output.shape == (2, 128, 8, 10)
def test_bottleneck_default_frequency_aggregation_matches_channels() -> None:
"""Minimal configs keep advertised and actual output channels in sync."""
config = BottleneckConfig(channels=128, layers=[])
bottleneck = build_bottleneck(
input_height=8,
in_channels=64,
config=config,
)
assert isinstance(bottleneck, Bottleneck)
assert bottleneck.out_channels == 128
assert bottleneck.conv_vert.out_channels == 128
output = bottleneck(torch.randn(2, 64, 8, 10))
assert output.shape == (2, bottleneck.out_channels, 8, 10)

View File

@ -49,6 +49,16 @@ def build_default_module(
) )
def build_fast_train_config() -> TrainingConfig:
train_config = TrainingConfig()
train_config.trainer.limit_train_batches = 1
train_config.trainer.limit_val_batches = 1
train_config.trainer.log_every_n_steps = 1
train_config.train_loader.batch_size = 1
train_config.train_loader.augmentations.enabled = False
return train_config
def test_can_initialize_default_module(): def test_can_initialize_default_module():
module = build_default_module() module = build_default_module()
assert isinstance(module, L.LightningModule) assert isinstance(module, L.LightningModule)
@ -271,19 +281,7 @@ def test_train_smoke_produces_loadable_checkpoint(
sample_audio_loader: AudioLoader, sample_audio_loader: AudioLoader,
): ):
# Given # Given
train_config = TrainingConfig.model_validate( train_config = build_fast_train_config()
{
"trainer": {
"limit_train_batches": 1,
"limit_val_batches": 1,
"log_every_n_steps": 1,
},
"train_loader": {
"batch_size": 1,
"augmentations": {"enabled": False},
},
}
)
# When # When
run_train( run_train(
@ -310,6 +308,47 @@ def test_train_smoke_produces_loadable_checkpoint(
assert outputs is not None assert outputs is not None
@pytest.mark.slow
def test_run_train_compiles_detector_when_train_config_requests_compile(
tmp_path: Path,
example_annotations: list[data.ClipAnnotation],
record_detector_compilation,
) -> None:
targets_config = TargetConfig()
targets = build_targets(targets_config)
roi_mapper = build_roi_mapping(targets_config.roi)
model = build_model(
ModelConfig(),
class_names=targets.class_names,
dimension_names=roi_mapper.dimension_names,
)
train_config = build_fast_train_config()
train_config.compile_model = True
recorder = record_detector_compilation(model)
module = run_train(
train_annotations=example_annotations[:1],
val_annotations=example_annotations[:1],
model=model,
targets=targets,
roi_mapper=roi_mapper,
targets_config=targets_config,
train_config=train_config,
num_epochs=1,
train_workers=0,
val_workers=0,
checkpoint_dir=tmp_path / "checkpoints",
log_dir=tmp_path / "logs",
seed=0,
)
assert (
getattr(module.model.detector, "_compiled_call_impl", None) is not None
)
assert recorder.compile_count == 1
assert recorder.call_count > 0
def test_build_training_module_uses_provided_model() -> None: def test_build_training_module_uses_provided_model() -> None:
targets = build_targets(TargetConfig()) targets = build_targets(TargetConfig())
roi_mapper = build_roi_mapping(TargetConfig().roi) roi_mapper = build_roi_mapping(TargetConfig().roi)

394
uv.lock generated
View File

@ -453,7 +453,7 @@ wheels = [
[[package]] [[package]]
name = "batdetect2" name = "batdetect2"
version = "2.0.0b1" version = "2.0.0b2"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "click" }, { name = "click" },
@ -557,8 +557,8 @@ requires-dist = [
{ name = "soundevent", extras = ["audio", "geometry", "plot"], specifier = ">=2.10.0" }, { name = "soundevent", extras = ["audio", "geometry", "plot"], specifier = ">=2.10.0" },
{ name = "soundfile", specifier = ">=0.12.1" }, { name = "soundfile", specifier = ">=0.12.1" },
{ name = "tensorboard", specifier = ">=2.16.2" }, { name = "tensorboard", specifier = ">=2.16.2" },
{ name = "torch", specifier = ">=1.13.1" }, { name = "torch", specifier = ">=2.0.0" },
{ name = "torchaudio", specifier = ">=1.13.1" }, { name = "torchaudio", specifier = ">=2.0.0" },
{ name = "xarray", specifier = ">=2024.0.0" }, { name = "xarray", specifier = ">=2024.0.0" },
] ]
@ -626,10 +626,10 @@ name = "bitsandbytes"
version = "0.49.2" version = "0.49.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "packaging" },
{ name = "torch", marker = "sys_platform == 'darwin' or sys_platform == 'linux' or sys_platform == 'win32'" }, { name = "torch" },
] ]
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/d8/7d/f1fe0992334b18cd8494f89aeec1dcc674635584fcd9f115784fea3a1d05/bitsandbytes-0.49.2-py3-none-macosx_14_0_arm64.whl", hash = "sha256:87be5975edeac5396d699ecbc39dfc47cf2c026daaf2d5852a94368611a6823f", size = 131940, upload-time = "2026-02-16T21:26:04.572Z" }, { url = "https://files.pythonhosted.org/packages/d8/7d/f1fe0992334b18cd8494f89aeec1dcc674635584fcd9f115784fea3a1d05/bitsandbytes-0.49.2-py3-none-macosx_14_0_arm64.whl", hash = "sha256:87be5975edeac5396d699ecbc39dfc47cf2c026daaf2d5852a94368611a6823f", size = 131940, upload-time = "2026-02-16T21:26:04.572Z" },
@ -1070,7 +1070,7 @@ resolution-markers = [
"python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'",
] ]
dependencies = [ dependencies = [
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" }
wheels = [ wheels = [
@ -1151,7 +1151,7 @@ resolution-markers = [
"python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'",
] ]
dependencies = [ dependencies = [
{ name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" } },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" }
wheels = [ wheels = [
@ -1345,7 +1345,7 @@ name = "cuda-bindings"
version = "13.2.0" version = "13.2.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "cuda-pathfinder", marker = "(platform_machine != 'ARM64' and sys_platform == 'win32') or sys_platform == 'linux'" }, { name = "cuda-pathfinder" },
] ]
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/1a/fe/7351d7e586a8b4c9f89731bfe4cf0148223e8f9903ff09571f78b3fb0682/cuda_bindings-13.2.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08b395f79cb89ce0cd8effff07c4a1e20101b873c256a1aeb286e8fd7bd0f556", size = 5744254, upload-time = "2026-03-11T00:12:29.798Z" }, { url = "https://files.pythonhosted.org/packages/1a/fe/7351d7e586a8b4c9f89731bfe4cf0148223e8f9903ff09571f78b3fb0682/cuda_bindings-13.2.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08b395f79cb89ce0cd8effff07c4a1e20101b873c256a1aeb286e8fd7bd0f556", size = 5744254, upload-time = "2026-03-11T00:12:29.798Z" },
@ -1376,37 +1376,37 @@ wheels = [
[package.optional-dependencies] [package.optional-dependencies]
cublas = [ cublas = [
{ name = "nvidia-cublas", marker = "(platform_machine != 'ARM64' and sys_platform == 'win32') or sys_platform == 'linux'" }, { name = "nvidia-cublas" },
] ]
cudart = [ cudart = [
{ name = "nvidia-cuda-runtime", marker = "(platform_machine != 'ARM64' and sys_platform == 'win32') or sys_platform == 'linux'" }, { name = "nvidia-cuda-runtime" },
] ]
cufft = [ cufft = [
{ name = "nvidia-cufft", marker = "(platform_machine != 'ARM64' and sys_platform == 'win32') or sys_platform == 'linux'" }, { name = "nvidia-cufft" },
] ]
cufile = [ cufile = [
{ name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, { name = "nvidia-cufile" },
] ]
cupti = [ cupti = [
{ name = "nvidia-cuda-cupti", marker = "(platform_machine != 'ARM64' and sys_platform == 'win32') or sys_platform == 'linux'" }, { name = "nvidia-cuda-cupti" },
] ]
curand = [ curand = [
{ name = "nvidia-curand", marker = "(platform_machine != 'ARM64' and sys_platform == 'win32') or sys_platform == 'linux'" }, { name = "nvidia-curand" },
] ]
cusolver = [ cusolver = [
{ name = "nvidia-cusolver", marker = "(platform_machine != 'ARM64' and sys_platform == 'win32') or sys_platform == 'linux'" }, { name = "nvidia-cusolver" },
] ]
cusparse = [ cusparse = [
{ name = "nvidia-cusparse", marker = "(platform_machine != 'ARM64' and sys_platform == 'win32') or sys_platform == 'linux'" }, { name = "nvidia-cusparse" },
] ]
nvjitlink = [ nvjitlink = [
{ name = "nvidia-nvjitlink", marker = "(platform_machine != 'ARM64' and sys_platform == 'win32') or sys_platform == 'linux'" }, { name = "nvidia-nvjitlink" },
] ]
nvrtc = [ nvrtc = [
{ name = "nvidia-cuda-nvrtc", marker = "(platform_machine != 'ARM64' and sys_platform == 'win32') or sys_platform == 'linux'" }, { name = "nvidia-cuda-nvrtc" },
] ]
nvtx = [ nvtx = [
{ name = "nvidia-nvtx", marker = "(platform_machine != 'ARM64' and sys_platform == 'win32') or sys_platform == 'linux'" }, { name = "nvidia-nvtx" },
] ]
[[package]] [[package]]
@ -1875,7 +1875,7 @@ name = "exceptiongroup"
version = "1.3.1" version = "1.3.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
wheels = [ wheels = [
@ -2417,7 +2417,7 @@ name = "gunicorn"
version = "25.3.0" version = "25.3.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "packaging", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" }, { name = "packaging" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/c4/f4/e78fa054248fab913e2eab0332c6c2cb07421fca1ce56d8fe43b6aef57a4/gunicorn-25.3.0.tar.gz", hash = "sha256:f74e1b2f9f76f6cd1ca01198968bd2dd65830edc24b6e8e4d78de8320e2fe889", size = 634883, upload-time = "2026-03-27T00:00:26.092Z" } sdist = { url = "https://files.pythonhosted.org/packages/c4/f4/e78fa054248fab913e2eab0332c6c2cb07421fca1ce56d8fe43b6aef57a4/gunicorn-25.3.0.tar.gz", hash = "sha256:f74e1b2f9f76f6cd1ca01198968bd2dd65830edc24b6e8e4d78de8320e2fe889", size = 634883, upload-time = "2026-03-27T00:00:26.092Z" }
wheels = [ wheels = [
@ -2638,17 +2638,17 @@ resolution-markers = [
"python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'",
] ]
dependencies = [ dependencies = [
{ name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, { name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "decorator", marker = "python_full_version < '3.11'" }, { name = "decorator" },
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "exceptiongroup" },
{ name = "jedi", marker = "python_full_version < '3.11'" }, { name = "jedi" },
{ name = "matplotlib-inline", marker = "python_full_version < '3.11'" }, { name = "matplotlib-inline" },
{ name = "pexpect", marker = "python_full_version < '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
{ name = "prompt-toolkit", marker = "python_full_version < '3.11'" }, { name = "prompt-toolkit" },
{ name = "pygments", marker = "python_full_version < '3.11'" }, { name = "pygments" },
{ name = "stack-data", marker = "python_full_version < '3.11'" }, { name = "stack-data" },
{ name = "traitlets", marker = "python_full_version < '3.11'" }, { name = "traitlets" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/40/18/f8598d287006885e7136451fdea0755af4ebcbfe342836f24deefaed1164/ipython-8.39.0.tar.gz", hash = "sha256:4110ae96012c379b8b6db898a07e186c40a2a1ef5d57a7fa83166047d9da7624", size = 5513971, upload-time = "2026-03-27T10:02:13.94Z" } sdist = { url = "https://files.pythonhosted.org/packages/40/18/f8598d287006885e7136451fdea0755af4ebcbfe342836f24deefaed1164/ipython-8.39.0.tar.gz", hash = "sha256:4110ae96012c379b8b6db898a07e186c40a2a1ef5d57a7fa83166047d9da7624", size = 5513971, upload-time = "2026-03-27T10:02:13.94Z" }
wheels = [ wheels = [
@ -2674,18 +2674,18 @@ resolution-markers = [
"python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'",
] ]
dependencies = [ dependencies = [
{ name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, { name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "decorator", marker = "python_full_version >= '3.11'" }, { name = "decorator" },
{ name = "ipython-pygments-lexers", marker = "python_full_version >= '3.11'" }, { name = "ipython-pygments-lexers" },
{ name = "jedi", marker = "python_full_version >= '3.11'" }, { name = "jedi" },
{ name = "matplotlib-inline", marker = "python_full_version >= '3.11'" }, { name = "matplotlib-inline" },
{ name = "pexpect", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
{ name = "prompt-toolkit", marker = "python_full_version >= '3.11'" }, { name = "prompt-toolkit" },
{ name = "psutil", marker = "python_full_version >= '3.11'" }, { name = "psutil" },
{ name = "pygments", marker = "python_full_version >= '3.11'" }, { name = "pygments" },
{ name = "stack-data", marker = "python_full_version >= '3.11'" }, { name = "stack-data" },
{ name = "traitlets", marker = "python_full_version >= '3.11'" }, { name = "traitlets" },
{ name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, { name = "typing-extensions", marker = "python_full_version < '3.12'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/87cda5842cf5c31837c06ddb588e11c3c35d8ece89b7a0108c06b8c9b00a/ipython-9.13.0.tar.gz", hash = "sha256:7e834b6afc99f020e3f05966ced34792f40267d64cb1ea9043886dab0dde5967", size = 4430549, upload-time = "2026-04-24T12:24:55.221Z" } sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/87cda5842cf5c31837c06ddb588e11c3c35d8ece89b7a0108c06b8c9b00a/ipython-9.13.0.tar.gz", hash = "sha256:7e834b6afc99f020e3f05966ced34792f40267d64cb1ea9043886dab0dde5967", size = 4430549, upload-time = "2026-04-24T12:24:55.221Z" }
wheels = [ wheels = [
@ -2697,7 +2697,7 @@ name = "ipython-pygments-lexers"
version = "1.1.1" version = "1.1.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "pygments", marker = "python_full_version >= '3.11'" }, { name = "pygments" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" }
wheels = [ wheels = [
@ -3460,7 +3460,7 @@ resolution-markers = [
"python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'",
] ]
dependencies = [ dependencies = [
{ name = "mdurl", marker = "python_full_version < '3.11'" }, { name = "mdurl" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" }
wheels = [ wheels = [
@ -3486,7 +3486,7 @@ resolution-markers = [
"python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'",
] ]
dependencies = [ dependencies = [
{ name = "mdurl", marker = "python_full_version >= '3.11'" }, { name = "mdurl" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" }
wheels = [ wheels = [
@ -3961,12 +3961,12 @@ resolution-markers = [
"python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'",
] ]
dependencies = [ dependencies = [
{ name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" } },
{ name = "jinja2", marker = "python_full_version < '3.11'" }, { name = "jinja2" },
{ name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" } },
{ name = "mdit-py-plugins", marker = "python_full_version < '3.11'" }, { name = "mdit-py-plugins" },
{ name = "pyyaml", marker = "python_full_version < '3.11'" }, { name = "pyyaml" },
{ name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" } },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/66/a5/9626ba4f73555b3735ad86247a8077d4603aa8628537687c839ab08bfe44/myst_parser-4.0.1.tar.gz", hash = "sha256:5cfea715e4f3574138aecbf7d54132296bfd72bb614d31168f48c477a830a7c4", size = 93985, upload-time = "2025-02-12T10:53:03.833Z" } sdist = { url = "https://files.pythonhosted.org/packages/66/a5/9626ba4f73555b3735ad86247a8077d4603aa8628537687c839ab08bfe44/myst_parser-4.0.1.tar.gz", hash = "sha256:5cfea715e4f3574138aecbf7d54132296bfd72bb614d31168f48c477a830a7c4", size = 93985, upload-time = "2025-02-12T10:53:03.833Z" }
wheels = [ wheels = [
@ -3992,12 +3992,12 @@ resolution-markers = [
"python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'",
] ]
dependencies = [ dependencies = [
{ name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" } },
{ name = "jinja2", marker = "python_full_version >= '3.11'" }, { name = "jinja2" },
{ name = "markdown-it-py", version = "4.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "markdown-it-py", version = "4.2.0", source = { registry = "https://pypi.org/simple" } },
{ name = "mdit-py-plugins", marker = "python_full_version >= '3.11'" }, { name = "mdit-py-plugins" },
{ name = "pyyaml", marker = "python_full_version >= '3.11'" }, { name = "pyyaml" },
{ name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
{ name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/33/fa/7b45eef11b7971f0beb29d27b7bfe0d747d063aa29e170d9edd004733c8a/myst_parser-5.0.0.tar.gz", hash = "sha256:f6f231452c56e8baa662cc352c548158f6a16fcbd6e3800fc594978002b94f3a", size = 98535, upload-time = "2026-01-15T09:08:18.036Z" } sdist = { url = "https://files.pythonhosted.org/packages/33/fa/7b45eef11b7971f0beb29d27b7bfe0d747d063aa29e170d9edd004733c8a/myst_parser-5.0.0.tar.gz", hash = "sha256:f6f231452c56e8baa662cc352c548158f6a16fcbd6e3800fc594978002b94f3a", size = 98535, upload-time = "2026-01-15T09:08:18.036Z" }
@ -4086,9 +4086,9 @@ resolution-markers = [
"python_full_version < '3.11' and platform_machine == 'ARM64' and sys_platform == 'win32'", "python_full_version < '3.11' and platform_machine == 'ARM64' and sys_platform == 'win32'",
] ]
dependencies = [ dependencies = [
{ name = "certifi", marker = "python_full_version < '3.11' and platform_machine == 'ARM64' and sys_platform == 'win32'" }, { name = "certifi" },
{ name = "cftime", marker = "python_full_version < '3.11' and platform_machine == 'ARM64' and sys_platform == 'win32'" }, { name = "cftime" },
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'ARM64' and sys_platform == 'win32'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/0e/76/7bc801796dee752c1ce9cd6935564a6ee79d5c9d9ef9192f57b156495a35/netcdf4-1.7.3.tar.gz", hash = "sha256:83f122fc3415e92b1d4904fd6a0898468b5404c09432c34beb6b16c533884673", size = 836095, upload-time = "2025-10-13T18:38:00.76Z" } sdist = { url = "https://files.pythonhosted.org/packages/0e/76/7bc801796dee752c1ce9cd6935564a6ee79d5c9d9ef9192f57b156495a35/netcdf4-1.7.3.tar.gz", hash = "sha256:83f122fc3415e92b1d4904fd6a0898468b5404c09432c34beb6b16c533884673", size = 836095, upload-time = "2025-10-13T18:38:00.76Z" }
@ -4114,8 +4114,8 @@ resolution-markers = [
"python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'",
] ]
dependencies = [ dependencies = [
{ name = "certifi", marker = "python_full_version >= '3.11' or platform_machine != 'ARM64' or sys_platform != 'win32'" }, { name = "certifi" },
{ name = "cftime", marker = "python_full_version >= '3.11' or platform_machine != 'ARM64' or sys_platform != 'win32'" }, { name = "cftime" },
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'ARM64') or (python_full_version < '3.11' and sys_platform != 'win32')" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'ARM64') or (python_full_version < '3.11' and sys_platform != 'win32')" },
{ name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
] ]
@ -4427,7 +4427,7 @@ name = "nvidia-cudnn-cu13"
version = "9.19.0.56" version = "9.19.0.56"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "nvidia-cublas", marker = "(platform_machine != 'ARM64' and sys_platform == 'win32') or sys_platform == 'linux'" }, { name = "nvidia-cublas" },
] ]
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/f1/84/26025437c1e6b61a707442184fa0c03d083b661adf3a3eecfd6d21677740/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:6ed29ffaee1176c612daf442e4dd6cfeb6a0caa43ddcbeb59da94953030b1be4", size = 433781201, upload-time = "2026-02-03T20:40:53.805Z" }, { url = "https://files.pythonhosted.org/packages/f1/84/26025437c1e6b61a707442184fa0c03d083b661adf3a3eecfd6d21677740/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:6ed29ffaee1176c612daf442e4dd6cfeb6a0caa43ddcbeb59da94953030b1be4", size = 433781201, upload-time = "2026-02-03T20:40:53.805Z" },
@ -4439,7 +4439,7 @@ name = "nvidia-cufft"
version = "12.0.0.61" version = "12.0.0.61"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "nvidia-nvjitlink", marker = "(platform_machine != 'ARM64' and sys_platform == 'win32') or sys_platform == 'linux'" }, { name = "nvidia-nvjitlink" },
] ]
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" },
@ -4469,9 +4469,9 @@ name = "nvidia-cusolver"
version = "12.0.4.66" version = "12.0.4.66"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "nvidia-cublas", marker = "(platform_machine != 'ARM64' and sys_platform == 'win32') or sys_platform == 'linux'" }, { name = "nvidia-cublas" },
{ name = "nvidia-cusparse", marker = "(platform_machine != 'ARM64' and sys_platform == 'win32') or sys_platform == 'linux'" }, { name = "nvidia-cusparse" },
{ name = "nvidia-nvjitlink", marker = "(platform_machine != 'ARM64' and sys_platform == 'win32') or sys_platform == 'linux'" }, { name = "nvidia-nvjitlink" },
] ]
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" },
@ -4483,7 +4483,7 @@ name = "nvidia-cusparse"
version = "12.6.3.3" version = "12.6.3.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "nvidia-nvjitlink", marker = "(platform_machine != 'ARM64' and sys_platform == 'win32') or sys_platform == 'linux'" }, { name = "nvidia-nvjitlink" },
] ]
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" },
@ -4762,8 +4762,8 @@ resolution-markers = [
"python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'",
] ]
dependencies = [ dependencies = [
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } },
{ name = "types-pytz", marker = "python_full_version < '3.11'" }, { name = "types-pytz" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/92/5d/be23854a73fda69f1dbdda7bc10fbd6f930bd1fa87aaec389f00c901c1e8/pandas_stubs-2.3.3.260113.tar.gz", hash = "sha256:076e3724bcaa73de78932b012ec64b3010463d377fa63116f4e6850643d93800", size = 116131, upload-time = "2026-01-13T22:30:16.704Z" } sdist = { url = "https://files.pythonhosted.org/packages/92/5d/be23854a73fda69f1dbdda7bc10fbd6f930bd1fa87aaec389f00c901c1e8/pandas_stubs-2.3.3.260113.tar.gz", hash = "sha256:076e3724bcaa73de78932b012ec64b3010463d377fa63116f4e6850643d93800", size = 116131, upload-time = "2026-01-13T22:30:16.704Z" }
wheels = [ wheels = [
@ -4789,7 +4789,7 @@ resolution-markers = [
"python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'",
] ]
dependencies = [ dependencies = [
{ name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" } },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/27/1d/297ff2c7ea50a768a2247621d6451abb2a07c0e9be7ca6d36ebe371658e5/pandas_stubs-3.0.0.260204.tar.gz", hash = "sha256:bf9294b76352effcffa9cb85edf0bed1339a7ec0c30b8e1ac3d66b4228f1fbc3", size = 109383, upload-time = "2026-02-04T15:17:17.247Z" } sdist = { url = "https://files.pythonhosted.org/packages/27/1d/297ff2c7ea50a768a2247621d6451abb2a07c0e9be7ca6d36ebe371658e5/pandas_stubs-3.0.0.260204.tar.gz", hash = "sha256:bf9294b76352effcffa9cb85edf0bed1339a7ec0c30b8e1ac3d66b4228f1fbc3", size = 109383, upload-time = "2026-02-04T15:17:17.247Z" }
wheels = [ wheels = [
@ -4828,7 +4828,7 @@ name = "pexpect"
version = "4.9.0" version = "4.9.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "ptyprocess", marker = "platform_machine != 'ARM64' or sys_platform != 'win32'" }, { name = "ptyprocess" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" }
wheels = [ wheels = [
@ -5311,14 +5311,14 @@ resolution-markers = [
"python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'",
] ]
dependencies = [ dependencies = [
{ name = "accessible-pygments", marker = "python_full_version < '3.11'" }, { name = "accessible-pygments" },
{ name = "babel", marker = "python_full_version < '3.11'" }, { name = "babel" },
{ name = "beautifulsoup4", marker = "python_full_version < '3.11'" }, { name = "beautifulsoup4" },
{ name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" } },
{ name = "packaging", marker = "python_full_version < '3.11'" }, { name = "packaging" },
{ name = "pygments", marker = "python_full_version < '3.11'" }, { name = "pygments" },
{ name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" } },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/67/ea/3ab478cccacc2e8ef69892c42c44ae547bae089f356c4b47caf61730958d/pydata_sphinx_theme-0.15.4.tar.gz", hash = "sha256:7762ec0ac59df3acecf49fd2f889e1b4565dbce8b88b2e29ee06fdd90645a06d", size = 2400673, upload-time = "2024-06-25T19:28:45.041Z" } sdist = { url = "https://files.pythonhosted.org/packages/67/ea/3ab478cccacc2e8ef69892c42c44ae547bae089f356c4b47caf61730958d/pydata_sphinx_theme-0.15.4.tar.gz", hash = "sha256:7762ec0ac59df3acecf49fd2f889e1b4565dbce8b88b2e29ee06fdd90645a06d", size = 2400673, upload-time = "2024-06-25T19:28:45.041Z" }
wheels = [ wheels = [
@ -5344,14 +5344,14 @@ resolution-markers = [
"python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'",
] ]
dependencies = [ dependencies = [
{ name = "accessible-pygments", marker = "python_full_version >= '3.11'" }, { name = "accessible-pygments" },
{ name = "babel", marker = "python_full_version >= '3.11'" }, { name = "babel" },
{ name = "beautifulsoup4", marker = "python_full_version >= '3.11'" }, { name = "beautifulsoup4" },
{ name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" } },
{ name = "pygments", marker = "python_full_version >= '3.11'" }, { name = "pygments" },
{ name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
{ name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
{ name = "typing-extensions", marker = "python_full_version >= '3.11'" }, { name = "typing-extensions" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/00/20/bb50f9de3a6de69e6abd6b087b52fa2418a0418b19597601605f855ad044/pydata_sphinx_theme-0.16.1.tar.gz", hash = "sha256:a08b7f0b7f70387219dc659bff0893a7554d5eb39b59d3b8ef37b8401b7642d7", size = 2412693, upload-time = "2024-12-17T10:53:39.537Z" } sdist = { url = "https://files.pythonhosted.org/packages/00/20/bb50f9de3a6de69e6abd6b087b52fa2418a0418b19597601605f855ad044/pydata_sphinx_theme-0.16.1.tar.gz", hash = "sha256:a08b7f0b7f70387219dc659bff0893a7554d5eb39b59d3b8ef37b8401b7642d7", size = 2412693, upload-time = "2024-12-17T10:53:39.537Z" }
wheels = [ wheels = [
@ -5390,7 +5390,7 @@ resolution-markers = [
"python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'",
] ]
dependencies = [ dependencies = [
{ name = "cffi", marker = "python_full_version < '3.11'" }, { name = "cffi" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/2e/ea/762d00f6f518423cd889e39b12028844cc95f91a6413cf7136e184864821/pygit2-1.18.2.tar.gz", hash = "sha256:eca87e0662c965715b7f13491d5e858df2c0908341dee9bde2bc03268e460f55", size = 797200, upload-time = "2025-08-16T13:52:36.853Z" } sdist = { url = "https://files.pythonhosted.org/packages/2e/ea/762d00f6f518423cd889e39b12028844cc95f91a6413cf7136e184864821/pygit2-1.18.2.tar.gz", hash = "sha256:eca87e0662c965715b7f13491d5e858df2c0908341dee9bde2bc03268e460f55", size = 797200, upload-time = "2025-08-16T13:52:36.853Z" }
wheels = [ wheels = [
@ -5451,7 +5451,7 @@ resolution-markers = [
"python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'",
] ]
dependencies = [ dependencies = [
{ name = "cffi", marker = "python_full_version >= '3.11'" }, { name = "cffi" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/3a/a4/10ce00feef5c43eddacab19ae6610c4d4ef3ab77e544e9ee938772cd1c17/pygit2-1.19.2.tar.gz", hash = "sha256:cbeb3dbca9ca6ee3d5ea5d02f5e844c2d6084a2d5d6621e3e06aa2b11c645bfd", size = 803448, upload-time = "2026-03-29T14:57:27.565Z" } sdist = { url = "https://files.pythonhosted.org/packages/3a/a4/10ce00feef5c43eddacab19ae6610c4d4ef3ab77e544e9ee938772cd1c17/pygit2-1.19.2.tar.gz", hash = "sha256:cbeb3dbca9ca6ee3d5ea5d02f5e844c2d6084a2d5d6621e3e06aa2b11c645bfd", size = 803448, upload-time = "2026-03-29T14:57:27.565Z" }
wheels = [ wheels = [
@ -5880,15 +5880,15 @@ resolution-markers = [
"python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'",
] ]
dependencies = [ dependencies = [
{ name = "affine", marker = "python_full_version < '3.12'" }, { name = "affine" },
{ name = "attrs", marker = "python_full_version < '3.12'" }, { name = "attrs" },
{ name = "certifi", marker = "python_full_version < '3.12'" }, { name = "certifi" },
{ name = "click", marker = "python_full_version < '3.12'" }, { name = "click" },
{ name = "click-plugins", marker = "python_full_version < '3.12'" }, { name = "click-plugins" },
{ name = "cligj", marker = "python_full_version < '3.12'" }, { name = "cligj" },
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version != '3.11.*'" },
{ name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
{ name = "pyparsing", marker = "python_full_version < '3.12'" }, { name = "pyparsing" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/ec/fa/fce8dc9f09e5bc6520b6fc1b4ecfa510af9ca06eb42ad7bdff9c9b8989d0/rasterio-1.4.4.tar.gz", hash = "sha256:c95424e2c7f009b8f7df1095d645c52895cd332c0c2e1b4c2e073ea28b930320", size = 445004, upload-time = "2025-12-12T18:01:08.971Z" } sdist = { url = "https://files.pythonhosted.org/packages/ec/fa/fce8dc9f09e5bc6520b6fc1b4ecfa510af9ca06eb42ad7bdff9c9b8989d0/rasterio-1.4.4.tar.gz", hash = "sha256:c95424e2c7f009b8f7df1095d645c52895cd332c0c2e1b4c2e073ea28b930320", size = 445004, upload-time = "2025-12-12T18:01:08.971Z" }
wheels = [ wheels = [
@ -5938,13 +5938,13 @@ resolution-markers = [
"python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'",
] ]
dependencies = [ dependencies = [
{ name = "affine", marker = "python_full_version >= '3.12'" }, { name = "affine" },
{ name = "attrs", marker = "python_full_version >= '3.12'" }, { name = "attrs" },
{ name = "certifi", marker = "python_full_version >= '3.12'" }, { name = "certifi" },
{ name = "click", marker = "python_full_version >= '3.12'" }, { name = "click" },
{ name = "cligj", marker = "python_full_version >= '3.12'" }, { name = "cligj" },
{ name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" } },
{ name = "pyparsing", marker = "python_full_version >= '3.12'" }, { name = "pyparsing" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/f6/88/edb4b66b6cb2c13f123af5a3896bf70c0cbe73ab3cd4243cb4eb0212a0f6/rasterio-1.5.0.tar.gz", hash = "sha256:1e0ea56b02eea4989b36edf8e58a5a3ef40e1b7edcb04def2603accd5ab3ee7b", size = 452184, upload-time = "2026-01-05T16:06:47.169Z" } sdist = { url = "https://files.pythonhosted.org/packages/f6/88/edb4b66b6cb2c13f123af5a3896bf70c0cbe73ab3cd4243cb4eb0212a0f6/rasterio-1.5.0.tar.gz", hash = "sha256:1e0ea56b02eea4989b36edf8e58a5a3ef40e1b7edcb04def2603accd5ab3ee7b", size = 452184, upload-time = "2026-01-05T16:06:47.169Z" }
wheels = [ wheels = [
@ -6228,10 +6228,10 @@ resolution-markers = [
"python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'",
] ]
dependencies = [ dependencies = [
{ name = "joblib", marker = "python_full_version < '3.11'" }, { name = "joblib" },
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } },
{ name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" } },
{ name = "threadpoolctl", marker = "python_full_version < '3.11'" }, { name = "threadpoolctl" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" }
wheels = [ wheels = [
@ -6281,10 +6281,10 @@ resolution-markers = [
"python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'",
] ]
dependencies = [ dependencies = [
{ name = "joblib", marker = "python_full_version >= '3.11'" }, { name = "joblib" },
{ name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" } },
{ name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" } },
{ name = "threadpoolctl", marker = "python_full_version >= '3.11'" }, { name = "threadpoolctl" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" }
wheels = [ wheels = [
@ -6325,7 +6325,7 @@ resolution-markers = [
"python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'",
] ]
dependencies = [ dependencies = [
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" }
wheels = [ wheels = [
@ -6395,7 +6395,7 @@ resolution-markers = [
"python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'",
] ]
dependencies = [ dependencies = [
{ name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" } },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" }
wheels = [ wheels = [
@ -6748,23 +6748,23 @@ resolution-markers = [
"python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'",
] ]
dependencies = [ dependencies = [
{ name = "alabaster", marker = "python_full_version < '3.11'" }, { name = "alabaster" },
{ name = "babel", marker = "python_full_version < '3.11'" }, { name = "babel" },
{ name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, { name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" } },
{ name = "imagesize", marker = "python_full_version < '3.11'" }, { name = "imagesize" },
{ name = "jinja2", marker = "python_full_version < '3.11'" }, { name = "jinja2" },
{ name = "packaging", marker = "python_full_version < '3.11'" }, { name = "packaging" },
{ name = "pygments", marker = "python_full_version < '3.11'" }, { name = "pygments" },
{ name = "requests", marker = "python_full_version < '3.11'" }, { name = "requests" },
{ name = "snowballstemmer", marker = "python_full_version < '3.11'" }, { name = "snowballstemmer" },
{ name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.11'" }, { name = "sphinxcontrib-applehelp" },
{ name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.11'" }, { name = "sphinxcontrib-devhelp" },
{ name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.11'" }, { name = "sphinxcontrib-htmlhelp" },
{ name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.11'" }, { name = "sphinxcontrib-jsmath" },
{ name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.11'" }, { name = "sphinxcontrib-qthelp" },
{ name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.11'" }, { name = "sphinxcontrib-serializinghtml" },
{ name = "tomli", marker = "python_full_version < '3.11'" }, { name = "tomli" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" } sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" }
wheels = [ wheels = [
@ -6782,23 +6782,23 @@ resolution-markers = [
"python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'",
] ]
dependencies = [ dependencies = [
{ name = "alabaster", marker = "python_full_version == '3.11.*'" }, { name = "alabaster" },
{ name = "babel", marker = "python_full_version == '3.11.*'" }, { name = "babel" },
{ name = "colorama", marker = "python_full_version == '3.11.*' and sys_platform == 'win32'" }, { name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" } },
{ name = "imagesize", marker = "python_full_version == '3.11.*'" }, { name = "imagesize" },
{ name = "jinja2", marker = "python_full_version == '3.11.*'" }, { name = "jinja2" },
{ name = "packaging", marker = "python_full_version == '3.11.*'" }, { name = "packaging" },
{ name = "pygments", marker = "python_full_version == '3.11.*'" }, { name = "pygments" },
{ name = "requests", marker = "python_full_version == '3.11.*'" }, { name = "requests" },
{ name = "roman-numerals", marker = "python_full_version == '3.11.*'" }, { name = "roman-numerals" },
{ name = "snowballstemmer", marker = "python_full_version == '3.11.*'" }, { name = "snowballstemmer" },
{ name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.11.*'" }, { name = "sphinxcontrib-applehelp" },
{ name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.11.*'" }, { name = "sphinxcontrib-devhelp" },
{ name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.11.*'" }, { name = "sphinxcontrib-htmlhelp" },
{ name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.11.*'" }, { name = "sphinxcontrib-jsmath" },
{ name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.11.*'" }, { name = "sphinxcontrib-qthelp" },
{ name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.11.*'" }, { name = "sphinxcontrib-serializinghtml" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" }
wheels = [ wheels = [
@ -6820,23 +6820,23 @@ resolution-markers = [
"python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'",
] ]
dependencies = [ dependencies = [
{ name = "alabaster", marker = "python_full_version >= '3.12'" }, { name = "alabaster" },
{ name = "babel", marker = "python_full_version >= '3.12'" }, { name = "babel" },
{ name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, { name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" } },
{ name = "imagesize", marker = "python_full_version >= '3.12'" }, { name = "imagesize" },
{ name = "jinja2", marker = "python_full_version >= '3.12'" }, { name = "jinja2" },
{ name = "packaging", marker = "python_full_version >= '3.12'" }, { name = "packaging" },
{ name = "pygments", marker = "python_full_version >= '3.12'" }, { name = "pygments" },
{ name = "requests", marker = "python_full_version >= '3.12'" }, { name = "requests" },
{ name = "roman-numerals", marker = "python_full_version >= '3.12'" }, { name = "roman-numerals" },
{ name = "snowballstemmer", marker = "python_full_version >= '3.12'" }, { name = "snowballstemmer" },
{ name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" }, { name = "sphinxcontrib-applehelp" },
{ name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'" }, { name = "sphinxcontrib-devhelp" },
{ name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'" }, { name = "sphinxcontrib-htmlhelp" },
{ name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'" }, { name = "sphinxcontrib-jsmath" },
{ name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'" }, { name = "sphinxcontrib-qthelp" },
{ name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'" }, { name = "sphinxcontrib-serializinghtml" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" }
wheels = [ wheels = [
@ -6854,12 +6854,12 @@ resolution-markers = [
"python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'",
] ]
dependencies = [ dependencies = [
{ name = "colorama", marker = "python_full_version < '3.11'" }, { name = "colorama" },
{ name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" } },
{ name = "starlette", marker = "python_full_version < '3.11'" }, { name = "starlette" },
{ name = "uvicorn", marker = "python_full_version < '3.11'" }, { name = "uvicorn" },
{ name = "watchfiles", marker = "python_full_version < '3.11'" }, { name = "watchfiles" },
{ name = "websockets", marker = "python_full_version < '3.11'" }, { name = "websockets" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/a5/2c/155e1de2c1ba96a72e5dba152c509a8b41e047ee5c2def9e9f0d812f8be7/sphinx_autobuild-2024.10.3.tar.gz", hash = "sha256:248150f8f333e825107b6d4b86113ab28fa51750e5f9ae63b59dc339be951fb1", size = 14023, upload-time = "2024-10-02T23:15:30.172Z" } sdist = { url = "https://files.pythonhosted.org/packages/a5/2c/155e1de2c1ba96a72e5dba152c509a8b41e047ee5c2def9e9f0d812f8be7/sphinx_autobuild-2024.10.3.tar.gz", hash = "sha256:248150f8f333e825107b6d4b86113ab28fa51750e5f9ae63b59dc339be951fb1", size = 14023, upload-time = "2024-10-02T23:15:30.172Z" }
wheels = [ wheels = [
@ -6885,13 +6885,13 @@ resolution-markers = [
"python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'",
] ]
dependencies = [ dependencies = [
{ name = "colorama", marker = "python_full_version >= '3.11'" }, { name = "colorama" },
{ name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
{ name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
{ name = "starlette", marker = "python_full_version >= '3.11'" }, { name = "starlette" },
{ name = "uvicorn", marker = "python_full_version >= '3.11'" }, { name = "uvicorn" },
{ name = "watchfiles", marker = "python_full_version >= '3.11'" }, { name = "watchfiles" },
{ name = "websockets", marker = "python_full_version >= '3.11'" }, { name = "websockets" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/e0/3c/a59a3a453d4133777f7ed2e83c80b7dc817d43c74b74298ca0af869662ad/sphinx_autobuild-2025.8.25.tar.gz", hash = "sha256:9cf5aab32853c8c31af572e4fecdc09c997e2b8be5a07daf2a389e270e85b213", size = 15200, upload-time = "2025-08-25T18:44:55.436Z" } sdist = { url = "https://files.pythonhosted.org/packages/e0/3c/a59a3a453d4133777f7ed2e83c80b7dc817d43c74b74298ca0af869662ad/sphinx_autobuild-2025.8.25.tar.gz", hash = "sha256:9cf5aab32853c8c31af572e4fecdc09c997e2b8be5a07daf2a389e270e85b213", size = 15200, upload-time = "2025-08-25T18:44:55.436Z" }
wheels = [ wheels = [
@ -6909,7 +6909,7 @@ resolution-markers = [
"python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'",
] ]
dependencies = [ dependencies = [
{ name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" } },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/26/f0/43c6a5ff3e7b08a8c3b32f81b859f1b518ccc31e45f22e2b41ced38be7b9/sphinx_autodoc_typehints-3.0.1.tar.gz", hash = "sha256:b9b40dd15dee54f6f810c924f863f9cf1c54f9f3265c495140ea01be7f44fa55", size = 36282, upload-time = "2025-01-16T18:25:30.958Z" } sdist = { url = "https://files.pythonhosted.org/packages/26/f0/43c6a5ff3e7b08a8c3b32f81b859f1b518ccc31e45f22e2b41ced38be7b9/sphinx_autodoc_typehints-3.0.1.tar.gz", hash = "sha256:b9b40dd15dee54f6f810c924f863f9cf1c54f9f3265c495140ea01be7f44fa55", size = 36282, upload-time = "2025-01-16T18:25:30.958Z" }
wheels = [ wheels = [
@ -6927,7 +6927,7 @@ resolution-markers = [
"python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'",
] ]
dependencies = [ dependencies = [
{ name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" } },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/1d/f6/bdd93582b2aaad2cfe9eb5695a44883c8bc44572dd3c351a947acbb13789/sphinx_autodoc_typehints-3.6.1.tar.gz", hash = "sha256:fa0b686ae1b85965116c88260e5e4b82faec3687c2e94d6a10f9b36c3743e2fe", size = 37563, upload-time = "2026-01-02T15:23:46.543Z" } sdist = { url = "https://files.pythonhosted.org/packages/1d/f6/bdd93582b2aaad2cfe9eb5695a44883c8bc44572dd3c351a947acbb13789/sphinx_autodoc_typehints-3.6.1.tar.gz", hash = "sha256:fa0b686ae1b85965116c88260e5e4b82faec3687c2e94d6a10f9b36c3743e2fe", size = 37563, upload-time = "2026-01-02T15:23:46.543Z" }
wheels = [ wheels = [
@ -6949,7 +6949,7 @@ resolution-markers = [
"python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'",
] ]
dependencies = [ dependencies = [
{ name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" } },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/75/75/9a5695b3d3b848a43cfe91c7d7d3c5ab543eb3bc5c2a12ffaf5003a2a4f6/sphinx_autodoc_typehints-3.10.2.tar.gz", hash = "sha256:34db651eb14343ba16bd9c03e0f5093971f9bbd3cc6b0a1470adecd578940b9c", size = 74241, upload-time = "2026-04-15T22:09:48.87Z" } sdist = { url = "https://files.pythonhosted.org/packages/75/75/9a5695b3d3b848a43cfe91c7d7d3c5ab543eb3bc5c2a12ffaf5003a2a4f6/sphinx_autodoc_typehints-3.10.2.tar.gz", hash = "sha256:34db651eb14343ba16bd9c03e0f5093971f9bbd3cc6b0a1470adecd578940b9c", size = 74241, upload-time = "2026-04-15T22:09:48.87Z" }
wheels = [ wheels = [
@ -6967,8 +6967,8 @@ resolution-markers = [
"python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'",
] ]
dependencies = [ dependencies = [
{ name = "pydata-sphinx-theme", version = "0.15.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "pydata-sphinx-theme", version = "0.15.4", source = { registry = "https://pypi.org/simple" } },
{ name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" } },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/45/19/d002ed96bdc7738c15847c730e1e88282d738263deac705d5713b4d8fa94/sphinx_book_theme-1.1.4.tar.gz", hash = "sha256:73efe28af871d0a89bd05856d300e61edce0d5b2fbb7984e84454be0fedfe9ed", size = 439188, upload-time = "2025-02-20T16:32:32.581Z" } sdist = { url = "https://files.pythonhosted.org/packages/45/19/d002ed96bdc7738c15847c730e1e88282d738263deac705d5713b4d8fa94/sphinx_book_theme-1.1.4.tar.gz", hash = "sha256:73efe28af871d0a89bd05856d300e61edce0d5b2fbb7984e84454be0fedfe9ed", size = 439188, upload-time = "2025-02-20T16:32:32.581Z" }
wheels = [ wheels = [
@ -6994,8 +6994,8 @@ resolution-markers = [
"python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'",
] ]
dependencies = [ dependencies = [
{ name = "pydata-sphinx-theme", version = "0.16.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pydata-sphinx-theme", version = "0.16.1", source = { registry = "https://pypi.org/simple" } },
{ name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
{ name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/eb/f7/154786f3cfb7692cd7acc24b6dfe4dcd1146b66f376b17df9e47125555e9/sphinx_book_theme-1.2.0.tar.gz", hash = "sha256:4a7ebfc7da4395309ac942ddfc38fbec5c5254c3be22195e99ad12586fbda9e3", size = 443962, upload-time = "2026-03-09T23:20:30.442Z" } sdist = { url = "https://files.pythonhosted.org/packages/eb/f7/154786f3cfb7692cd7acc24b6dfe4dcd1146b66f376b17df9e47125555e9/sphinx_book_theme-1.2.0.tar.gz", hash = "sha256:4a7ebfc7da4395309ac942ddfc38fbec5c5254c3be22195e99ad12586fbda9e3", size = 443962, upload-time = "2026-03-09T23:20:30.442Z" }
@ -7163,8 +7163,8 @@ name = "standard-aifc"
version = "3.13.0" version = "3.13.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "audioop-lts", marker = "python_full_version >= '3.13'" }, { name = "audioop-lts" },
{ name = "standard-chunk", marker = "python_full_version >= '3.13'" }, { name = "standard-chunk" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/c4/53/6050dc3dde1671eb3db592c13b55a8005e5040131f7509cef0215212cb84/standard_aifc-3.13.0.tar.gz", hash = "sha256:64e249c7cb4b3daf2fdba4e95721f811bde8bdfc43ad9f936589b7bb2fae2e43", size = 15240, upload-time = "2024-10-30T16:01:31.772Z" } sdist = { url = "https://files.pythonhosted.org/packages/c4/53/6050dc3dde1671eb3db592c13b55a8005e5040131f7509cef0215212cb84/standard_aifc-3.13.0.tar.gz", hash = "sha256:64e249c7cb4b3daf2fdba4e95721f811bde8bdfc43ad9f936589b7bb2fae2e43", size = 15240, upload-time = "2024-10-30T16:01:31.772Z" }
wheels = [ wheels = [
@ -7185,7 +7185,7 @@ name = "standard-sunau"
version = "3.13.0" version = "3.13.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "audioop-lts", marker = "python_full_version >= '3.13'" }, { name = "audioop-lts" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/66/e3/ce8d38cb2d70e05ffeddc28bb09bad77cfef979eb0a299c9117f7ed4e6a9/standard_sunau-3.13.0.tar.gz", hash = "sha256:b319a1ac95a09a2378a8442f403c66f4fd4b36616d6df6ae82b8e536ee790908", size = 9368, upload-time = "2024-10-30T16:01:41.626Z" } sdist = { url = "https://files.pythonhosted.org/packages/66/e3/ce8d38cb2d70e05ffeddc28bb09bad77cfef979eb0a299c9117f7ed4e6a9/standard_sunau-3.13.0.tar.gz", hash = "sha256:b319a1ac95a09a2378a8442f403c66f4fd4b36616d6df6ae82b8e536ee790908", size = 9368, upload-time = "2024-10-30T16:01:41.626Z" }
wheels = [ wheels = [
@ -7924,9 +7924,9 @@ resolution-markers = [
"python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'",
] ]
dependencies = [ dependencies = [
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } },
{ name = "packaging", marker = "python_full_version < '3.11'" }, { name = "packaging" },
{ name = "pandas", marker = "python_full_version < '3.11'" }, { name = "pandas" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/19/ec/e50d833518f10b0c24feb184b209bb6856f25b919ba8c1f89678b930b1cd/xarray-2025.6.1.tar.gz", hash = "sha256:a84f3f07544634a130d7dc615ae44175419f4c77957a7255161ed99c69c7c8b0", size = 3003185, upload-time = "2025-06-12T03:04:09.099Z" } sdist = { url = "https://files.pythonhosted.org/packages/19/ec/e50d833518f10b0c24feb184b209bb6856f25b919ba8c1f89678b930b1cd/xarray-2025.6.1.tar.gz", hash = "sha256:a84f3f07544634a130d7dc615ae44175419f4c77957a7255161ed99c69c7c8b0", size = 3003185, upload-time = "2025-06-12T03:04:09.099Z" }
wheels = [ wheels = [
@ -7952,9 +7952,9 @@ resolution-markers = [
"python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'", "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32'",
] ]
dependencies = [ dependencies = [
{ name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" } },
{ name = "packaging", marker = "python_full_version >= '3.11'" }, { name = "packaging" },
{ name = "pandas", marker = "python_full_version >= '3.11'" }, { name = "pandas" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/4b/a6/6fe936a798a3a38a79c7422d1a31afd2e9a14690fcb0ccff96bc01f04bf2/xarray-2026.4.0.tar.gz", hash = "sha256:c4ac9a01a945d90d5b1628e2af045099a9d4943536d4f2ee3ae963c3b222d15b", size = 3132311, upload-time = "2026-04-13T19:45:36.688Z" } sdist = { url = "https://files.pythonhosted.org/packages/4b/a6/6fe936a798a3a38a79c7422d1a31afd2e9a14690fcb0ccff96bc01f04bf2/xarray-2026.4.0.tar.gz", hash = "sha256:c4ac9a01a945d90d5b1628e2af045099a9d4943536d4f2ee3ae963c3b222d15b", size = 3132311, upload-time = "2026-04-13T19:45:36.688Z" }
wheels = [ wheels = [