From 7a6a288814a1b736325f72dab4c85ecaa67cd60a Mon Sep 17 00:00:00 2001 From: arjunsridhar12345 Date: Mon, 1 Jun 2026 16:10:06 -0700 Subject: [PATCH 01/36] refactor: remove temp dataset --- pyproject.toml | 2 +- .../raw_data_loader/temp_dataset.py | 117 ------------------ 2 files changed, 1 insertion(+), 118 deletions(-) delete mode 100644 src/dynamic_foraging_processing/raw_data_loader/temp_dataset.py diff --git a/pyproject.toml b/pyproject.toml index b6d6b18..58087a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,7 @@ extend-ignore = [] convention = 'google' [tool.coverage.run] -omit = ["*__init__*", "*/temp_dataset.py"] +omit = ["*__init__*"] source = ["dynamic_foraging_processing", "tests"] [tool.coverage.report] diff --git a/src/dynamic_foraging_processing/raw_data_loader/temp_dataset.py b/src/dynamic_foraging_processing/raw_data_loader/temp_dataset.py deleted file mode 100644 index 561f477..0000000 --- a/src/dynamic_foraging_processing/raw_data_loader/temp_dataset.py +++ /dev/null @@ -1,117 +0,0 @@ -"""Temporary local dataset builder. - -This is a local copy of ``aind_behavior_dynamic_foraging.data_contract._dataset.make_dataset``. -This file ONLY exists because test data has only been acquired. -It will be removed once the full dataset is acquired. -""" - -from pathlib import Path - -from aind_behavior_curriculum import TrainerState -from aind_behavior_dynamic_foraging import __semver__ -from aind_behavior_dynamic_foraging.rig import AindDynamicForagingRig -from aind_behavior_dynamic_foraging.task_logic import AindDynamicForagingTaskLogic -from aind_behavior_services.session import Session -from contraqctor.contract import Dataset, DataStreamCollection -from contraqctor.contract.harp import ( - DeviceYmlByFile, - HarpDevice, -) -from contraqctor.contract.json import Json, PydanticModel - - -def make_dataset( - root_path: Path, - name: str = "DynamicForagingDataset", - description: str = "A Dynamic Foraging dataset", - version: str = __semver__, -) -> Dataset: - """Creates a Dataset object for the Dynamic Foraging experiment. - - This function constructs a hierarchical representation of the data streams - collected during an experiment, including hardware device data, software - events, and configuration files. - - Parameters - ---------- - root_path : Path - Path to the root directory containing the dataset. - name : str, optional - Name of the dataset, defaults to ``"DynamicForagingDataset"``. - description : str, optional - Description of the dataset, defaults to ``"A Dynamic Foraging dataset"``. - version : str, optional - Version of the dataset, defaults to the package version. - - Returns - ------- - Dataset - A Dataset object containing a hierarchical representation of all data - streams from the Dynamic Foraging experiment. - """ - root_path = Path(root_path) - return Dataset( - name=name, - version=version, - description=description, - data_streams=[ - DataStreamCollection( - name="Behavior", - description="Data from the Behavior modality", - data_streams=[ - Json( - name="PreviousMetrics", - reader_params=Json.make_params( - path=root_path / "behavior/metrics.json", - ), - ), - PydanticModel( - name="TrainerState", - reader_params=PydanticModel.make_params( - model=TrainerState, - path=root_path / "behavior/trainer_state.json", - ), - ), - HarpDevice( - name="HarpBehavior", - reader_params=HarpDevice.make_params( - path=root_path / "behavior/Behavior.harp", - device_yml_hint=DeviceYmlByFile(), - ), - ), - DataStreamCollection( - name="InputSchemas", - description="Configuration files for the behavior rig, task_logic and session.", - data_streams=[ - PydanticModel( - name="Rig", - reader_params=PydanticModel.make_params( - model=AindDynamicForagingRig, - path=root_path / "behavior/Logs/rig_output.json", - ), - ), - PydanticModel( - name="TaskLogic", - reader_params=PydanticModel.make_params( - model=AindDynamicForagingTaskLogic, - path=root_path / "behavior/Logs/tasklogic_output.json", - ), - ), - PydanticModel( - name="Session", - reader_params=PydanticModel.make_params( - model=Session, - path=root_path / "behavior/Logs/session_output.json", - ), - ), - ], - ), - ], - ), - ], - ) - - -def dataset(path, version: str = __semver__) -> Dataset: - """Build a Dataset at ``path`` using the local (temp) make_dataset.""" - return make_dataset(Path(path), version=version) From 24e6b17f0cf13cbac5101fdff8bd45024244d484 Mon Sep 17 00:00:00 2001 From: arjunsridhar12345 Date: Mon, 1 Jun 2026 16:11:29 -0700 Subject: [PATCH 02/36] feat: add acquisition processor module --- .../process/__init__.py | 1 + .../process/acquisition/__init__.py | 6 ++ .../acquisition/acquisition_builder.py | 64 ++++++++++++++++++ .../process/acquisition/models.py | 66 +++++++++++++++++++ 4 files changed, 137 insertions(+) create mode 100644 src/dynamic_foraging_processing/process/__init__.py create mode 100644 src/dynamic_foraging_processing/process/acquisition/__init__.py create mode 100644 src/dynamic_foraging_processing/process/acquisition/acquisition_builder.py create mode 100644 src/dynamic_foraging_processing/process/acquisition/models.py diff --git a/src/dynamic_foraging_processing/process/__init__.py b/src/dynamic_foraging_processing/process/__init__.py new file mode 100644 index 0000000..ec793c6 --- /dev/null +++ b/src/dynamic_foraging_processing/process/__init__.py @@ -0,0 +1 @@ +"""Processing modules for dynamic foraging datasets.""" diff --git a/src/dynamic_foraging_processing/process/acquisition/__init__.py b/src/dynamic_foraging_processing/process/acquisition/__init__.py new file mode 100644 index 0000000..ba151f1 --- /dev/null +++ b/src/dynamic_foraging_processing/process/acquisition/__init__.py @@ -0,0 +1,6 @@ +"""Acquisition processing for dynamic foraging datasets.""" + +from dynamic_foraging_processing.process.acquisition.acquisition_builder import AcqusitionBuilder +from dynamic_foraging_processing.process.acquisition.models import AcquisitionSeries, NWBAcquisition + +__all__ = ["AcqusitionBuilder", "AcquisitionSeries", "NWBAcquisition"] diff --git a/src/dynamic_foraging_processing/process/acquisition/acquisition_builder.py b/src/dynamic_foraging_processing/process/acquisition/acquisition_builder.py new file mode 100644 index 0000000..006a6c6 --- /dev/null +++ b/src/dynamic_foraging_processing/process/acquisition/acquisition_builder.py @@ -0,0 +1,64 @@ +"""Acquisition builder for NWB acquisition module.""" + +import contraqctor +import pandas as pd + +from dynamic_foraging_processing.process.acquisition.models import ( + AcquisitionSeries, + NWBAcquisition, +) + + +class AcqusitionBuilder: + """Builds the NWB acquisition module from raw dynamic foraging data.""" + + def __init__(self, dataset: contraqctor.contract.Dataset): + """Initialize the acquisition builder. + + Parameters + ---------- + dataset : contraqctor.contract.Dataset + Dataset providing access to the dynamic foraging data. + """ + self.dataset = dataset + + def get_reward_delivery(self) -> pd.DataFrame: + """Get the reward delivery stream from the dataset. + + Returns + ------- + pandas.DataFrame + DataFrame from the loaded ``OutputSet`` stream under + ``Behavior/HarpBehavior``. + """ + data = self.dataset.at("Behavior").at("HarpBehavior").at("OutputSet").load().data + data_write_messages = data[data["MessageType"] == "WRITE"] + + return data_write_messages + + def build_acquisition(self) -> NWBAcquisition: + """Build the NWB acquisition collection. + + Returns + ------- + NWBAcquisition + Object holding all acquisition series. + """ + writes = self.get_reward_delivery() + # TODO: fix data so that it is array of annotations of whether the reward was earned, manual, or automatic + return NWBAcquisition( + reward_delivery_left=AcquisitionSeries( + name="reward_delivery_left", + data=writes["SupplyPort0"].to_numpy(), + timestamps=writes.index.to_numpy(), + unit="second", + description="The reward delivery time of the left lick port. The data field annotates whether the reward was earned, manual, or automatic", + ), + reward_delivery_right=AcquisitionSeries( + name="reward_delivery_right", + data=writes["SupplyPort1"].to_numpy(), + timestamps=writes.index.to_numpy(), + unit="second", + description="The reward delivery time of the right lick port. The data field annotates whether the reward was earned, manual, or automatic", + ), + ) diff --git a/src/dynamic_foraging_processing/process/acquisition/models.py b/src/dynamic_foraging_processing/process/acquisition/models.py new file mode 100644 index 0000000..0ca86f8 --- /dev/null +++ b/src/dynamic_foraging_processing/process/acquisition/models.py @@ -0,0 +1,66 @@ +"""Pydantic models mirroring the NWB acquisition schema. + +These types decouple the processing layer from ``pynwb``: builders produce +plain pydantic models that an NWB writer can later translate into the +corresponding ``pynwb`` objects. +""" + +import numpy as np +import pandas as pd +from pydantic import BaseModel, ConfigDict, model_validator + + +class AcquisitionSeries(BaseModel): + """Single time series destined for an NWB acquisition entry. + + Parameters + ---------- + name : str + Name of the series. + data : numpy.ndarray or pandas.Series + Sample values. + timestamps : numpy.ndarray or pandas.Series + Timestamps aligned with ``data``. + unit : str + Unit of the samples in ``data``. + description : str + Human-readable description of the series. + """ + + # Allow numpy/pandas fields (no native pydantic schema) and make instances immutable. + model_config = ConfigDict(arbitrary_types_allowed=True, frozen=True) + + name: str + data: np.ndarray | pd.Series + timestamps: np.ndarray | pd.Series + unit: str + description: str + + @model_validator(mode="after") + def _check_lengths(self) -> "AcquisitionSeries": + """Validate that ``data`` and ``timestamps`` have matching lengths.""" + if self.data.shape[0] != self.timestamps.shape[0]: + raise ValueError( + f"data and timestamps must have the same length for series " + f"{self.name!r}: got {self.data.shape[0]} and " + f"{self.timestamps.shape[0]}." + ) + return self + + +class NWBAcquisition(BaseModel): + """Collection of acquisition series for the NWB acquisition module. + + Parameters + ---------- + reward_delivery_left : AcquisitionSeries + Reward delivery events for the left port. + reward_delivery_right : AcquisitionSeries + Reward delivery events for the right port. + """ + + # Immutable container; nested AcquisitionSeries already permits arbitrary types. + model_config = ConfigDict(frozen=True) + + reward_delivery_left: AcquisitionSeries + reward_delivery_right: AcquisitionSeries From d1a01eac5058b3af8855c0057369254478d77091 Mon Sep 17 00:00:00 2001 From: arjunsridhar12345 Date: Mon, 1 Jun 2026 16:11:52 -0700 Subject: [PATCH 03/36] refactor: use import from data contract instead of temp file --- src/dynamic_foraging_processing/raw_data_loader/loader.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/dynamic_foraging_processing/raw_data_loader/loader.py b/src/dynamic_foraging_processing/raw_data_loader/loader.py index 054fd90..5d30c5a 100644 --- a/src/dynamic_foraging_processing/raw_data_loader/loader.py +++ b/src/dynamic_foraging_processing/raw_data_loader/loader.py @@ -5,8 +5,7 @@ import typing as t import pandas as pd - -from .temp_dataset import dataset as _build_dataset +from aind_behavior_dynamic_foraging.data_contract import dataset as _build_dataset if t.TYPE_CHECKING: from contraqctor.contract import Dataset From 99c6adf89088342adefa3a98345aeb5fe5775046 Mon Sep 17 00:00:00 2001 From: arjunsridhar12345 Date: Mon, 1 Jun 2026 16:12:15 -0700 Subject: [PATCH 04/36] test: add tests for acquisition builder --- tests/test_process/__init__.py | 1 + .../test_process/test_acquisition/__init__.py | 1 + .../test_acquisition_builder.py | 99 +++++++++++++++++++ .../test_acquisition/test_models.py | 80 +++++++++++++++ 4 files changed, 181 insertions(+) create mode 100644 tests/test_process/__init__.py create mode 100644 tests/test_process/test_acquisition/__init__.py create mode 100644 tests/test_process/test_acquisition/test_acquisition_builder.py create mode 100644 tests/test_process/test_acquisition/test_models.py diff --git a/tests/test_process/__init__.py b/tests/test_process/__init__.py new file mode 100644 index 0000000..70b4143 --- /dev/null +++ b/tests/test_process/__init__.py @@ -0,0 +1 @@ +"""Tests for ``dynamic_foraging_processing.process``.""" diff --git a/tests/test_process/test_acquisition/__init__.py b/tests/test_process/test_acquisition/__init__.py new file mode 100644 index 0000000..bcb6321 --- /dev/null +++ b/tests/test_process/test_acquisition/__init__.py @@ -0,0 +1 @@ +"""Tests for ``dynamic_foraging_processing.process.acquisition``.""" diff --git a/tests/test_process/test_acquisition/test_acquisition_builder.py b/tests/test_process/test_acquisition/test_acquisition_builder.py new file mode 100644 index 0000000..ec2f6d3 --- /dev/null +++ b/tests/test_process/test_acquisition/test_acquisition_builder.py @@ -0,0 +1,99 @@ +"""Tests for ``dynamic_foraging_processing.process.acquisition.acquisition_builder``.""" + +from unittest.mock import MagicMock + +import numpy as np +import pandas as pd + +from dynamic_foraging_processing.process.acquisition import AcqusitionBuilder +from dynamic_foraging_processing.process.acquisition.models import ( + AcquisitionSeries, + NWBAcquisition, +) + + +def _make_output_set_frame() -> pd.DataFrame: + """Build an OutputSet-like DataFrame with WRITE and non-WRITE rows.""" + return pd.DataFrame( + { + "MessageType": ["WRITE", "READ", "WRITE"], + "SupplyPort0": [1, 0, 0], + "SupplyPort1": [0, 0, 1], + }, + index=pd.Index([0.1, 0.2, 0.3], name="time"), + ) + + +def _make_dataset(frame: pd.DataFrame) -> MagicMock: + """Build a mock dataset whose ``at`` chain resolves to ``frame``.""" + dataset = MagicMock() + stream = MagicMock() + stream.data = frame + dataset.at.return_value.at.return_value.at.return_value.load.return_value = stream + return dataset + + +# --------------------------------------------------------------------------- +# __init__ +# --------------------------------------------------------------------------- + + +def test_init_stores_dataset(): + """The provided dataset is stored on the instance.""" + ds = _make_dataset(_make_output_set_frame()) + builder = AcqusitionBuilder(dataset=ds) + assert builder.dataset is ds + + +# --------------------------------------------------------------------------- +# get_reward_delivery +# --------------------------------------------------------------------------- + + +def test_get_reward_delivery_filters_to_write_messages(): + """Only ``MessageType == 'WRITE'`` rows are returned.""" + frame = _make_output_set_frame() + ds = _make_dataset(frame) + builder = AcqusitionBuilder(dataset=ds) + + result = builder.get_reward_delivery() + + assert list(result["MessageType"]) == ["WRITE", "WRITE"] + assert list(result.index) == [0.1, 0.3] + ds.at.assert_called_once_with("Behavior") + ds.at.return_value.at.assert_called_once_with("HarpBehavior") + ds.at.return_value.at.return_value.at.assert_called_once_with("OutputSet") + + +# --------------------------------------------------------------------------- +# build_acquisition +# --------------------------------------------------------------------------- + + +def test_build_acquisition_returns_populated_nwb_acquisition(): + """``build_acquisition`` returns an ``NWBAcquisition`` with both ports populated.""" + frame = _make_output_set_frame() + ds = _make_dataset(frame) + builder = AcqusitionBuilder(dataset=ds) + + acquisition = builder.build_acquisition() + + assert isinstance(acquisition, NWBAcquisition) + + left = acquisition.reward_delivery_left + right = acquisition.reward_delivery_right + assert isinstance(left, AcquisitionSeries) + assert isinstance(right, AcquisitionSeries) + + expected_timestamps = np.array([0.1, 0.3]) + np.testing.assert_array_equal(left.data, np.array([1, 0])) + np.testing.assert_array_equal(left.timestamps, expected_timestamps) + assert left.name == "reward_delivery_left" + assert left.unit == "second" + assert "left lick port" in left.description + + np.testing.assert_array_equal(right.data, np.array([0, 1])) + np.testing.assert_array_equal(right.timestamps, expected_timestamps) + assert right.name == "reward_delivery_right" + assert right.unit == "second" + assert "right lick port" in right.description diff --git a/tests/test_process/test_acquisition/test_models.py b/tests/test_process/test_acquisition/test_models.py new file mode 100644 index 0000000..ed02fb0 --- /dev/null +++ b/tests/test_process/test_acquisition/test_models.py @@ -0,0 +1,80 @@ +"""Tests for ``dynamic_foraging_processing.process.acquisition.models``.""" + +import numpy as np +import pytest + +from dynamic_foraging_processing.process.acquisition.models import ( + AcquisitionSeries, + NWBAcquisition, +) + + +def _make_series(name: str = "series", n: int = 3) -> AcquisitionSeries: + """Build a valid ``AcquisitionSeries`` for tests.""" + return AcquisitionSeries( + name=name, + data=np.arange(n), + timestamps=np.arange(n, dtype=float), + unit="second", + description="desc", + ) + + +# --------------------------------------------------------------------------- +# AcquisitionSeries +# --------------------------------------------------------------------------- + + +def test_acquisition_series_valid(): + """Equal-length data and timestamps construct successfully.""" + series = _make_series() + assert series.name == "series" + assert series.unit == "second" + assert series.description == "desc" + assert series.data.shape[0] == series.timestamps.shape[0] + + +def test_acquisition_series_length_mismatch_raises(): + """Mismatched data and timestamps lengths raise a validation error.""" + with pytest.raises(ValueError, match="must have the same length"): + AcquisitionSeries( + name="bad", + data=np.arange(3), + timestamps=np.arange(2, dtype=float), + unit="second", + description="desc", + ) + + +def test_acquisition_series_is_frozen(): + """Instances are immutable.""" + series = _make_series() + with pytest.raises(ValueError): + series.name = "other" + + +# --------------------------------------------------------------------------- +# NWBAcquisition +# --------------------------------------------------------------------------- + + +def test_nwb_acquisition_holds_series(): + """``NWBAcquisition`` stores both reward delivery series.""" + left = _make_series("reward_delivery_left") + right = _make_series("reward_delivery_right") + acquisition = NWBAcquisition( + reward_delivery_left=left, + reward_delivery_right=right, + ) + assert acquisition.reward_delivery_left is left + assert acquisition.reward_delivery_right is right + + +def test_nwb_acquisition_is_frozen(): + """``NWBAcquisition`` instances are immutable.""" + acquisition = NWBAcquisition( + reward_delivery_left=_make_series("reward_delivery_left"), + reward_delivery_right=_make_series("reward_delivery_right"), + ) + with pytest.raises(ValueError): + acquisition.reward_delivery_left = _make_series("other") From 9d6656df9494b321ca632bcb1581930f8d0d4534 Mon Sep 17 00:00:00 2001 From: arjunsridhar12345 Date: Mon, 1 Jun 2026 16:12:32 -0700 Subject: [PATCH 05/36] feat: add acquisition builder example notebook on how to use --- examples/acquisition_builder_example.ipynb | 484 +++++++++++++++++++++ 1 file changed, 484 insertions(+) create mode 100644 examples/acquisition_builder_example.ipynb diff --git a/examples/acquisition_builder_example.ipynb b/examples/acquisition_builder_example.ipynb new file mode 100644 index 0000000..f82dc23 --- /dev/null +++ b/examples/acquisition_builder_example.ipynb @@ -0,0 +1,484 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "3464fbe0", + "metadata": {}, + "source": [ + "# AcqusitionBuilder Example\n", + "\n", + "This notebook demonstrates how to use the `AcqusitionBuilder` from `dynamic_foraging_processing` to produce a typed `NWBAcquisition` object from a raw dynamic foraging dataset. This object mirrors the structure of the NWB acquisition module and can later be mapped to `pynwb` objects by a downstream writer. It builds on top of the `RawDataLoader` shown in `raw_data_loader_example.ipynb`." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "defab714", + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2\n", + "from pathlib import Path\n", + "\n", + "from dynamic_foraging_processing.process.acquisition import AcqusitionBuilder\n", + "from dynamic_foraging_processing.raw_data_loader import RawDataLoader" + ] + }, + { + "cell_type": "markdown", + "id": "b1a6b673", + "metadata": {}, + "source": [ + "## Load a dataset\n", + "\n", + "Point `RawDataLoader` at the root directory of a dataset acquisition. The builder consumes the underlying `dataset` object." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "db9b70d0", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Update this path to point at a real acquisition directory on your machine.\n", + "dataset_path = Path(\n", + " r\"C:\\Users\\arjun.sridhar\\Downloads\\821586_2026-04-28T200347Z\\821586_2026-04-28T200347Z\"\n", + ")\n", + "\n", + "loader = RawDataLoader(path=dataset_path)\n", + "builder = AcqusitionBuilder(dataset=loader.dataset)\n", + "builder" + ] + }, + { + "cell_type": "markdown", + "id": "3bc226c1", + "metadata": {}, + "source": [ + "## Inspect an individual stream\n", + "\n", + "`get_reward_delivery` returns the filtered `OutputSet` write messages from the Behavior Board as a `pandas.DataFrame`. Use this when you want the raw frame rather than the typed acquisition object." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "bfd5ceeb", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
DOPort0DOPort1DOPort2SupplyPort0SupplyPort1SupplyPort2Led0Led1Rgb0Rgb1DO0DO1DO2DO3MessageType
Time
2.504232e+06FalseFalseFalseTrueFalseFalseFalseFalseFalseFalseFalseFalseFalseFalseWRITE
2.504279e+06FalseFalseFalseTrueFalseFalseFalseFalseFalseFalseFalseFalseFalseFalseWRITE
2.504341e+06FalseFalseFalseTrueFalseFalseFalseFalseFalseFalseFalseFalseFalseFalseWRITE
2.504399e+06FalseFalseFalseTrueFalseFalseFalseFalseFalseFalseFalseFalseFalseFalseWRITE
2.504468e+06FalseFalseFalseTrueFalseFalseFalseFalseFalseFalseFalseFalseFalseFalseWRITE
\n", + "
" + ], + "text/plain": [ + " DOPort0 DOPort1 DOPort2 SupplyPort0 SupplyPort1 \\\n", + "Time \n", + "2.504232e+06 False False False True False \n", + "2.504279e+06 False False False True False \n", + "2.504341e+06 False False False True False \n", + "2.504399e+06 False False False True False \n", + "2.504468e+06 False False False True False \n", + "\n", + " SupplyPort2 Led0 Led1 Rgb0 Rgb1 DO0 DO1 DO2 \\\n", + "Time \n", + "2.504232e+06 False False False False False False False False \n", + "2.504279e+06 False False False False False False False False \n", + "2.504341e+06 False False False False False False False False \n", + "2.504399e+06 False False False False False False False False \n", + "2.504468e+06 False False False False False False False False \n", + "\n", + " DO3 MessageType \n", + "Time \n", + "2.504232e+06 False WRITE \n", + "2.504279e+06 False WRITE \n", + "2.504341e+06 False WRITE \n", + "2.504399e+06 False WRITE \n", + "2.504468e+06 False WRITE " + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "reward_writes = builder.get_reward_delivery()\n", + "reward_writes.head()" + ] + }, + { + "cell_type": "markdown", + "id": "3578e255", + "metadata": {}, + "source": [ + "## Build the NWB acquisition object\n", + "\n", + "`build_acquisition` returns an `NWBAcquisition` pydantic model containing one `AcquisitionSeries` per stream. Each series holds `data`, `timestamps`, `unit`, and `description` fields ready to be mapped to a `pynwb.TimeSeries` by a downstream writer." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "041237d6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "AcquisitionSeries(name='reward_delivery_left', data=array([ True, True, True, True, True, True, True, True, True,\n", + " True, True, True, False, False, False, False, False, False,\n", + " False, False, False, False, False, False, False, False, False,\n", + " True, True, True, True, True, True, True, True, True,\n", + " True, True, True, True, True, True, True, True, True,\n", + " False, True, True, True, True, True, True, False, False,\n", + " False, False, False, False, False, False, False, False, False,\n", + " False, False, True, True, True, True, True, True, False,\n", + " True, True, True, True, True, False, False, False, False,\n", + " False, False, False, False, False, False, False, False, False,\n", + " False, False, False, False, True, False, True, True, False,\n", + " True, True, True, True, False, False, False, False, False,\n", + " False, False, False, True, True, True, True, True, True,\n", + " True, True, True, False, False, False, False, False, False,\n", + " False, False, False, False, False, False, False, False, False,\n", + " False, False, False, False, False, False, False, False, False,\n", + " False, False, True, True, True, True, True, True, True,\n", + " True, False, False, False, False, False, False, False, False,\n", + " False, False, False, False, False, False, False, True, True,\n", + " True, True, True, True, True, True, True, True, True,\n", + " True, True, True, True, True, False, False, False, False,\n", + " False, False, False, False, False, False, False, False, False,\n", + " False, True, True, True, True, True, True, True, True]), timestamps=array([2504232.053504, 2504279.20848 , 2504341.438496, 2504398.506496,\n", + " 2504467.84448 , 2504514.383488, 2504565.286496, 2504618.533504,\n", + " 2504618.881504, 2504627.669504, 2504640.941504, 2504643.76048 ,\n", + " 2504651.18448 , 2504662.913504, 2504671.69248 , 2504684.726496,\n", + " 2504691.473504, 2504698.72448 , 2504765.250496, 2504769.64448 ,\n", + " 2504774.645504, 2504777.115488, 2504786.081504, 2504791.007488,\n", + " 2504793.475488, 2504818.827488, 2504820.987488, 2504891.575488,\n", + " 2504934.526496, 2504936.882496, 2504941.302496, 2504952.182496,\n", + " 2504966.27648 , 2504973.441504, 2504980.361504, 2504985.165504,\n", + " 2504990.705504, 2504997.241504, 2505061.589504, 2505081.039488,\n", + " 2505088.125504, 2505091.52448 , 2505107.359488, 2505116.38848 ,\n", + " 2505132.27648 , 2505138.043488, 2505196.32848 , 2505203.946496,\n", + " 2505212.15648 , 2505220.835488, 2505224.494496, 2505232.26448 ,\n", + " 2505290.934496, 2505304.489504, 2505349.795488, 2505360.617504,\n", + " 2505364.109504, 2505372.851488, 2505387.859488, 2505390.28448 ,\n", + " 2505428.921504, 2505449.167488, 2505459.007488, 2505477.175488,\n", + " 2505480.72048 , 2505541.602496, 2505572.531488, 2505592.847488,\n", + " 2505595.28848 , 2505599.363488, 2505603.150496, 2505625.447488,\n", + " 2505675.363488, 2505703.593504, 2505734.519488, 2505798.763488,\n", + " 2505813.827488, 2505851.226496, 2505855.615488, 2505877.821504,\n", + " 2505883.785504, 2505893.427488, 2505920.017504, 2505923.26448 ,\n", + " 2505948.17248 , 2505967.997504, 2505971.865504, 2505987.975488,\n", + " 2506036.009504, 2506070.925504, 2506080.379488, 2506085.177504,\n", + " 2506092.965504, 2506095.507488, 2506150.546496, 2506176.88048 ,\n", + " 2506205.54048 , 2506257.889504, 2506270.477504, 2506301.254496,\n", + " 2506307.003488, 2506311.233504, 2506319.139488, 2506371.395488,\n", + " 2506385.181504, 2506390.94048 , 2506395.761504, 2506422.599488,\n", + " 2506464.15648 , 2506482.24048 , 2506486.081504, 2506545.131488,\n", + " 2506570.63648 , 2506580.686496, 2506587.362496, 2506591.069504,\n", + " 2506594.58848 , 2506612.086496, 2506623.087488, 2506679.23648 ,\n", + " 2506739.62848 , 2506774.262496, 2506815.774496, 2506823.258496,\n", + " 2506839.407488, 2506843.139488, 2506848.971488, 2506873.49648 ,\n", + " 2506883.145504, 2506891.331488, 2506917.966496, 2506921.995488,\n", + " 2506927.949536, 2506943.025504, 2506954.127488, 2506966.94448 ,\n", + " 2506969.78048 , 2506973.654496, 2506976.801504, 2506984.445504,\n", + " 2507003.799488, 2507016.555488, 2507020.971488, 2507024.109504,\n", + " 2507034.91248 , 2507037.46048 , 2507093.973504, 2507140.530496,\n", + " 2507146.529504, 2507151.947488, 2507170.374496, 2507221.669504,\n", + " 2507283.739488, 2507301.88048 , 2507339.06448 , 2507355.04048 ,\n", + " 2507357.250496, 2507375.401504, 2507382.273504, 2507391.831488,\n", + " 2507396.775488, 2507402.291488, 2507411.589504, 2507418.918496,\n", + " 2507424.690528, 2507441.539488, 2507450.459488, 2507463.30848 ,\n", + " 2507490.667488, 2507536.605504, 2507585.907488, 2507592.086496,\n", + " 2507599.551488, 2507611.819488, 2507616.17648 , 2507630.40448 ,\n", + " 2507639.751488, 2507648.72048 , 2507651.27648 , 2507664.36848 ,\n", + " 2507678.305504, 2507719.806496, 2507730.76848 , 2507733.32448 ,\n", + " 2507747.76848 , 2507784.155488, 2507793.22048 , 2507801.16048 ,\n", + " 2507807.842496, 2507815.59648 , 2507819.439488, 2507833.76848 ,\n", + " 2507838.01648 , 2507840.645504, 2507851.04848 , 2507858.631488,\n", + " 2507864.510496, 2507870.893504, 2507908.498496, 2507951.117504,\n", + " 2508012.931488, 2508022.565504, 2508025.83248 , 2508042.771488,\n", + " 2508048.06848 , 2508051.125504, 2508071.53648 ]), unit='second', description='The reward delivery time of the left lick port. The data field annotates whether the reward was earned, manual, or automatic')" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "acquisition = builder.build_acquisition()\n", + "acquisition.reward_delivery_left" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "0d73bc6f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "name: reward_delivery_left\n", + "unit: second\n", + "description: The reward delivery time of the left lick port. The data field annotates whether the reward was earned, manual, or automatic\n", + "n samples: 207\n" + ] + }, + { + "data": { + "text/plain": [ + "array([ True, True, True, True, True, True, True, True, True,\n", + " True])" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "left = acquisition.reward_delivery_left\n", + "print(f\"name: {left.name}\")\n", + "print(f\"unit: {left.unit}\")\n", + "print(f\"description: {left.description}\")\n", + "print(f\"n samples: {left.data.shape[0]}\")\n", + "left.data[:10]" + ] + }, + { + "cell_type": "markdown", + "id": "581e034b", + "metadata": {}, + "source": [ + "## Serialize for downstream use\n", + "\n", + "Because `NWBAcquisition` is a pydantic model, you can dump the field structure (without the heavy array payloads) for logging or manifest creation." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "3fc6f1c6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'reward_delivery_left': {'unit': 'second',\n", + " 'description': 'The reward delivery time of the left lick port. The data field annotates whether the reward was earned, manual, or automatic',\n", + " 'n_samples': 207},\n", + " 'reward_delivery_right': {'unit': 'second',\n", + " 'description': 'The reward delivery time of the right lick port. The data field annotates whether the reward was earned, manual, or automatic',\n", + " 'n_samples': 207}}" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "{\n", + " name: {\n", + " \"unit\": series.unit,\n", + " \"description\": series.description,\n", + " \"n_samples\": series.data.shape[0],\n", + " }\n", + " for name, series in acquisition\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e48ab0b8", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "dynamic-foraging-processing", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From f5b7f6f37525dd293baef1f770f461073aadf6b9 Mon Sep 17 00:00:00 2001 From: arjunsridhar12345 Date: Mon, 1 Jun 2026 16:24:24 -0700 Subject: [PATCH 06/36] refactor: simplify description of unit --- src/dynamic_foraging_processing/process/acquisition/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dynamic_foraging_processing/process/acquisition/models.py b/src/dynamic_foraging_processing/process/acquisition/models.py index 0ca86f8..c5cd152 100644 --- a/src/dynamic_foraging_processing/process/acquisition/models.py +++ b/src/dynamic_foraging_processing/process/acquisition/models.py @@ -22,7 +22,7 @@ class AcquisitionSeries(BaseModel): timestamps : numpy.ndarray or pandas.Series Timestamps aligned with ``data``. unit : str - Unit of the samples in ``data``. + Unit describing the data. description : str Human-readable description of the series. """ From fcf311d9e3f2ea942f78810e79699fc0cf4e3325 Mon Sep 17 00:00:00 2001 From: arjunsridhar12345 Date: Mon, 1 Jun 2026 17:13:30 -0700 Subject: [PATCH 07/36] fix: rename to match names in current NWB file --- examples/acquisition_builder_example.ipynb | 108 +++++++++++++++--- .../acquisition/acquisition_builder.py | 8 +- .../process/acquisition/models.py | 8 +- 3 files changed, 99 insertions(+), 25 deletions(-) diff --git a/examples/acquisition_builder_example.ipynb b/examples/acquisition_builder_example.ipynb index f82dc23..93a81e8 100644 --- a/examples/acquisition_builder_example.ipynb +++ b/examples/acquisition_builder_example.ipynb @@ -44,7 +44,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 2, @@ -278,14 +278,14 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 4, "id": "041237d6", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "AcquisitionSeries(name='reward_delivery_left', data=array([ True, True, True, True, True, True, True, True, True,\n", + "NWBAcquisition(left_reward_delivery_time=AcquisitionSeries(name='left_reward_delivery_time', data=array([ True, True, True, True, True, True, True, True, True,\n", " True, True, True, False, False, False, False, False, False,\n", " False, False, False, False, False, False, False, False, False,\n", " True, True, True, True, True, True, True, True, True,\n", @@ -358,22 +358,95 @@ " 2507838.01648 , 2507840.645504, 2507851.04848 , 2507858.631488,\n", " 2507864.510496, 2507870.893504, 2507908.498496, 2507951.117504,\n", " 2508012.931488, 2508022.565504, 2508025.83248 , 2508042.771488,\n", - " 2508048.06848 , 2508051.125504, 2508071.53648 ]), unit='second', description='The reward delivery time of the left lick port. The data field annotates whether the reward was earned, manual, or automatic')" + " 2508048.06848 , 2508051.125504, 2508071.53648 ]), unit='second', description='The reward delivery time of the left lick port. The data field annotates whether the reward was earned, manual, or automatic'), right_reward_delivery_time=AcquisitionSeries(name='right_reward_delivery_time', data=array([False, False, False, False, False, False, False, False, False,\n", + " False, False, False, True, True, True, True, True, True,\n", + " True, True, True, True, True, True, True, True, True,\n", + " False, False, False, False, False, False, False, False, False,\n", + " False, False, False, False, False, False, False, False, False,\n", + " True, False, False, False, False, False, False, True, True,\n", + " True, True, True, True, True, True, True, True, True,\n", + " True, True, False, False, False, False, False, False, True,\n", + " False, False, False, False, False, True, True, True, True,\n", + " True, True, True, True, True, True, True, True, True,\n", + " True, True, True, True, False, True, False, False, True,\n", + " False, False, False, False, True, True, True, True, True,\n", + " True, True, True, False, False, False, False, False, False,\n", + " False, False, False, True, True, True, True, True, True,\n", + " True, True, True, True, True, True, True, True, True,\n", + " True, True, True, True, True, True, True, True, True,\n", + " True, True, False, False, False, False, False, False, False,\n", + " False, True, True, True, True, True, True, True, True,\n", + " True, True, True, True, True, True, True, False, False,\n", + " False, False, False, False, False, False, False, False, False,\n", + " False, False, False, False, False, True, True, True, True,\n", + " True, True, True, True, True, True, True, True, True,\n", + " True, False, False, False, False, False, False, False, False]), timestamps=array([2504232.053504, 2504279.20848 , 2504341.438496, 2504398.506496,\n", + " 2504467.84448 , 2504514.383488, 2504565.286496, 2504618.533504,\n", + " 2504618.881504, 2504627.669504, 2504640.941504, 2504643.76048 ,\n", + " 2504651.18448 , 2504662.913504, 2504671.69248 , 2504684.726496,\n", + " 2504691.473504, 2504698.72448 , 2504765.250496, 2504769.64448 ,\n", + " 2504774.645504, 2504777.115488, 2504786.081504, 2504791.007488,\n", + " 2504793.475488, 2504818.827488, 2504820.987488, 2504891.575488,\n", + " 2504934.526496, 2504936.882496, 2504941.302496, 2504952.182496,\n", + " 2504966.27648 , 2504973.441504, 2504980.361504, 2504985.165504,\n", + " 2504990.705504, 2504997.241504, 2505061.589504, 2505081.039488,\n", + " 2505088.125504, 2505091.52448 , 2505107.359488, 2505116.38848 ,\n", + " 2505132.27648 , 2505138.043488, 2505196.32848 , 2505203.946496,\n", + " 2505212.15648 , 2505220.835488, 2505224.494496, 2505232.26448 ,\n", + " 2505290.934496, 2505304.489504, 2505349.795488, 2505360.617504,\n", + " 2505364.109504, 2505372.851488, 2505387.859488, 2505390.28448 ,\n", + " 2505428.921504, 2505449.167488, 2505459.007488, 2505477.175488,\n", + " 2505480.72048 , 2505541.602496, 2505572.531488, 2505592.847488,\n", + " 2505595.28848 , 2505599.363488, 2505603.150496, 2505625.447488,\n", + " 2505675.363488, 2505703.593504, 2505734.519488, 2505798.763488,\n", + " 2505813.827488, 2505851.226496, 2505855.615488, 2505877.821504,\n", + " 2505883.785504, 2505893.427488, 2505920.017504, 2505923.26448 ,\n", + " 2505948.17248 , 2505967.997504, 2505971.865504, 2505987.975488,\n", + " 2506036.009504, 2506070.925504, 2506080.379488, 2506085.177504,\n", + " 2506092.965504, 2506095.507488, 2506150.546496, 2506176.88048 ,\n", + " 2506205.54048 , 2506257.889504, 2506270.477504, 2506301.254496,\n", + " 2506307.003488, 2506311.233504, 2506319.139488, 2506371.395488,\n", + " 2506385.181504, 2506390.94048 , 2506395.761504, 2506422.599488,\n", + " 2506464.15648 , 2506482.24048 , 2506486.081504, 2506545.131488,\n", + " 2506570.63648 , 2506580.686496, 2506587.362496, 2506591.069504,\n", + " 2506594.58848 , 2506612.086496, 2506623.087488, 2506679.23648 ,\n", + " 2506739.62848 , 2506774.262496, 2506815.774496, 2506823.258496,\n", + " 2506839.407488, 2506843.139488, 2506848.971488, 2506873.49648 ,\n", + " 2506883.145504, 2506891.331488, 2506917.966496, 2506921.995488,\n", + " 2506927.949536, 2506943.025504, 2506954.127488, 2506966.94448 ,\n", + " 2506969.78048 , 2506973.654496, 2506976.801504, 2506984.445504,\n", + " 2507003.799488, 2507016.555488, 2507020.971488, 2507024.109504,\n", + " 2507034.91248 , 2507037.46048 , 2507093.973504, 2507140.530496,\n", + " 2507146.529504, 2507151.947488, 2507170.374496, 2507221.669504,\n", + " 2507283.739488, 2507301.88048 , 2507339.06448 , 2507355.04048 ,\n", + " 2507357.250496, 2507375.401504, 2507382.273504, 2507391.831488,\n", + " 2507396.775488, 2507402.291488, 2507411.589504, 2507418.918496,\n", + " 2507424.690528, 2507441.539488, 2507450.459488, 2507463.30848 ,\n", + " 2507490.667488, 2507536.605504, 2507585.907488, 2507592.086496,\n", + " 2507599.551488, 2507611.819488, 2507616.17648 , 2507630.40448 ,\n", + " 2507639.751488, 2507648.72048 , 2507651.27648 , 2507664.36848 ,\n", + " 2507678.305504, 2507719.806496, 2507730.76848 , 2507733.32448 ,\n", + " 2507747.76848 , 2507784.155488, 2507793.22048 , 2507801.16048 ,\n", + " 2507807.842496, 2507815.59648 , 2507819.439488, 2507833.76848 ,\n", + " 2507838.01648 , 2507840.645504, 2507851.04848 , 2507858.631488,\n", + " 2507864.510496, 2507870.893504, 2507908.498496, 2507951.117504,\n", + " 2508012.931488, 2508022.565504, 2508025.83248 , 2508042.771488,\n", + " 2508048.06848 , 2508051.125504, 2508071.53648 ]), unit='second', description='The reward delivery time of the right lick port. The data field annotates whether the reward was earned, manual, or automatic'))" ] }, - "execution_count": 6, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "acquisition = builder.build_acquisition()\n", - "acquisition.reward_delivery_left" + "acquisition" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 5, "id": "0d73bc6f", "metadata": {}, "outputs": [ @@ -381,7 +454,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "name: reward_delivery_left\n", + "name: left_reward_delivery_time\n", "unit: second\n", "description: The reward delivery time of the left lick port. The data field annotates whether the reward was earned, manual, or automatic\n", "n samples: 207\n" @@ -390,22 +463,23 @@ { "data": { "text/plain": [ - "array([ True, True, True, True, True, True, True, True, True,\n", - " True])" + "array([2504232.053504, 2504279.20848 , 2504341.438496, 2504398.506496,\n", + " 2504467.84448 , 2504514.383488, 2504565.286496, 2504618.533504,\n", + " 2504618.881504, 2504627.669504])" ] }, - "execution_count": 7, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "left = acquisition.reward_delivery_left\n", + "left = acquisition.left_reward_delivery_time\n", "print(f\"name: {left.name}\")\n", "print(f\"unit: {left.unit}\")\n", "print(f\"description: {left.description}\")\n", "print(f\"n samples: {left.data.shape[0]}\")\n", - "left.data[:10]" + "left.timestamps[:10]" ] }, { @@ -420,22 +494,22 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 6, "id": "3fc6f1c6", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'reward_delivery_left': {'unit': 'second',\n", + "{'left_reward_delivery_time': {'unit': 'second',\n", " 'description': 'The reward delivery time of the left lick port. The data field annotates whether the reward was earned, manual, or automatic',\n", " 'n_samples': 207},\n", - " 'reward_delivery_right': {'unit': 'second',\n", + " 'right_reward_delivery_time': {'unit': 'second',\n", " 'description': 'The reward delivery time of the right lick port. The data field annotates whether the reward was earned, manual, or automatic',\n", " 'n_samples': 207}}" ] }, - "execution_count": 8, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } diff --git a/src/dynamic_foraging_processing/process/acquisition/acquisition_builder.py b/src/dynamic_foraging_processing/process/acquisition/acquisition_builder.py index 006a6c6..f11539d 100644 --- a/src/dynamic_foraging_processing/process/acquisition/acquisition_builder.py +++ b/src/dynamic_foraging_processing/process/acquisition/acquisition_builder.py @@ -47,15 +47,15 @@ def build_acquisition(self) -> NWBAcquisition: writes = self.get_reward_delivery() # TODO: fix data so that it is array of annotations of whether the reward was earned, manual, or automatic return NWBAcquisition( - reward_delivery_left=AcquisitionSeries( - name="reward_delivery_left", + left_reward_delivery_time=AcquisitionSeries( + name="left_reward_delivery_time", data=writes["SupplyPort0"].to_numpy(), timestamps=writes.index.to_numpy(), unit="second", description="The reward delivery time of the left lick port. The data field annotates whether the reward was earned, manual, or automatic", ), - reward_delivery_right=AcquisitionSeries( - name="reward_delivery_right", + right_reward_delivery_time=AcquisitionSeries( + name="right_reward_delivery_time", data=writes["SupplyPort1"].to_numpy(), timestamps=writes.index.to_numpy(), unit="second", diff --git a/src/dynamic_foraging_processing/process/acquisition/models.py b/src/dynamic_foraging_processing/process/acquisition/models.py index c5cd152..6a8f9a7 100644 --- a/src/dynamic_foraging_processing/process/acquisition/models.py +++ b/src/dynamic_foraging_processing/process/acquisition/models.py @@ -53,14 +53,14 @@ class NWBAcquisition(BaseModel): Parameters ---------- - reward_delivery_left : AcquisitionSeries + left_reward_delivery_time : AcquisitionSeries Reward delivery events for the left port. - reward_delivery_right : AcquisitionSeries + right_reward_delivery_time : AcquisitionSeries Reward delivery events for the right port. """ # Immutable container; nested AcquisitionSeries already permits arbitrary types. model_config = ConfigDict(frozen=True) - reward_delivery_left: AcquisitionSeries - reward_delivery_right: AcquisitionSeries + left_reward_delivery_time: AcquisitionSeries + right_reward_delivery_time: AcquisitionSeries From 282b726dbef88e6cf77b308da852cd8e6548ad52 Mon Sep 17 00:00:00 2001 From: arjunsridhar12345 Date: Mon, 1 Jun 2026 17:13:41 -0700 Subject: [PATCH 08/36] test: update failing tests --- .../test_acquisition/test_acquisition_builder.py | 8 ++++---- tests/test_process/test_acquisition/test_models.py | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/test_process/test_acquisition/test_acquisition_builder.py b/tests/test_process/test_acquisition/test_acquisition_builder.py index ec2f6d3..b673496 100644 --- a/tests/test_process/test_acquisition/test_acquisition_builder.py +++ b/tests/test_process/test_acquisition/test_acquisition_builder.py @@ -80,20 +80,20 @@ def test_build_acquisition_returns_populated_nwb_acquisition(): assert isinstance(acquisition, NWBAcquisition) - left = acquisition.reward_delivery_left - right = acquisition.reward_delivery_right + left = acquisition.left_reward_delivery_time + right = acquisition.right_reward_delivery_time assert isinstance(left, AcquisitionSeries) assert isinstance(right, AcquisitionSeries) expected_timestamps = np.array([0.1, 0.3]) np.testing.assert_array_equal(left.data, np.array([1, 0])) np.testing.assert_array_equal(left.timestamps, expected_timestamps) - assert left.name == "reward_delivery_left" + assert left.name == "left_reward_delivery_time" assert left.unit == "second" assert "left lick port" in left.description np.testing.assert_array_equal(right.data, np.array([0, 1])) np.testing.assert_array_equal(right.timestamps, expected_timestamps) - assert right.name == "reward_delivery_right" + assert right.name == "right_reward_delivery_time" assert right.unit == "second" assert "right lick port" in right.description diff --git a/tests/test_process/test_acquisition/test_models.py b/tests/test_process/test_acquisition/test_models.py index ed02fb0..8aa05d6 100644 --- a/tests/test_process/test_acquisition/test_models.py +++ b/tests/test_process/test_acquisition/test_models.py @@ -63,18 +63,18 @@ def test_nwb_acquisition_holds_series(): left = _make_series("reward_delivery_left") right = _make_series("reward_delivery_right") acquisition = NWBAcquisition( - reward_delivery_left=left, - reward_delivery_right=right, + left_reward_delivery_time=left, + right_reward_delivery_time=right, ) - assert acquisition.reward_delivery_left is left - assert acquisition.reward_delivery_right is right + assert acquisition.left_reward_delivery_time is left + assert acquisition.right_reward_delivery_time is right def test_nwb_acquisition_is_frozen(): """``NWBAcquisition`` instances are immutable.""" acquisition = NWBAcquisition( - reward_delivery_left=_make_series("reward_delivery_left"), - reward_delivery_right=_make_series("reward_delivery_right"), + left_reward_delivery_time=_make_series("reward_delivery_left"), + right_reward_delivery_time=_make_series("reward_delivery_right"), ) with pytest.raises(ValueError): - acquisition.reward_delivery_left = _make_series("other") + acquisition.left_reward_delivery_time = _make_series("other") From 0fedb01a7633f0cb07e4116a5c205c9ba5de855d Mon Sep 17 00:00:00 2001 From: arjunsridhar12345 Date: Tue, 2 Jun 2026 09:57:46 -0700 Subject: [PATCH 09/36] refactor: remove nwb from name to try to make it more clear --- examples/acquisition_builder_example.ipynb | 12 ++++++------ .../process/acquisition/__init__.py | 7 +++++-- .../process/acquisition/acquisition_builder.py | 8 ++++---- .../process/acquisition/models.py | 2 +- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/examples/acquisition_builder_example.ipynb b/examples/acquisition_builder_example.ipynb index 93a81e8..f55c2a2 100644 --- a/examples/acquisition_builder_example.ipynb +++ b/examples/acquisition_builder_example.ipynb @@ -7,7 +7,7 @@ "source": [ "# AcqusitionBuilder Example\n", "\n", - "This notebook demonstrates how to use the `AcqusitionBuilder` from `dynamic_foraging_processing` to produce a typed `NWBAcquisition` object from a raw dynamic foraging dataset. This object mirrors the structure of the NWB acquisition module and can later be mapped to `pynwb` objects by a downstream writer. It builds on top of the `RawDataLoader` shown in `raw_data_loader_example.ipynb`." + "This notebook demonstrates how to use the `AcqusitionBuilder` from `dynamic_foraging_processing` to produce a typed `AcquisitionCollection` object from a raw dynamic foraging dataset. This object mirrors the structure of the NWB acquisition module and can later be mapped to `pynwb` objects by a downstream writer. It builds on top of the `RawDataLoader` shown in `raw_data_loader_example.ipynb`." ] }, { @@ -44,7 +44,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 2, @@ -271,9 +271,9 @@ "id": "3578e255", "metadata": {}, "source": [ - "## Build the NWB acquisition object\n", + "## Build the Acquisition Collection object\n", "\n", - "`build_acquisition` returns an `NWBAcquisition` pydantic model containing one `AcquisitionSeries` per stream. Each series holds `data`, `timestamps`, `unit`, and `description` fields ready to be mapped to a `pynwb.TimeSeries` by a downstream writer." + "`build_acquisition` returns an `AcquisitionCollection` pydantic model containing one `AcquisitionSeries` per stream. Each series holds `data`, `timestamps`, `unit`, and `description` fields ready to be mapped to a `pynwb.TimeSeries` by a downstream writer." ] }, { @@ -285,7 +285,7 @@ { "data": { "text/plain": [ - "NWBAcquisition(left_reward_delivery_time=AcquisitionSeries(name='left_reward_delivery_time', data=array([ True, True, True, True, True, True, True, True, True,\n", + "AcquisitionCollection(left_reward_delivery_time=AcquisitionSeries(name='left_reward_delivery_time', data=array([ True, True, True, True, True, True, True, True, True,\n", " True, True, True, False, False, False, False, False, False,\n", " False, False, False, False, False, False, False, False, False,\n", " True, True, True, True, True, True, True, True, True,\n", @@ -489,7 +489,7 @@ "source": [ "## Serialize for downstream use\n", "\n", - "Because `NWBAcquisition` is a pydantic model, you can dump the field structure (without the heavy array payloads) for logging or manifest creation." + "Because `AcquisitionCollection` is a pydantic model, you can dump the field structure (without the heavy array payloads) for logging or manifest creation." ] }, { diff --git a/src/dynamic_foraging_processing/process/acquisition/__init__.py b/src/dynamic_foraging_processing/process/acquisition/__init__.py index ba151f1..7a47419 100644 --- a/src/dynamic_foraging_processing/process/acquisition/__init__.py +++ b/src/dynamic_foraging_processing/process/acquisition/__init__.py @@ -1,6 +1,9 @@ """Acquisition processing for dynamic foraging datasets.""" from dynamic_foraging_processing.process.acquisition.acquisition_builder import AcqusitionBuilder -from dynamic_foraging_processing.process.acquisition.models import AcquisitionSeries, NWBAcquisition +from dynamic_foraging_processing.process.acquisition.models import ( + AcquisitionCollection, + AcquisitionSeries, +) -__all__ = ["AcqusitionBuilder", "AcquisitionSeries", "NWBAcquisition"] +__all__ = ["AcqusitionBuilder", "AcquisitionCollection", "AcquisitionSeries"] diff --git a/src/dynamic_foraging_processing/process/acquisition/acquisition_builder.py b/src/dynamic_foraging_processing/process/acquisition/acquisition_builder.py index f11539d..65d7858 100644 --- a/src/dynamic_foraging_processing/process/acquisition/acquisition_builder.py +++ b/src/dynamic_foraging_processing/process/acquisition/acquisition_builder.py @@ -4,8 +4,8 @@ import pandas as pd from dynamic_foraging_processing.process.acquisition.models import ( + AcquisitionCollection, AcquisitionSeries, - NWBAcquisition, ) @@ -36,17 +36,17 @@ def get_reward_delivery(self) -> pd.DataFrame: return data_write_messages - def build_acquisition(self) -> NWBAcquisition: + def build_acquisition(self) -> AcquisitionCollection: """Build the NWB acquisition collection. Returns ------- - NWBAcquisition + AcquisitionCollection Object holding all acquisition series. """ writes = self.get_reward_delivery() # TODO: fix data so that it is array of annotations of whether the reward was earned, manual, or automatic - return NWBAcquisition( + return AcquisitionCollection( left_reward_delivery_time=AcquisitionSeries( name="left_reward_delivery_time", data=writes["SupplyPort0"].to_numpy(), diff --git a/src/dynamic_foraging_processing/process/acquisition/models.py b/src/dynamic_foraging_processing/process/acquisition/models.py index 6a8f9a7..d03c74a 100644 --- a/src/dynamic_foraging_processing/process/acquisition/models.py +++ b/src/dynamic_foraging_processing/process/acquisition/models.py @@ -48,7 +48,7 @@ def _check_lengths(self) -> "AcquisitionSeries": return self -class NWBAcquisition(BaseModel): +class AcquisitionCollection(BaseModel): """Collection of acquisition series for the NWB acquisition module. Parameters From 8d246a2363067c2c633f767dab84b5abe4f950b5 Mon Sep 17 00:00:00 2001 From: arjunsridhar12345 Date: Tue, 2 Jun 2026 09:58:03 -0700 Subject: [PATCH 10/36] test: update tests --- .../test_acquisition/test_acquisition_builder.py | 6 +++--- .../test_process/test_acquisition/test_models.py | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/test_process/test_acquisition/test_acquisition_builder.py b/tests/test_process/test_acquisition/test_acquisition_builder.py index b673496..fc69947 100644 --- a/tests/test_process/test_acquisition/test_acquisition_builder.py +++ b/tests/test_process/test_acquisition/test_acquisition_builder.py @@ -7,8 +7,8 @@ from dynamic_foraging_processing.process.acquisition import AcqusitionBuilder from dynamic_foraging_processing.process.acquisition.models import ( + AcquisitionCollection, AcquisitionSeries, - NWBAcquisition, ) @@ -71,14 +71,14 @@ def test_get_reward_delivery_filters_to_write_messages(): def test_build_acquisition_returns_populated_nwb_acquisition(): - """``build_acquisition`` returns an ``NWBAcquisition`` with both ports populated.""" + """``build_acquisition`` returns an ``AcquisitionCollection`` with both ports populated.""" frame = _make_output_set_frame() ds = _make_dataset(frame) builder = AcqusitionBuilder(dataset=ds) acquisition = builder.build_acquisition() - assert isinstance(acquisition, NWBAcquisition) + assert isinstance(acquisition, AcquisitionCollection) left = acquisition.left_reward_delivery_time right = acquisition.right_reward_delivery_time diff --git a/tests/test_process/test_acquisition/test_models.py b/tests/test_process/test_acquisition/test_models.py index 8aa05d6..e55025e 100644 --- a/tests/test_process/test_acquisition/test_models.py +++ b/tests/test_process/test_acquisition/test_models.py @@ -4,8 +4,8 @@ import pytest from dynamic_foraging_processing.process.acquisition.models import ( + AcquisitionCollection, AcquisitionSeries, - NWBAcquisition, ) @@ -54,15 +54,15 @@ def test_acquisition_series_is_frozen(): # --------------------------------------------------------------------------- -# NWBAcquisition +# AcquisitionCollection # --------------------------------------------------------------------------- -def test_nwb_acquisition_holds_series(): - """``NWBAcquisition`` stores both reward delivery series.""" +def test_acquisition_collection_holds_series(): + """``AcquisitionCollection`` stores both reward delivery series.""" left = _make_series("reward_delivery_left") right = _make_series("reward_delivery_right") - acquisition = NWBAcquisition( + acquisition = AcquisitionCollection( left_reward_delivery_time=left, right_reward_delivery_time=right, ) @@ -70,9 +70,9 @@ def test_nwb_acquisition_holds_series(): assert acquisition.right_reward_delivery_time is right -def test_nwb_acquisition_is_frozen(): - """``NWBAcquisition`` instances are immutable.""" - acquisition = NWBAcquisition( +def test_acquisition_collection_is_frozen(): + """``AcquisitionCollection`` instances are immutable.""" + acquisition = AcquisitionCollection( left_reward_delivery_time=_make_series("reward_delivery_left"), right_reward_delivery_time=_make_series("reward_delivery_right"), ) From f2645b05402351ee2e373ca5ee94efd1b4967ec5 Mon Sep 17 00:00:00 2001 From: arjunsridhar12345 Date: Wed, 3 Jun 2026 17:23:32 -0700 Subject: [PATCH 11/36] refactor: remove intermediate acquisition object and all references --- examples/acquisition_builder_example.ipynb | 558 ------------------ .../nwb/__init__.py | 1 + .../process/__init__.py | 1 - .../process/acquisition/__init__.py | 9 - .../acquisition/acquisition_builder.py | 64 -- .../process/acquisition/models.py | 66 --- tests/test_nwb/__init__.py | 1 + tests/test_process/__init__.py | 1 - .../test_process/test_acquisition/__init__.py | 1 - .../test_acquisition_builder.py | 99 ---- .../test_acquisition/test_models.py | 80 --- 11 files changed, 2 insertions(+), 879 deletions(-) delete mode 100644 examples/acquisition_builder_example.ipynb create mode 100644 src/dynamic_foraging_processing/nwb/__init__.py delete mode 100644 src/dynamic_foraging_processing/process/__init__.py delete mode 100644 src/dynamic_foraging_processing/process/acquisition/__init__.py delete mode 100644 src/dynamic_foraging_processing/process/acquisition/acquisition_builder.py delete mode 100644 src/dynamic_foraging_processing/process/acquisition/models.py create mode 100644 tests/test_nwb/__init__.py delete mode 100644 tests/test_process/__init__.py delete mode 100644 tests/test_process/test_acquisition/__init__.py delete mode 100644 tests/test_process/test_acquisition/test_acquisition_builder.py delete mode 100644 tests/test_process/test_acquisition/test_models.py diff --git a/examples/acquisition_builder_example.ipynb b/examples/acquisition_builder_example.ipynb deleted file mode 100644 index f55c2a2..0000000 --- a/examples/acquisition_builder_example.ipynb +++ /dev/null @@ -1,558 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "3464fbe0", - "metadata": {}, - "source": [ - "# AcqusitionBuilder Example\n", - "\n", - "This notebook demonstrates how to use the `AcqusitionBuilder` from `dynamic_foraging_processing` to produce a typed `AcquisitionCollection` object from a raw dynamic foraging dataset. This object mirrors the structure of the NWB acquisition module and can later be mapped to `pynwb` objects by a downstream writer. It builds on top of the `RawDataLoader` shown in `raw_data_loader_example.ipynb`." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "defab714", - "metadata": {}, - "outputs": [], - "source": [ - "%load_ext autoreload\n", - "%autoreload 2\n", - "from pathlib import Path\n", - "\n", - "from dynamic_foraging_processing.process.acquisition import AcqusitionBuilder\n", - "from dynamic_foraging_processing.raw_data_loader import RawDataLoader" - ] - }, - { - "cell_type": "markdown", - "id": "b1a6b673", - "metadata": {}, - "source": [ - "## Load a dataset\n", - "\n", - "Point `RawDataLoader` at the root directory of a dataset acquisition. The builder consumes the underlying `dataset` object." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "db9b70d0", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Update this path to point at a real acquisition directory on your machine.\n", - "dataset_path = Path(\n", - " r\"C:\\Users\\arjun.sridhar\\Downloads\\821586_2026-04-28T200347Z\\821586_2026-04-28T200347Z\"\n", - ")\n", - "\n", - "loader = RawDataLoader(path=dataset_path)\n", - "builder = AcqusitionBuilder(dataset=loader.dataset)\n", - "builder" - ] - }, - { - "cell_type": "markdown", - "id": "3bc226c1", - "metadata": {}, - "source": [ - "## Inspect an individual stream\n", - "\n", - "`get_reward_delivery` returns the filtered `OutputSet` write messages from the Behavior Board as a `pandas.DataFrame`. Use this when you want the raw frame rather than the typed acquisition object." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "bfd5ceeb", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
DOPort0DOPort1DOPort2SupplyPort0SupplyPort1SupplyPort2Led0Led1Rgb0Rgb1DO0DO1DO2DO3MessageType
Time
2.504232e+06FalseFalseFalseTrueFalseFalseFalseFalseFalseFalseFalseFalseFalseFalseWRITE
2.504279e+06FalseFalseFalseTrueFalseFalseFalseFalseFalseFalseFalseFalseFalseFalseWRITE
2.504341e+06FalseFalseFalseTrueFalseFalseFalseFalseFalseFalseFalseFalseFalseFalseWRITE
2.504399e+06FalseFalseFalseTrueFalseFalseFalseFalseFalseFalseFalseFalseFalseFalseWRITE
2.504468e+06FalseFalseFalseTrueFalseFalseFalseFalseFalseFalseFalseFalseFalseFalseWRITE
\n", - "
" - ], - "text/plain": [ - " DOPort0 DOPort1 DOPort2 SupplyPort0 SupplyPort1 \\\n", - "Time \n", - "2.504232e+06 False False False True False \n", - "2.504279e+06 False False False True False \n", - "2.504341e+06 False False False True False \n", - "2.504399e+06 False False False True False \n", - "2.504468e+06 False False False True False \n", - "\n", - " SupplyPort2 Led0 Led1 Rgb0 Rgb1 DO0 DO1 DO2 \\\n", - "Time \n", - "2.504232e+06 False False False False False False False False \n", - "2.504279e+06 False False False False False False False False \n", - "2.504341e+06 False False False False False False False False \n", - "2.504399e+06 False False False False False False False False \n", - "2.504468e+06 False False False False False False False False \n", - "\n", - " DO3 MessageType \n", - "Time \n", - "2.504232e+06 False WRITE \n", - "2.504279e+06 False WRITE \n", - "2.504341e+06 False WRITE \n", - "2.504399e+06 False WRITE \n", - "2.504468e+06 False WRITE " - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "reward_writes = builder.get_reward_delivery()\n", - "reward_writes.head()" - ] - }, - { - "cell_type": "markdown", - "id": "3578e255", - "metadata": {}, - "source": [ - "## Build the Acquisition Collection object\n", - "\n", - "`build_acquisition` returns an `AcquisitionCollection` pydantic model containing one `AcquisitionSeries` per stream. Each series holds `data`, `timestamps`, `unit`, and `description` fields ready to be mapped to a `pynwb.TimeSeries` by a downstream writer." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "041237d6", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "AcquisitionCollection(left_reward_delivery_time=AcquisitionSeries(name='left_reward_delivery_time', data=array([ True, True, True, True, True, True, True, True, True,\n", - " True, True, True, False, False, False, False, False, False,\n", - " False, False, False, False, False, False, False, False, False,\n", - " True, True, True, True, True, True, True, True, True,\n", - " True, True, True, True, True, True, True, True, True,\n", - " False, True, True, True, True, True, True, False, False,\n", - " False, False, False, False, False, False, False, False, False,\n", - " False, False, True, True, True, True, True, True, False,\n", - " True, True, True, True, True, False, False, False, False,\n", - " False, False, False, False, False, False, False, False, False,\n", - " False, False, False, False, True, False, True, True, False,\n", - " True, True, True, True, False, False, False, False, False,\n", - " False, False, False, True, True, True, True, True, True,\n", - " True, True, True, False, False, False, False, False, False,\n", - " False, False, False, False, False, False, False, False, False,\n", - " False, False, False, False, False, False, False, False, False,\n", - " False, False, True, True, True, True, True, True, True,\n", - " True, False, False, False, False, False, False, False, False,\n", - " False, False, False, False, False, False, False, True, True,\n", - " True, True, True, True, True, True, True, True, True,\n", - " True, True, True, True, True, False, False, False, False,\n", - " False, False, False, False, False, False, False, False, False,\n", - " False, True, True, True, True, True, True, True, True]), timestamps=array([2504232.053504, 2504279.20848 , 2504341.438496, 2504398.506496,\n", - " 2504467.84448 , 2504514.383488, 2504565.286496, 2504618.533504,\n", - " 2504618.881504, 2504627.669504, 2504640.941504, 2504643.76048 ,\n", - " 2504651.18448 , 2504662.913504, 2504671.69248 , 2504684.726496,\n", - " 2504691.473504, 2504698.72448 , 2504765.250496, 2504769.64448 ,\n", - " 2504774.645504, 2504777.115488, 2504786.081504, 2504791.007488,\n", - " 2504793.475488, 2504818.827488, 2504820.987488, 2504891.575488,\n", - " 2504934.526496, 2504936.882496, 2504941.302496, 2504952.182496,\n", - " 2504966.27648 , 2504973.441504, 2504980.361504, 2504985.165504,\n", - " 2504990.705504, 2504997.241504, 2505061.589504, 2505081.039488,\n", - " 2505088.125504, 2505091.52448 , 2505107.359488, 2505116.38848 ,\n", - " 2505132.27648 , 2505138.043488, 2505196.32848 , 2505203.946496,\n", - " 2505212.15648 , 2505220.835488, 2505224.494496, 2505232.26448 ,\n", - " 2505290.934496, 2505304.489504, 2505349.795488, 2505360.617504,\n", - " 2505364.109504, 2505372.851488, 2505387.859488, 2505390.28448 ,\n", - " 2505428.921504, 2505449.167488, 2505459.007488, 2505477.175488,\n", - " 2505480.72048 , 2505541.602496, 2505572.531488, 2505592.847488,\n", - " 2505595.28848 , 2505599.363488, 2505603.150496, 2505625.447488,\n", - " 2505675.363488, 2505703.593504, 2505734.519488, 2505798.763488,\n", - " 2505813.827488, 2505851.226496, 2505855.615488, 2505877.821504,\n", - " 2505883.785504, 2505893.427488, 2505920.017504, 2505923.26448 ,\n", - " 2505948.17248 , 2505967.997504, 2505971.865504, 2505987.975488,\n", - " 2506036.009504, 2506070.925504, 2506080.379488, 2506085.177504,\n", - " 2506092.965504, 2506095.507488, 2506150.546496, 2506176.88048 ,\n", - " 2506205.54048 , 2506257.889504, 2506270.477504, 2506301.254496,\n", - " 2506307.003488, 2506311.233504, 2506319.139488, 2506371.395488,\n", - " 2506385.181504, 2506390.94048 , 2506395.761504, 2506422.599488,\n", - " 2506464.15648 , 2506482.24048 , 2506486.081504, 2506545.131488,\n", - " 2506570.63648 , 2506580.686496, 2506587.362496, 2506591.069504,\n", - " 2506594.58848 , 2506612.086496, 2506623.087488, 2506679.23648 ,\n", - " 2506739.62848 , 2506774.262496, 2506815.774496, 2506823.258496,\n", - " 2506839.407488, 2506843.139488, 2506848.971488, 2506873.49648 ,\n", - " 2506883.145504, 2506891.331488, 2506917.966496, 2506921.995488,\n", - " 2506927.949536, 2506943.025504, 2506954.127488, 2506966.94448 ,\n", - " 2506969.78048 , 2506973.654496, 2506976.801504, 2506984.445504,\n", - " 2507003.799488, 2507016.555488, 2507020.971488, 2507024.109504,\n", - " 2507034.91248 , 2507037.46048 , 2507093.973504, 2507140.530496,\n", - " 2507146.529504, 2507151.947488, 2507170.374496, 2507221.669504,\n", - " 2507283.739488, 2507301.88048 , 2507339.06448 , 2507355.04048 ,\n", - " 2507357.250496, 2507375.401504, 2507382.273504, 2507391.831488,\n", - " 2507396.775488, 2507402.291488, 2507411.589504, 2507418.918496,\n", - " 2507424.690528, 2507441.539488, 2507450.459488, 2507463.30848 ,\n", - " 2507490.667488, 2507536.605504, 2507585.907488, 2507592.086496,\n", - " 2507599.551488, 2507611.819488, 2507616.17648 , 2507630.40448 ,\n", - " 2507639.751488, 2507648.72048 , 2507651.27648 , 2507664.36848 ,\n", - " 2507678.305504, 2507719.806496, 2507730.76848 , 2507733.32448 ,\n", - " 2507747.76848 , 2507784.155488, 2507793.22048 , 2507801.16048 ,\n", - " 2507807.842496, 2507815.59648 , 2507819.439488, 2507833.76848 ,\n", - " 2507838.01648 , 2507840.645504, 2507851.04848 , 2507858.631488,\n", - " 2507864.510496, 2507870.893504, 2507908.498496, 2507951.117504,\n", - " 2508012.931488, 2508022.565504, 2508025.83248 , 2508042.771488,\n", - " 2508048.06848 , 2508051.125504, 2508071.53648 ]), unit='second', description='The reward delivery time of the left lick port. The data field annotates whether the reward was earned, manual, or automatic'), right_reward_delivery_time=AcquisitionSeries(name='right_reward_delivery_time', data=array([False, False, False, False, False, False, False, False, False,\n", - " False, False, False, True, True, True, True, True, True,\n", - " True, True, True, True, True, True, True, True, True,\n", - " False, False, False, False, False, False, False, False, False,\n", - " False, False, False, False, False, False, False, False, False,\n", - " True, False, False, False, False, False, False, True, True,\n", - " True, True, True, True, True, True, True, True, True,\n", - " True, True, False, False, False, False, False, False, True,\n", - " False, False, False, False, False, True, True, True, True,\n", - " True, True, True, True, True, True, True, True, True,\n", - " True, True, True, True, False, True, False, False, True,\n", - " False, False, False, False, True, True, True, True, True,\n", - " True, True, True, False, False, False, False, False, False,\n", - " False, False, False, True, True, True, True, True, True,\n", - " True, True, True, True, True, True, True, True, True,\n", - " True, True, True, True, True, True, True, True, True,\n", - " True, True, False, False, False, False, False, False, False,\n", - " False, True, True, True, True, True, True, True, True,\n", - " True, True, True, True, True, True, True, False, False,\n", - " False, False, False, False, False, False, False, False, False,\n", - " False, False, False, False, False, True, True, True, True,\n", - " True, True, True, True, True, True, True, True, True,\n", - " True, False, False, False, False, False, False, False, False]), timestamps=array([2504232.053504, 2504279.20848 , 2504341.438496, 2504398.506496,\n", - " 2504467.84448 , 2504514.383488, 2504565.286496, 2504618.533504,\n", - " 2504618.881504, 2504627.669504, 2504640.941504, 2504643.76048 ,\n", - " 2504651.18448 , 2504662.913504, 2504671.69248 , 2504684.726496,\n", - " 2504691.473504, 2504698.72448 , 2504765.250496, 2504769.64448 ,\n", - " 2504774.645504, 2504777.115488, 2504786.081504, 2504791.007488,\n", - " 2504793.475488, 2504818.827488, 2504820.987488, 2504891.575488,\n", - " 2504934.526496, 2504936.882496, 2504941.302496, 2504952.182496,\n", - " 2504966.27648 , 2504973.441504, 2504980.361504, 2504985.165504,\n", - " 2504990.705504, 2504997.241504, 2505061.589504, 2505081.039488,\n", - " 2505088.125504, 2505091.52448 , 2505107.359488, 2505116.38848 ,\n", - " 2505132.27648 , 2505138.043488, 2505196.32848 , 2505203.946496,\n", - " 2505212.15648 , 2505220.835488, 2505224.494496, 2505232.26448 ,\n", - " 2505290.934496, 2505304.489504, 2505349.795488, 2505360.617504,\n", - " 2505364.109504, 2505372.851488, 2505387.859488, 2505390.28448 ,\n", - " 2505428.921504, 2505449.167488, 2505459.007488, 2505477.175488,\n", - " 2505480.72048 , 2505541.602496, 2505572.531488, 2505592.847488,\n", - " 2505595.28848 , 2505599.363488, 2505603.150496, 2505625.447488,\n", - " 2505675.363488, 2505703.593504, 2505734.519488, 2505798.763488,\n", - " 2505813.827488, 2505851.226496, 2505855.615488, 2505877.821504,\n", - " 2505883.785504, 2505893.427488, 2505920.017504, 2505923.26448 ,\n", - " 2505948.17248 , 2505967.997504, 2505971.865504, 2505987.975488,\n", - " 2506036.009504, 2506070.925504, 2506080.379488, 2506085.177504,\n", - " 2506092.965504, 2506095.507488, 2506150.546496, 2506176.88048 ,\n", - " 2506205.54048 , 2506257.889504, 2506270.477504, 2506301.254496,\n", - " 2506307.003488, 2506311.233504, 2506319.139488, 2506371.395488,\n", - " 2506385.181504, 2506390.94048 , 2506395.761504, 2506422.599488,\n", - " 2506464.15648 , 2506482.24048 , 2506486.081504, 2506545.131488,\n", - " 2506570.63648 , 2506580.686496, 2506587.362496, 2506591.069504,\n", - " 2506594.58848 , 2506612.086496, 2506623.087488, 2506679.23648 ,\n", - " 2506739.62848 , 2506774.262496, 2506815.774496, 2506823.258496,\n", - " 2506839.407488, 2506843.139488, 2506848.971488, 2506873.49648 ,\n", - " 2506883.145504, 2506891.331488, 2506917.966496, 2506921.995488,\n", - " 2506927.949536, 2506943.025504, 2506954.127488, 2506966.94448 ,\n", - " 2506969.78048 , 2506973.654496, 2506976.801504, 2506984.445504,\n", - " 2507003.799488, 2507016.555488, 2507020.971488, 2507024.109504,\n", - " 2507034.91248 , 2507037.46048 , 2507093.973504, 2507140.530496,\n", - " 2507146.529504, 2507151.947488, 2507170.374496, 2507221.669504,\n", - " 2507283.739488, 2507301.88048 , 2507339.06448 , 2507355.04048 ,\n", - " 2507357.250496, 2507375.401504, 2507382.273504, 2507391.831488,\n", - " 2507396.775488, 2507402.291488, 2507411.589504, 2507418.918496,\n", - " 2507424.690528, 2507441.539488, 2507450.459488, 2507463.30848 ,\n", - " 2507490.667488, 2507536.605504, 2507585.907488, 2507592.086496,\n", - " 2507599.551488, 2507611.819488, 2507616.17648 , 2507630.40448 ,\n", - " 2507639.751488, 2507648.72048 , 2507651.27648 , 2507664.36848 ,\n", - " 2507678.305504, 2507719.806496, 2507730.76848 , 2507733.32448 ,\n", - " 2507747.76848 , 2507784.155488, 2507793.22048 , 2507801.16048 ,\n", - " 2507807.842496, 2507815.59648 , 2507819.439488, 2507833.76848 ,\n", - " 2507838.01648 , 2507840.645504, 2507851.04848 , 2507858.631488,\n", - " 2507864.510496, 2507870.893504, 2507908.498496, 2507951.117504,\n", - " 2508012.931488, 2508022.565504, 2508025.83248 , 2508042.771488,\n", - " 2508048.06848 , 2508051.125504, 2508071.53648 ]), unit='second', description='The reward delivery time of the right lick port. The data field annotates whether the reward was earned, manual, or automatic'))" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "acquisition = builder.build_acquisition()\n", - "acquisition" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "0d73bc6f", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "name: left_reward_delivery_time\n", - "unit: second\n", - "description: The reward delivery time of the left lick port. The data field annotates whether the reward was earned, manual, or automatic\n", - "n samples: 207\n" - ] - }, - { - "data": { - "text/plain": [ - "array([2504232.053504, 2504279.20848 , 2504341.438496, 2504398.506496,\n", - " 2504467.84448 , 2504514.383488, 2504565.286496, 2504618.533504,\n", - " 2504618.881504, 2504627.669504])" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "left = acquisition.left_reward_delivery_time\n", - "print(f\"name: {left.name}\")\n", - "print(f\"unit: {left.unit}\")\n", - "print(f\"description: {left.description}\")\n", - "print(f\"n samples: {left.data.shape[0]}\")\n", - "left.timestamps[:10]" - ] - }, - { - "cell_type": "markdown", - "id": "581e034b", - "metadata": {}, - "source": [ - "## Serialize for downstream use\n", - "\n", - "Because `AcquisitionCollection` is a pydantic model, you can dump the field structure (without the heavy array payloads) for logging or manifest creation." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "3fc6f1c6", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'left_reward_delivery_time': {'unit': 'second',\n", - " 'description': 'The reward delivery time of the left lick port. The data field annotates whether the reward was earned, manual, or automatic',\n", - " 'n_samples': 207},\n", - " 'right_reward_delivery_time': {'unit': 'second',\n", - " 'description': 'The reward delivery time of the right lick port. The data field annotates whether the reward was earned, manual, or automatic',\n", - " 'n_samples': 207}}" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "{\n", - " name: {\n", - " \"unit\": series.unit,\n", - " \"description\": series.description,\n", - " \"n_samples\": series.data.shape[0],\n", - " }\n", - " for name, series in acquisition\n", - "}" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "e48ab0b8", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "dynamic-foraging-processing", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.13" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/src/dynamic_foraging_processing/nwb/__init__.py b/src/dynamic_foraging_processing/nwb/__init__.py new file mode 100644 index 0000000..70bf214 --- /dev/null +++ b/src/dynamic_foraging_processing/nwb/__init__.py @@ -0,0 +1 @@ +"""NWB modules for dynamic foraging datasets.""" diff --git a/src/dynamic_foraging_processing/process/__init__.py b/src/dynamic_foraging_processing/process/__init__.py deleted file mode 100644 index ec793c6..0000000 --- a/src/dynamic_foraging_processing/process/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Processing modules for dynamic foraging datasets.""" diff --git a/src/dynamic_foraging_processing/process/acquisition/__init__.py b/src/dynamic_foraging_processing/process/acquisition/__init__.py deleted file mode 100644 index 7a47419..0000000 --- a/src/dynamic_foraging_processing/process/acquisition/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Acquisition processing for dynamic foraging datasets.""" - -from dynamic_foraging_processing.process.acquisition.acquisition_builder import AcqusitionBuilder -from dynamic_foraging_processing.process.acquisition.models import ( - AcquisitionCollection, - AcquisitionSeries, -) - -__all__ = ["AcqusitionBuilder", "AcquisitionCollection", "AcquisitionSeries"] diff --git a/src/dynamic_foraging_processing/process/acquisition/acquisition_builder.py b/src/dynamic_foraging_processing/process/acquisition/acquisition_builder.py deleted file mode 100644 index 65d7858..0000000 --- a/src/dynamic_foraging_processing/process/acquisition/acquisition_builder.py +++ /dev/null @@ -1,64 +0,0 @@ -"""Acquisition builder for NWB acquisition module.""" - -import contraqctor -import pandas as pd - -from dynamic_foraging_processing.process.acquisition.models import ( - AcquisitionCollection, - AcquisitionSeries, -) - - -class AcqusitionBuilder: - """Builds the NWB acquisition module from raw dynamic foraging data.""" - - def __init__(self, dataset: contraqctor.contract.Dataset): - """Initialize the acquisition builder. - - Parameters - ---------- - dataset : contraqctor.contract.Dataset - Dataset providing access to the dynamic foraging data. - """ - self.dataset = dataset - - def get_reward_delivery(self) -> pd.DataFrame: - """Get the reward delivery stream from the dataset. - - Returns - ------- - pandas.DataFrame - DataFrame from the loaded ``OutputSet`` stream under - ``Behavior/HarpBehavior``. - """ - data = self.dataset.at("Behavior").at("HarpBehavior").at("OutputSet").load().data - data_write_messages = data[data["MessageType"] == "WRITE"] - - return data_write_messages - - def build_acquisition(self) -> AcquisitionCollection: - """Build the NWB acquisition collection. - - Returns - ------- - AcquisitionCollection - Object holding all acquisition series. - """ - writes = self.get_reward_delivery() - # TODO: fix data so that it is array of annotations of whether the reward was earned, manual, or automatic - return AcquisitionCollection( - left_reward_delivery_time=AcquisitionSeries( - name="left_reward_delivery_time", - data=writes["SupplyPort0"].to_numpy(), - timestamps=writes.index.to_numpy(), - unit="second", - description="The reward delivery time of the left lick port. The data field annotates whether the reward was earned, manual, or automatic", - ), - right_reward_delivery_time=AcquisitionSeries( - name="right_reward_delivery_time", - data=writes["SupplyPort1"].to_numpy(), - timestamps=writes.index.to_numpy(), - unit="second", - description="The reward delivery time of the right lick port. The data field annotates whether the reward was earned, manual, or automatic", - ), - ) diff --git a/src/dynamic_foraging_processing/process/acquisition/models.py b/src/dynamic_foraging_processing/process/acquisition/models.py deleted file mode 100644 index d03c74a..0000000 --- a/src/dynamic_foraging_processing/process/acquisition/models.py +++ /dev/null @@ -1,66 +0,0 @@ -"""Pydantic models mirroring the NWB acquisition schema. - -These types decouple the processing layer from ``pynwb``: builders produce -plain pydantic models that an NWB writer can later translate into the -corresponding ``pynwb`` objects. -""" - -import numpy as np -import pandas as pd -from pydantic import BaseModel, ConfigDict, model_validator - - -class AcquisitionSeries(BaseModel): - """Single time series destined for an NWB acquisition entry. - - Parameters - ---------- - name : str - Name of the series. - data : numpy.ndarray or pandas.Series - Sample values. - timestamps : numpy.ndarray or pandas.Series - Timestamps aligned with ``data``. - unit : str - Unit describing the data. - description : str - Human-readable description of the series. - """ - - # Allow numpy/pandas fields (no native pydantic schema) and make instances immutable. - model_config = ConfigDict(arbitrary_types_allowed=True, frozen=True) - - name: str - data: np.ndarray | pd.Series - timestamps: np.ndarray | pd.Series - unit: str - description: str - - @model_validator(mode="after") - def _check_lengths(self) -> "AcquisitionSeries": - """Validate that ``data`` and ``timestamps`` have matching lengths.""" - if self.data.shape[0] != self.timestamps.shape[0]: - raise ValueError( - f"data and timestamps must have the same length for series " - f"{self.name!r}: got {self.data.shape[0]} and " - f"{self.timestamps.shape[0]}." - ) - return self - - -class AcquisitionCollection(BaseModel): - """Collection of acquisition series for the NWB acquisition module. - - Parameters - ---------- - left_reward_delivery_time : AcquisitionSeries - Reward delivery events for the left port. - right_reward_delivery_time : AcquisitionSeries - Reward delivery events for the right port. - """ - - # Immutable container; nested AcquisitionSeries already permits arbitrary types. - model_config = ConfigDict(frozen=True) - - left_reward_delivery_time: AcquisitionSeries - right_reward_delivery_time: AcquisitionSeries diff --git a/tests/test_nwb/__init__.py b/tests/test_nwb/__init__.py new file mode 100644 index 0000000..be048ac --- /dev/null +++ b/tests/test_nwb/__init__.py @@ -0,0 +1 @@ +"""Tests for ``dynamic_foraging_processing.nwb``.""" diff --git a/tests/test_process/__init__.py b/tests/test_process/__init__.py deleted file mode 100644 index 70b4143..0000000 --- a/tests/test_process/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for ``dynamic_foraging_processing.process``.""" diff --git a/tests/test_process/test_acquisition/__init__.py b/tests/test_process/test_acquisition/__init__.py deleted file mode 100644 index bcb6321..0000000 --- a/tests/test_process/test_acquisition/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for ``dynamic_foraging_processing.process.acquisition``.""" diff --git a/tests/test_process/test_acquisition/test_acquisition_builder.py b/tests/test_process/test_acquisition/test_acquisition_builder.py deleted file mode 100644 index fc69947..0000000 --- a/tests/test_process/test_acquisition/test_acquisition_builder.py +++ /dev/null @@ -1,99 +0,0 @@ -"""Tests for ``dynamic_foraging_processing.process.acquisition.acquisition_builder``.""" - -from unittest.mock import MagicMock - -import numpy as np -import pandas as pd - -from dynamic_foraging_processing.process.acquisition import AcqusitionBuilder -from dynamic_foraging_processing.process.acquisition.models import ( - AcquisitionCollection, - AcquisitionSeries, -) - - -def _make_output_set_frame() -> pd.DataFrame: - """Build an OutputSet-like DataFrame with WRITE and non-WRITE rows.""" - return pd.DataFrame( - { - "MessageType": ["WRITE", "READ", "WRITE"], - "SupplyPort0": [1, 0, 0], - "SupplyPort1": [0, 0, 1], - }, - index=pd.Index([0.1, 0.2, 0.3], name="time"), - ) - - -def _make_dataset(frame: pd.DataFrame) -> MagicMock: - """Build a mock dataset whose ``at`` chain resolves to ``frame``.""" - dataset = MagicMock() - stream = MagicMock() - stream.data = frame - dataset.at.return_value.at.return_value.at.return_value.load.return_value = stream - return dataset - - -# --------------------------------------------------------------------------- -# __init__ -# --------------------------------------------------------------------------- - - -def test_init_stores_dataset(): - """The provided dataset is stored on the instance.""" - ds = _make_dataset(_make_output_set_frame()) - builder = AcqusitionBuilder(dataset=ds) - assert builder.dataset is ds - - -# --------------------------------------------------------------------------- -# get_reward_delivery -# --------------------------------------------------------------------------- - - -def test_get_reward_delivery_filters_to_write_messages(): - """Only ``MessageType == 'WRITE'`` rows are returned.""" - frame = _make_output_set_frame() - ds = _make_dataset(frame) - builder = AcqusitionBuilder(dataset=ds) - - result = builder.get_reward_delivery() - - assert list(result["MessageType"]) == ["WRITE", "WRITE"] - assert list(result.index) == [0.1, 0.3] - ds.at.assert_called_once_with("Behavior") - ds.at.return_value.at.assert_called_once_with("HarpBehavior") - ds.at.return_value.at.return_value.at.assert_called_once_with("OutputSet") - - -# --------------------------------------------------------------------------- -# build_acquisition -# --------------------------------------------------------------------------- - - -def test_build_acquisition_returns_populated_nwb_acquisition(): - """``build_acquisition`` returns an ``AcquisitionCollection`` with both ports populated.""" - frame = _make_output_set_frame() - ds = _make_dataset(frame) - builder = AcqusitionBuilder(dataset=ds) - - acquisition = builder.build_acquisition() - - assert isinstance(acquisition, AcquisitionCollection) - - left = acquisition.left_reward_delivery_time - right = acquisition.right_reward_delivery_time - assert isinstance(left, AcquisitionSeries) - assert isinstance(right, AcquisitionSeries) - - expected_timestamps = np.array([0.1, 0.3]) - np.testing.assert_array_equal(left.data, np.array([1, 0])) - np.testing.assert_array_equal(left.timestamps, expected_timestamps) - assert left.name == "left_reward_delivery_time" - assert left.unit == "second" - assert "left lick port" in left.description - - np.testing.assert_array_equal(right.data, np.array([0, 1])) - np.testing.assert_array_equal(right.timestamps, expected_timestamps) - assert right.name == "right_reward_delivery_time" - assert right.unit == "second" - assert "right lick port" in right.description diff --git a/tests/test_process/test_acquisition/test_models.py b/tests/test_process/test_acquisition/test_models.py deleted file mode 100644 index e55025e..0000000 --- a/tests/test_process/test_acquisition/test_models.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Tests for ``dynamic_foraging_processing.process.acquisition.models``.""" - -import numpy as np -import pytest - -from dynamic_foraging_processing.process.acquisition.models import ( - AcquisitionCollection, - AcquisitionSeries, -) - - -def _make_series(name: str = "series", n: int = 3) -> AcquisitionSeries: - """Build a valid ``AcquisitionSeries`` for tests.""" - return AcquisitionSeries( - name=name, - data=np.arange(n), - timestamps=np.arange(n, dtype=float), - unit="second", - description="desc", - ) - - -# --------------------------------------------------------------------------- -# AcquisitionSeries -# --------------------------------------------------------------------------- - - -def test_acquisition_series_valid(): - """Equal-length data and timestamps construct successfully.""" - series = _make_series() - assert series.name == "series" - assert series.unit == "second" - assert series.description == "desc" - assert series.data.shape[0] == series.timestamps.shape[0] - - -def test_acquisition_series_length_mismatch_raises(): - """Mismatched data and timestamps lengths raise a validation error.""" - with pytest.raises(ValueError, match="must have the same length"): - AcquisitionSeries( - name="bad", - data=np.arange(3), - timestamps=np.arange(2, dtype=float), - unit="second", - description="desc", - ) - - -def test_acquisition_series_is_frozen(): - """Instances are immutable.""" - series = _make_series() - with pytest.raises(ValueError): - series.name = "other" - - -# --------------------------------------------------------------------------- -# AcquisitionCollection -# --------------------------------------------------------------------------- - - -def test_acquisition_collection_holds_series(): - """``AcquisitionCollection`` stores both reward delivery series.""" - left = _make_series("reward_delivery_left") - right = _make_series("reward_delivery_right") - acquisition = AcquisitionCollection( - left_reward_delivery_time=left, - right_reward_delivery_time=right, - ) - assert acquisition.left_reward_delivery_time is left - assert acquisition.right_reward_delivery_time is right - - -def test_acquisition_collection_is_frozen(): - """``AcquisitionCollection`` instances are immutable.""" - acquisition = AcquisitionCollection( - left_reward_delivery_time=_make_series("reward_delivery_left"), - right_reward_delivery_time=_make_series("reward_delivery_right"), - ) - with pytest.raises(ValueError): - acquisition.left_reward_delivery_time = _make_series("other") From 43905fc4b31e991277501860a3e3f496f2480240 Mon Sep 17 00:00:00 2001 From: arjunsridhar12345 Date: Wed, 3 Jun 2026 17:52:05 -0700 Subject: [PATCH 12/36] feat: add nwb acquisition builder --- .../nwb/__init__.py | 12 +++ .../nwb/acquisition/__init__.py | 13 +++ .../nwb/acquisition/acquisition_builder.py | 92 +++++++++++++++++++ .../nwb/acquisition/models.py | 68 ++++++++++++++ 4 files changed, 185 insertions(+) create mode 100644 src/dynamic_foraging_processing/nwb/acquisition/__init__.py create mode 100644 src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py create mode 100644 src/dynamic_foraging_processing/nwb/acquisition/models.py diff --git a/src/dynamic_foraging_processing/nwb/__init__.py b/src/dynamic_foraging_processing/nwb/__init__.py index 70bf214..92cd713 100644 --- a/src/dynamic_foraging_processing/nwb/__init__.py +++ b/src/dynamic_foraging_processing/nwb/__init__.py @@ -1 +1,13 @@ """NWB modules for dynamic foraging datasets.""" + +from dynamic_foraging_processing.nwb.acquisition import ( + AcquisitionBuilder, + AcquisitionSeries, + AcquisitionTable, +) + +__all__ = [ + "AcquisitionBuilder", + "AcquisitionSeries", + "AcquisitionTable", +] diff --git a/src/dynamic_foraging_processing/nwb/acquisition/__init__.py b/src/dynamic_foraging_processing/nwb/acquisition/__init__.py new file mode 100644 index 0000000..36efb69 --- /dev/null +++ b/src/dynamic_foraging_processing/nwb/acquisition/__init__.py @@ -0,0 +1,13 @@ +"""NWB acquisition models.""" + +from dynamic_foraging_processing.nwb.acquisition.acquisition_builder import AcquisitionBuilder +from dynamic_foraging_processing.nwb.acquisition.models import ( + AcquisitionSeries, + AcquisitionTable, +) + +__all__ = [ + "AcquisitionBuilder", + "AcquisitionSeries", + "AcquisitionTable", +] diff --git a/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py b/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py new file mode 100644 index 0000000..aa2c1fa --- /dev/null +++ b/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py @@ -0,0 +1,92 @@ +"""Acquisition builder for NWB acquisition module.""" + +import typing as t + +import pandas as pd + +from dynamic_foraging_processing.nwb.acquisition.models import ( + AcquisitionSeries, + AcquisitionTable, +) +from dynamic_foraging_processing.raw_data_loader import RawDataLoader + + +class AcquisitionBuilder: + """Builds the NWB acquisition module from raw dynamic foraging data.""" + + def __init__(self, loader: RawDataLoader): + """Initialize the acquisition builder. + + Parameters + ---------- + loader : RawDataLoader + Loader providing access to the dynamic foraging dataset. + """ + self.loader = loader + + def get_reward_delivery(self) -> pd.DataFrame: + """Get the reward delivery stream from the dataset. + + Returns + ------- + pandas.DataFrame + DataFrame from the loaded ``OutputSet`` stream under + ``Behavior/HarpBehavior``. + """ + data = self.loader.dataset.at("Behavior").at("HarpBehavior").at("OutputSet").load().data + data_write_messages = data[data["MessageType"] == "WRITE"] + + return data_write_messages + + def build_acquisition(self) -> t.List[t.Union[AcquisitionSeries, AcquisitionTable]]: + """Build the NWB acquisition entries. + + Returns + ------- + list of AcquisitionSeries or AcquisitionTable + Acquisition entries to write to the NWB acquisition module. + """ + rewards = self.get_reward_delivery() + acquisition_streams = self.loader.get_all_raw_data() + acqusition_streams_descriptions = self.loader.raw_data_stream_descriptions + + acquisiton_entries: t.List[t.Union[AcquisitionSeries, AcquisitionTable]] = [] + + for stream_name, stream_data in acquisition_streams.items(): + description = acqusition_streams_descriptions.get(stream_name, "") + acquisiton_entries.append( + AcquisitionTable( + name=stream_name, + data=stream_data, + description=description, + ) + ) + + # TODO: fix data so that it is array of annotations of whether the reward was earned, manual, or automatic + # TODO: add left and right lick times + acquisiton_entries.append( + AcquisitionSeries( + name="left_reward_delivery_time", + data=rewards["SupplyPort0"].to_numpy(), + timestamps=rewards.index.to_numpy(), + unit="second", + description=( + "The reward delivery time of the left lick port. The data field " + "annotates whether the reward was earned, manual, or automatic" + ), + ) + ) + acquisiton_entries.append( + AcquisitionSeries( + name="right_reward_delivery_time", + data=rewards["SupplyPort1"].to_numpy(), + timestamps=rewards.index.to_numpy(), + unit="second", + description=( + "The reward delivery time of the right lick port. The data field " + "annotates whether the reward was earned, manual, or automatic" + ), + ) + ) + + return acquisiton_entries diff --git a/src/dynamic_foraging_processing/nwb/acquisition/models.py b/src/dynamic_foraging_processing/nwb/acquisition/models.py new file mode 100644 index 0000000..d711b74 --- /dev/null +++ b/src/dynamic_foraging_processing/nwb/acquisition/models.py @@ -0,0 +1,68 @@ +"""Pydantic models mirroring the NWB acquisition schema. + +These types decouple the layer from ``pynwb``: builders produce +plain pydantic models that an NWB writer can later translate into the +corresponding ``pynwb`` objects. +""" + +import numpy as np +import pandas as pd +from pydantic import BaseModel, ConfigDict, model_validator + + +class AcquisitionSeries(BaseModel): + """Single time series destined for an NWB acquisition entry. + + Parameters + ---------- + name : str + Name of the series. + data : numpy.ndarray or pandas.Series + Sample values. + timestamps : numpy.ndarray or pandas.Series + Timestamps aligned with ``data``. + unit : str + Unit describing the data. + description : str + Human-readable description of the series. + """ + + # Allow numpy/pandas fields (no native pydantic schema) and make instances immutable. + model_config = ConfigDict(arbitrary_types_allowed=True, frozen=True) + + name: str + data: np.ndarray | pd.Series + timestamps: np.ndarray | pd.Series + unit: str + description: str + + @model_validator(mode="after") + def _check_lengths(self) -> "AcquisitionSeries": + """Validate that ``data`` and ``timestamps`` have matching lengths.""" + if self.data.shape[0] != self.timestamps.shape[0]: + raise ValueError( + f"data and timestamps must have the same length for series " + f"{self.name!r}: got {self.data.shape[0]} and " + f"{self.timestamps.shape[0]}." + ) + return self + + +class AcquisitionTable(BaseModel): + """Tabular acquisition entry destined for an NWB ``DynamicTable``. + + Parameters + ---------- + name : str + Name of the table. + data : pandas.DataFrame + Rows and columns of the table. + description : str + Human-readable description of the table. + """ + + model_config = ConfigDict(arbitrary_types_allowed=True, frozen=True) + + name: str + data: pd.DataFrame + description: str From 8c21d20a44a8c30388890b09698d168036da60e3 Mon Sep 17 00:00:00 2001 From: arjunsridhar12345 Date: Wed, 3 Jun 2026 17:52:30 -0700 Subject: [PATCH 13/36] test: update tests --- tests/test_nwb/test_acquisition/__init__.py | 1 + .../test_acquisition_builder.py | 94 +++++++++++++++++++ .../test_nwb/test_acquisition/test_models.py | 74 +++++++++++++++ 3 files changed, 169 insertions(+) create mode 100644 tests/test_nwb/test_acquisition/__init__.py create mode 100644 tests/test_nwb/test_acquisition/test_acquisition_builder.py create mode 100644 tests/test_nwb/test_acquisition/test_models.py diff --git a/tests/test_nwb/test_acquisition/__init__.py b/tests/test_nwb/test_acquisition/__init__.py new file mode 100644 index 0000000..e2a089d --- /dev/null +++ b/tests/test_nwb/test_acquisition/__init__.py @@ -0,0 +1 @@ +"""Tests for the NWB acquisition subpackage.""" diff --git a/tests/test_nwb/test_acquisition/test_acquisition_builder.py b/tests/test_nwb/test_acquisition/test_acquisition_builder.py new file mode 100644 index 0000000..07374ee --- /dev/null +++ b/tests/test_nwb/test_acquisition/test_acquisition_builder.py @@ -0,0 +1,94 @@ +"""Tests for ``dynamic_foraging_processing.nwb.acquisition.acquisition_builder``.""" + +from unittest.mock import MagicMock + +import numpy as np +import pandas as pd + +from dynamic_foraging_processing.nwb.acquisition import AcquisitionBuilder +from dynamic_foraging_processing.nwb.acquisition.models import ( + AcquisitionSeries, + AcquisitionTable, +) + + +def _make_output_set_frame() -> pd.DataFrame: + """Build an OutputSet-like DataFrame with WRITE and non-WRITE rows.""" + return pd.DataFrame( + { + "MessageType": ["WRITE", "READ", "WRITE"], + "SupplyPort0": [1, 0, 0], + "SupplyPort1": [0, 0, 1], + }, + index=pd.Index([0.1, 0.2, 0.3], name="time"), + ) + + +def _make_loader(frame: pd.DataFrame) -> MagicMock: + """Build a mock loader whose ``dataset.at`` chain resolves to ``frame``.""" + loader = MagicMock() + stream = MagicMock() + stream.data = frame + loader.dataset.at.return_value.at.return_value.at.return_value.load.return_value = stream + loader.get_all_raw_data.return_value = { + "Behavior.RawStream": pd.DataFrame({"x": [1, 2, 3]}), + } + loader.raw_data_stream_descriptions = {"Behavior.RawStream": "raw stream desc"} + return loader + + +def test_init_stores_loader(): + """The provided loader is stored on the instance.""" + loader = _make_loader(_make_output_set_frame()) + builder = AcquisitionBuilder(loader=loader) + assert builder.loader is loader + + +def test_get_reward_delivery_filters_to_write_messages(): + """Only ``MessageType == 'WRITE'`` rows are returned.""" + frame = _make_output_set_frame() + loader = _make_loader(frame) + builder = AcquisitionBuilder(loader=loader) + + result = builder.get_reward_delivery() + + assert list(result["MessageType"]) == ["WRITE", "WRITE"] + assert list(result.index) == [0.1, 0.3] + ds = loader.dataset + ds.at.assert_called_once_with("Behavior") + ds.at.return_value.at.assert_called_once_with("HarpBehavior") + ds.at.return_value.at.return_value.at.assert_called_once_with("OutputSet") + + +def test_build_acquisition_returns_populated_list(): + """``build_acquisition`` returns table entries plus both reward port series.""" + frame = _make_output_set_frame() + loader = _make_loader(frame) + builder = AcquisitionBuilder(loader=loader) + + acquisition = builder.build_acquisition() + + assert isinstance(acquisition, list) + assert len(acquisition) == 3 + + table, left, right = acquisition + assert isinstance(table, AcquisitionTable) + assert table.name == "Behavior.RawStream" + assert table.description == "raw stream desc" + assert list(table.data.columns) == ["x"] + + assert isinstance(left, AcquisitionSeries) + assert isinstance(right, AcquisitionSeries) + + expected_timestamps = np.array([0.1, 0.3]) + np.testing.assert_array_equal(left.data, np.array([1, 0])) + np.testing.assert_array_equal(left.timestamps, expected_timestamps) + assert left.name == "left_reward_delivery_time" + assert left.unit == "second" + assert "left lick port" in left.description + + np.testing.assert_array_equal(right.data, np.array([0, 1])) + np.testing.assert_array_equal(right.timestamps, expected_timestamps) + assert right.name == "right_reward_delivery_time" + assert right.unit == "second" + assert "right lick port" in right.description diff --git a/tests/test_nwb/test_acquisition/test_models.py b/tests/test_nwb/test_acquisition/test_models.py new file mode 100644 index 0000000..31f7c8a --- /dev/null +++ b/tests/test_nwb/test_acquisition/test_models.py @@ -0,0 +1,74 @@ +"""Tests for ``dynamic_foraging_processing.nwb.acquisition.models``.""" + +import numpy as np +import pandas as pd +import pytest + +from dynamic_foraging_processing.nwb.acquisition.models import ( + AcquisitionSeries, + AcquisitionTable, +) + + +def _make_series(name: str = "series", n: int = 3) -> AcquisitionSeries: + """Build a valid ``AcquisitionSeries`` for tests.""" + return AcquisitionSeries( + name=name, + data=np.arange(n), + timestamps=np.arange(n, dtype=float), + unit="second", + description="desc", + ) + + +def test_acquisition_series_valid(): + """Equal-length data and timestamps construct successfully.""" + series = _make_series() + assert series.name == "series" + assert series.unit == "second" + assert series.description == "desc" + assert series.data.shape[0] == series.timestamps.shape[0] + + +def test_acquisition_series_length_mismatch_raises(): + """Mismatched data and timestamps lengths raise a validation error.""" + with pytest.raises(ValueError, match="must have the same length"): + AcquisitionSeries( + name="bad", + data=np.arange(3), + timestamps=np.arange(2, dtype=float), + unit="second", + description="desc", + ) + + +def test_acquisition_series_is_frozen(): + """Instances are immutable.""" + series = _make_series() + with pytest.raises(ValueError): + series.name = "other" + + +def _make_table(name: str = "table") -> AcquisitionTable: + """Build a valid ``AcquisitionTable`` for tests.""" + return AcquisitionTable( + name=name, + data=pd.DataFrame({"a": [1, 2], "b": [3, 4]}), + description="desc", + ) + + +def test_acquisition_table_valid(): + """``AcquisitionTable`` constructs successfully with a DataFrame.""" + table = _make_table() + assert table.name == "table" + assert table.description == "desc" + assert list(table.data.columns) == ["a", "b"] + assert len(table.data) == 2 + + +def test_acquisition_table_is_frozen(): + """``AcquisitionTable`` instances are immutable.""" + table = _make_table() + with pytest.raises(ValueError): + table.name = "other" From eb846ed3fd51ddaf0a6cc137f9ed4a1b83402606 Mon Sep 17 00:00:00 2001 From: arjunsridhar12345 Date: Thu, 4 Jun 2026 15:03:07 -0700 Subject: [PATCH 14/36] feat: start adding logic for getting reward info to acquisition --- .../nwb/acquisition/acquisition_builder.py | 5 +- .../utils/__init__.py | 6 ++ .../utils/rewards.py | 53 +++++++++++++++ .../utils/timestamps.py | 66 +++++++++++++++++++ 4 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 src/dynamic_foraging_processing/utils/__init__.py create mode 100644 src/dynamic_foraging_processing/utils/rewards.py create mode 100644 src/dynamic_foraging_processing/utils/timestamps.py diff --git a/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py b/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py index aa2c1fa..2c4c9b5 100644 --- a/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py +++ b/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py @@ -47,6 +47,7 @@ def build_acquisition(self) -> t.List[t.Union[AcquisitionSeries, AcquisitionTabl Acquisition entries to write to the NWB acquisition module. """ rewards = self.get_reward_delivery() + acquisition_streams = self.loader.get_all_raw_data() acqusition_streams_descriptions = self.loader.raw_data_stream_descriptions @@ -68,7 +69,7 @@ def build_acquisition(self) -> t.List[t.Union[AcquisitionSeries, AcquisitionTabl AcquisitionSeries( name="left_reward_delivery_time", data=rewards["SupplyPort0"].to_numpy(), - timestamps=rewards.index.to_numpy(), + timestamps=rewards["SupplyPort0"].index.to_numpy(), unit="second", description=( "The reward delivery time of the left lick port. The data field " @@ -80,7 +81,7 @@ def build_acquisition(self) -> t.List[t.Union[AcquisitionSeries, AcquisitionTabl AcquisitionSeries( name="right_reward_delivery_time", data=rewards["SupplyPort1"].to_numpy(), - timestamps=rewards.index.to_numpy(), + timestamps=rewards["SupplyPort1"].index.to_numpy(), unit="second", description=( "The reward delivery time of the right lick port. The data field " diff --git a/src/dynamic_foraging_processing/utils/__init__.py b/src/dynamic_foraging_processing/utils/__init__.py new file mode 100644 index 0000000..1c77e9b --- /dev/null +++ b/src/dynamic_foraging_processing/utils/__init__.py @@ -0,0 +1,6 @@ +"""Utility helpers for dynamic foraging processing.""" + +from dynamic_foraging_processing.utils.rewards import get_annotated_rewards +from dynamic_foraging_processing.utils.timestamps import find_closest_timestamps + +__all__ = ["find_closest_timestamps", "get_annotated_rewards"] diff --git a/src/dynamic_foraging_processing/utils/rewards.py b/src/dynamic_foraging_processing/utils/rewards.py new file mode 100644 index 0000000..109b7ee --- /dev/null +++ b/src/dynamic_foraging_processing/utils/rewards.py @@ -0,0 +1,53 @@ +"""Reward-related processing helpers for dynamic foraging data.""" + +import numpy as np +import pandas as pd +from aind_behavior_dynamic_foraging.task_logic.trial_models import Trial + +from dynamic_foraging_processing.utils.timestamps import find_closest_timestamps + + +def get_annotated_rewards( + reward_delivery_times: np.ndarray, + trial_outcome_df: pd.DataFrame, + sample_global_auto_water_state: pd.DataFrame, +) -> np.ndarray: + """Annotate each reward delivery with information from the matching trial. + + Parameters + ---------- + reward_delivery_times : numpy.ndarray + Hardware timestamps of reward deliveries. + trial_outcome_df : pandas.DataFrame + Trial outcome table indexed by trial timestamp. + sample_global_auto_water_state : pandas.DataFrame + Auto-water state event table indexed by event timestamp. + + Returns + ------- + numpy.ndarray + Array of the same shape as ``reward_delivery_times`` containing the + annotation for each reward delivery. + """ + reward_times = np.asarray(reward_delivery_times) + trial_indices = find_closest_timestamps( + reward_times, + trial_outcome_df.index.to_numpy(), + ) + auto_water_indices = find_closest_timestamps( + reward_times, + sample_global_auto_water_state.index.to_numpy(), + ) + + annotated_rewards = [] + for i, trial_index in enumerate(trial_indices): + # TODO: use auto_water_indices[i] together with trial_outcome_df to + # determine manual and automatic rewards. + _ = auto_water_indices[i] + trial_data_at_index = trial_outcome_df.iloc[trial_index]["data"] + trial_model = Trial(**trial_data_at_index) + + if trial_model.is_auto_response_right is None: + annotated_rewards.append("earned") + + return np.array(annotated_rewards) diff --git a/src/dynamic_foraging_processing/utils/timestamps.py b/src/dynamic_foraging_processing/utils/timestamps.py new file mode 100644 index 0000000..54ded43 --- /dev/null +++ b/src/dynamic_foraging_processing/utils/timestamps.py @@ -0,0 +1,66 @@ +"""Timestamp matching utilities.""" + +import numpy as np + + +def find_closest_timestamps( + timestamps: np.ndarray, + reference_timestamps: np.ndarray, +) -> np.ndarray: + """Find the closest ``reference_timestamps`` for each entry in ``timestamps``. + + Parameters + ---------- + timestamps : numpy.ndarray + Query timestamps to match against ``reference_timestamps``. + reference_timestamps : numpy.ndarray + Candidate timestamps to search within. + + Returns + ------- + numpy.ndarray + Integer positions into ``reference_timestamps`` giving, for each query + timestamp, the index of the nearest candidate timestamp. + + Examples + -------- + Match query timestamps to the nearest reference timestamp. Note that + references are not required to be sorted, multiple queries can map to the + same reference, and ties resolve to the earlier reference: + + >>> import numpy as np + >>> from dynamic_foraging_processing.utils import find_closest_timestamps + >>> query_times = np.array([0.42, 0.05, 1.55, 0.95, 0.85]) + >>> reference_times = np.array([0.9, 0.1, 2.0, 0.4]) + >>> positions = find_closest_timestamps(query_times, reference_times) + >>> positions + array([3, 1, 2, 0, 0]) + >>> reference_times[positions] + array([0.4, 0.1, 2. , 0.9, 0.9]) + + Use the returned positions to align rows of a reference-indexed DataFrame: + + >>> import pandas as pd + >>> trial_outcome_df = pd.DataFrame( + ... {"outcome": ["earned", "manual", "automatic", "earned"]}, + ... index=reference_times, + ... ) + >>> trial_outcome_df.iloc[positions]["outcome"].tolist() + ['earned', 'manual', 'automatic', 'earned', 'earned'] + """ + timestamps = np.asarray(timestamps) + reference_timestamps = np.asarray(reference_timestamps) + + if reference_timestamps.size == 0: + raise ValueError("reference_timestamps must not be empty") + + sort_order = np.argsort(reference_timestamps) + sorted_refs = reference_timestamps[sort_order] + + right_idx = np.searchsorted(sorted_refs, timestamps) + right_idx = np.clip(right_idx, 1, len(sorted_refs) - 1) + left_idx = right_idx - 1 + + pick_left = (timestamps - sorted_refs[left_idx]) <= (sorted_refs[right_idx] - timestamps) + closest_sorted = np.where(pick_left, left_idx, right_idx) + return sort_order[closest_sorted] From f8ffdbce2774e18d740b855298442c771bcb37a7 Mon Sep 17 00:00:00 2001 From: arjunsridhar12345 Date: Thu, 4 Jun 2026 15:03:18 -0700 Subject: [PATCH 15/36] test: add tests --- tests/test_utils/__init__.py | 1 + tests/test_utils/test_rewards.py | 74 +++++++++++++++++++++++++++++ tests/test_utils/test_timestamps.py | 40 ++++++++++++++++ 3 files changed, 115 insertions(+) create mode 100644 tests/test_utils/__init__.py create mode 100644 tests/test_utils/test_rewards.py create mode 100644 tests/test_utils/test_timestamps.py diff --git a/tests/test_utils/__init__.py b/tests/test_utils/__init__.py new file mode 100644 index 0000000..630c6fe --- /dev/null +++ b/tests/test_utils/__init__.py @@ -0,0 +1 @@ +"""Tests for ``dynamic_foraging_processing.utils``.""" diff --git a/tests/test_utils/test_rewards.py b/tests/test_utils/test_rewards.py new file mode 100644 index 0000000..89689e3 --- /dev/null +++ b/tests/test_utils/test_rewards.py @@ -0,0 +1,74 @@ +"""Tests for ``dynamic_foraging_processing.utils.rewards``.""" + +import numpy as np +import pandas as pd + +from dynamic_foraging_processing.utils.rewards import get_annotated_rewards + + +def _trial_payload() -> dict: + """Return a minimal ``Trial`` payload using model defaults.""" + return { + "p_reward_left": 1.0, + "p_reward_right": 1.0, + "reward_consumption_duration": 5.0, + "reward_delay_duration": 0.0, + "secondary_reinforcer": None, + "response_deadline_duration": 5.0, + "enable_fast_retract": False, + "quiescence_period_duration": 0.5, + "inter_trial_interval_duration": 5.0, + "is_auto_response_right": None, + "lickspout_offset_delta": 0.0, + "extra_metadata": None, + } + + +def _trial_outcome_df(trial_times: np.ndarray) -> pd.DataFrame: + """Build a trial outcome DataFrame indexed by ``trial_times``.""" + return pd.DataFrame( + {"data": [_trial_payload() for _ in trial_times]}, + index=pd.Index(trial_times, name="time"), + ) + + +def _auto_water_state_df(state_times: np.ndarray) -> pd.DataFrame: + """Build an auto-water state DataFrame indexed by ``state_times``.""" + return pd.DataFrame( + {"state": [True for _ in state_times]}, + index=pd.Index(state_times, name="time"), + ) + + +def test_get_annotated_rewards_marks_default_trials_as_earned(): + """Trials with no auto-response setting are annotated as ``earned``.""" + reward_times = np.array([0.15, 0.42, 0.95]) + trial_outcome_df = _trial_outcome_df(np.array([0.1, 0.4, 0.9])) + auto_water_state_df = _auto_water_state_df(np.array([0.0, 1.0])) + + annotations = get_annotated_rewards(reward_times, trial_outcome_df, auto_water_state_df) + + np.testing.assert_array_equal(annotations, np.array(["earned", "earned", "earned"])) + + +def test_get_annotated_rewards_skips_trials_with_auto_response_set(): + """Trials with ``is_auto_response_right`` set are not annotated as ``earned``.""" + reward_times = np.array([0.15, 0.42]) + trial_outcome_df = _trial_outcome_df(np.array([0.1, 0.4])) + trial_outcome_df.iloc[0]["data"]["is_auto_response_right"] = True + auto_water_state_df = _auto_water_state_df(np.array([0.0, 1.0])) + + annotations = get_annotated_rewards(reward_times, trial_outcome_df, auto_water_state_df) + + np.testing.assert_array_equal(annotations, np.array(["earned"])) + + +def test_get_annotated_rewards_returns_ndarray(): + """The return value is a ``numpy.ndarray``.""" + reward_times = np.array([0.1]) + trial_outcome_df = _trial_outcome_df(np.array([0.0])) + auto_water_state_df = _auto_water_state_df(np.array([0.0])) + + result = get_annotated_rewards(reward_times, trial_outcome_df, auto_water_state_df) + + assert isinstance(result, np.ndarray) diff --git a/tests/test_utils/test_timestamps.py b/tests/test_utils/test_timestamps.py new file mode 100644 index 0000000..77b3ab9 --- /dev/null +++ b/tests/test_utils/test_timestamps.py @@ -0,0 +1,40 @@ +"""Tests for ``dynamic_foraging_processing.utils.timestamps``.""" + +import numpy as np +import pytest + +from dynamic_foraging_processing.utils.timestamps import find_closest_timestamps + + +def test_find_closest_timestamps_returns_nearest_position(): + """Each query maps to the nearest reference position.""" + query = np.array([0.42, 0.05, 1.55, 0.95, 0.85]) + reference = np.array([0.9, 0.1, 2.0, 0.4]) + + positions = find_closest_timestamps(query, reference) + + np.testing.assert_array_equal(positions, np.array([3, 1, 2, 0, 0])) + np.testing.assert_array_equal(reference[positions], np.array([0.4, 0.1, 2.0, 0.9, 0.9])) + + +def test_find_closest_timestamps_ties_resolve_to_earlier_reference(): + """When a query is equidistant between two references, the earlier one wins.""" + query = np.array([0.5]) + reference = np.array([0.0, 1.0]) + + positions = find_closest_timestamps(query, reference) + + np.testing.assert_array_equal(positions, np.array([0])) + + +def test_find_closest_timestamps_accepts_lists(): + """Inputs are coerced via ``np.asarray``.""" + positions = find_closest_timestamps([0.1, 0.9], [0.0, 1.0]) + + np.testing.assert_array_equal(positions, np.array([0, 1])) + + +def test_find_closest_timestamps_empty_reference_raises(): + """An empty reference array raises ``ValueError``.""" + with pytest.raises(ValueError, match="reference_timestamps must not be empty"): + find_closest_timestamps(np.array([0.0]), np.array([])) From 7e1f34624572927fe00239ee4b8c67651b3711ae Mon Sep 17 00:00:00 2001 From: arjunsridhar12345 Date: Tue, 23 Jun 2026 18:08:21 -0700 Subject: [PATCH 16/36] feat: try to add manual rewards --- .../nwb/acquisition/acquisition_builder.py | 132 +++++++++++++++--- .../utils/rewards.py | 91 +++++++++--- 2 files changed, 182 insertions(+), 41 deletions(-) diff --git a/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py b/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py index 2c4c9b5..daa73bf 100644 --- a/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py +++ b/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py @@ -9,6 +9,7 @@ AcquisitionTable, ) from dynamic_foraging_processing.raw_data_loader import RawDataLoader +from dynamic_foraging_processing.utils.rewards import get_annotated_rewards class AcquisitionBuilder: @@ -30,7 +31,7 @@ def get_reward_delivery(self) -> pd.DataFrame: Returns ------- pandas.DataFrame - DataFrame from the loaded ``OutputSet`` stream under + ``WRITE`` messages from the loaded ``OutputSet`` stream under ``Behavior/HarpBehavior``. """ data = self.loader.dataset.at("Behavior").at("HarpBehavior").at("OutputSet").load().data @@ -38,6 +39,102 @@ def get_reward_delivery(self) -> pd.DataFrame: return data_write_messages + def get_trial_outcomes(self) -> pd.DataFrame: + """Get the ``TrialOutcome`` software-event stream. + + Returns + ------- + pandas.DataFrame + The ``TrialOutcome`` stream under ``Behavior/SoftwareEvents``, + indexed by trial timestamp with a ``data`` payload column. + """ + return ( + self.loader.dataset.at("Behavior").at("SoftwareEvents").at("TrialOutcome").load().data + ) + + def get_manual_water_times(self) -> pd.DataFrame: + """Get the manual-water software-event stream. + + Returns + ------- + pandas.DataFrame + The ``GiveManualWaterRight`` stream under ``Behavior/SoftwareEvents``, + indexed by event timestamp with a ``data`` column that is ``True`` + for right-port manual water and ``False`` for left-port manual water. + An empty frame (with a ``data`` column) is returned when the stream + is absent. + """ + try: + return ( + self.loader.dataset.at("Behavior") + .at("SoftwareEvents") + .at("GiveManualWaterRight") + .load() + .data + ) + except (KeyError, FileNotFoundError): + return pd.DataFrame({"data": []}) + + def _reward_delivery_series( + self, + writes: pd.DataFrame, + trial_outcomes: pd.DataFrame, + manual_water: pd.DataFrame, + *, + port_column: str, + is_right: bool, + name: str, + side_label: str, + ) -> AcquisitionSeries: + """Build one lick port's reward-delivery series with reward annotations. + + Only valve-open events (``port_column`` is truthy) are reward + deliveries; the ``data`` field annotates each as earned, manual, or + automatic via :func:`get_annotated_rewards`. + + Parameters + ---------- + writes : pandas.DataFrame + ``OutputSet`` ``WRITE`` messages indexed by timestamp. + trial_outcomes : pandas.DataFrame + The ``TrialOutcome`` stream, indexed by trial timestamp. + manual_water : pandas.DataFrame + The ``GiveManualWaterRight`` stream; the ``data`` column selects the + side (``True`` right, ``False`` left). + port_column : str + Supply-port column for this side (``"SupplyPort0"`` left, + ``"SupplyPort1"`` right). + is_right : bool + ``True`` for the right lick port, ``False`` for the left. + name : str + Acquisition series name. + side_label : str + Human-readable side label used in the description. + + Returns + ------- + AcquisitionSeries + The reward-delivery series for this lick port. + """ + open_writes = writes[writes[port_column].fillna(False).astype(bool)] + delivery_times = open_writes.index.to_numpy() + manual_water_times = manual_water.index[manual_water["data"] == is_right].to_numpy() + annotations = get_annotated_rewards( + delivery_times, + trial_outcomes, + manual_water_times, + ) + return AcquisitionSeries( + name=name, + data=annotations, + timestamps=delivery_times, + unit="second", + description=( + f"The reward delivery time of the {side_label} lick port. The data field " + "annotates whether the reward was earned, manual, or automatic" + ), + ) + def build_acquisition(self) -> t.List[t.Union[AcquisitionSeries, AcquisitionTable]]: """Build the NWB acquisition entries. @@ -47,6 +144,8 @@ def build_acquisition(self) -> t.List[t.Union[AcquisitionSeries, AcquisitionTabl Acquisition entries to write to the NWB acquisition module. """ rewards = self.get_reward_delivery() + trial_outcomes = self.get_trial_outcomes() + manual_water = self.get_manual_water_times() acquisition_streams = self.loader.get_all_raw_data() acqusition_streams_descriptions = self.loader.raw_data_stream_descriptions @@ -63,30 +162,27 @@ def build_acquisition(self) -> t.List[t.Union[AcquisitionSeries, AcquisitionTabl ) ) - # TODO: fix data so that it is array of annotations of whether the reward was earned, manual, or automatic # TODO: add left and right lick times acquisiton_entries.append( - AcquisitionSeries( + self._reward_delivery_series( + rewards, + trial_outcomes, + manual_water, + port_column="SupplyPort0", + is_right=False, name="left_reward_delivery_time", - data=rewards["SupplyPort0"].to_numpy(), - timestamps=rewards["SupplyPort0"].index.to_numpy(), - unit="second", - description=( - "The reward delivery time of the left lick port. The data field " - "annotates whether the reward was earned, manual, or automatic" - ), + side_label="left", ) ) acquisiton_entries.append( - AcquisitionSeries( + self._reward_delivery_series( + rewards, + trial_outcomes, + manual_water, + port_column="SupplyPort1", + is_right=True, name="right_reward_delivery_time", - data=rewards["SupplyPort1"].to_numpy(), - timestamps=rewards["SupplyPort1"].index.to_numpy(), - unit="second", - description=( - "The reward delivery time of the right lick port. The data field " - "annotates whether the reward was earned, manual, or automatic" - ), + side_label="right", ) ) diff --git a/src/dynamic_foraging_processing/utils/rewards.py b/src/dynamic_foraging_processing/utils/rewards.py index 109b7ee..a92a3cd 100644 --- a/src/dynamic_foraging_processing/utils/rewards.py +++ b/src/dynamic_foraging_processing/utils/rewards.py @@ -1,53 +1,98 @@ """Reward-related processing helpers for dynamic foraging data.""" +import typing as t + import numpy as np import pandas as pd -from aind_behavior_dynamic_foraging.task_logic.trial_models import Trial +from aind_behavior_dynamic_foraging.task_logic.trial_models import TrialOutcome from dynamic_foraging_processing.utils.timestamps import find_closest_timestamps +def _parse_outcome(payload: t.Any) -> t.Optional[TrialOutcome]: + """Parse a ``TrialOutcome`` software-event payload into its domain model. + + Parameters + ---------- + payload : Any + The stream's ``data`` value: a serialized JSON string, a dict, an + already-parsed ``TrialOutcome``, or ``None``. + + Returns + ------- + TrialOutcome or None + The parsed outcome, or ``None`` when ``payload`` is ``None``. + """ + if payload is None or isinstance(payload, TrialOutcome): + return payload + if isinstance(payload, (str, bytes, bytearray)): + return TrialOutcome.model_validate_json(payload) + return TrialOutcome.model_validate(payload) + + def get_annotated_rewards( reward_delivery_times: np.ndarray, trial_outcome_df: pd.DataFrame, - sample_global_auto_water_state: pd.DataFrame, + manual_water_times: np.ndarray, ) -> np.ndarray: - """Annotate each reward delivery with information from the matching trial. + """Annotate each reward delivery as ``earned``, ``automatic``, or ``manual``. + + Annotates the deliveries of a single lick port. Each delivery is classified + as follows, with ``manual`` taking precedence because manual water is not + aligned to a go cue: + + - ``manual`` -- the delivery is the closest hardware (harp) timestamp to a + ``GiveManualWater`` software event for this port. The software-event + timestamps are correlated to the reward-delivery timestamps with + :func:`find_closest_timestamps`. + - ``automatic`` -- otherwise, when the matching trial auto-responded to this + same port (``is_auto_response_right is is_right``: ``True`` for the right + port, ``False`` for the left port). + - ``earned`` -- otherwise (no auto-response, or auto-response to the other + port). Parameters ---------- reward_delivery_times : numpy.ndarray - Hardware timestamps of reward deliveries. + Hardware (harp) timestamps of this port's reward deliveries. trial_outcome_df : pandas.DataFrame - Trial outcome table indexed by trial timestamp. - sample_global_auto_water_state : pandas.DataFrame - Auto-water state event table indexed by event timestamp. + Trial outcome table indexed by trial timestamp; each row's ``data`` + field is a :class:`TrialOutcome` payload. + manual_water_times : numpy.ndarray + Software-event timestamps of this port's manual water deliveries + (``GiveManualWaterLeft`` / ``GiveManualWaterRight``). Returns ------- numpy.ndarray - Array of the same shape as ``reward_delivery_times`` containing the - annotation for each reward delivery. + Array of the same shape as ``reward_delivery_times`` whose entries are + ``"earned"``, ``"automatic"``, or ``"manual"``. """ reward_times = np.asarray(reward_delivery_times) - trial_indices = find_closest_timestamps( - reward_times, - trial_outcome_df.index.to_numpy(), - ) - auto_water_indices = find_closest_timestamps( - reward_times, - sample_global_auto_water_state.index.to_numpy(), - ) + if reward_times.size == 0: + return np.array([], dtype=object) + + # Correlate each manual-water software event to its closest reward delivery; + # those deliveries are the manual ones. + manual_water_times = np.asarray(manual_water_times) + if manual_water_times.size: + manual_indices = set(find_closest_timestamps(manual_water_times, reward_times).tolist()) + else: + manual_indices = set() + + trial_indices = find_closest_timestamps(reward_times, trial_outcome_df.index.to_numpy()) annotated_rewards = [] for i, trial_index in enumerate(trial_indices): - # TODO: use auto_water_indices[i] together with trial_outcome_df to - # determine manual and automatic rewards. - _ = auto_water_indices[i] - trial_data_at_index = trial_outcome_df.iloc[trial_index]["data"] - trial_model = Trial(**trial_data_at_index) + if i in manual_indices: + annotated_rewards.append("manual") + continue - if trial_model.is_auto_response_right is None: + outcome = _parse_outcome(trial_outcome_df.iloc[trial_index]["data"]) + trial = outcome.trial if outcome is not None else None + if trial is None or trial.is_auto_response_right is None: annotated_rewards.append("earned") + else: + annotated_rewards.append("automatic") return np.array(annotated_rewards) From bceaeb8add0754458453579ed4dd3d00d305c7a1 Mon Sep 17 00:00:00 2001 From: arjunsridhar12345 Date: Tue, 23 Jun 2026 18:08:32 -0700 Subject: [PATCH 17/36] test: update tests --- .../test_acquisition_builder.py | 171 ++++++++++++++---- tests/test_utils/test_rewards.py | 110 +++++++---- 2 files changed, 213 insertions(+), 68 deletions(-) diff --git a/tests/test_nwb/test_acquisition/test_acquisition_builder.py b/tests/test_nwb/test_acquisition/test_acquisition_builder.py index 07374ee..15f37c5 100644 --- a/tests/test_nwb/test_acquisition/test_acquisition_builder.py +++ b/tests/test_nwb/test_acquisition/test_acquisition_builder.py @@ -12,24 +12,106 @@ ) +class _FakeStream: + """Leaf stream exposing ``load()``/``data`` like the contract.""" + + def __init__(self, data): + """Store the stream's data.""" + self._data = data + + def load(self): + """Return self (data is already in memory).""" + return self + + @property + def data(self): + """Return the stored data.""" + return self._data + + +class _FakeNode: + """Inner dataset node whose ``at`` navigates to named children.""" + + def __init__(self, children): + """Store the child nodes by name.""" + self._children = children + + def at(self, name): + """Return the named child, raising ``KeyError`` if absent.""" + return self._children[name] + + def _make_output_set_frame() -> pd.DataFrame: - """Build an OutputSet-like DataFrame with WRITE and non-WRITE rows.""" + """Build an OutputSet-like DataFrame with WRITE and non-WRITE rows. + + Left valve opens once (SupplyPort0 high at 0.1); the right valve opens twice + (SupplyPort1 high at 0.3 and 0.5). + """ return pd.DataFrame( { - "MessageType": ["WRITE", "READ", "WRITE"], - "SupplyPort0": [1, 0, 0], - "SupplyPort1": [0, 0, 1], + "MessageType": ["WRITE", "READ", "WRITE", "WRITE"], + "SupplyPort0": [1, 0, 0, 0], + "SupplyPort1": [0, 0, 1, 1], }, - index=pd.Index([0.1, 0.2, 0.3], name="time"), + index=pd.Index([0.1, 0.2, 0.3, 0.5], name="time"), ) -def _make_loader(frame: pd.DataFrame) -> MagicMock: - """Build a mock loader whose ``dataset.at`` chain resolves to ``frame``.""" +def _outcome_payload(auto) -> dict: + """Return a serialized ``TrialOutcome`` payload with the given auto-response.""" + return { + "trial": { + "p_reward_left": 1.0, + "p_reward_right": 1.0, + "response_deadline_duration": 3.0, + "reward_consumption_duration": 1.0, + "quiescence_period_duration": 0.5, + "inter_trial_interval_duration": 4.0, + "is_auto_response_right": auto, + }, + "is_right_choice": True, + "is_rewarded": True, + } + + +def _make_trial_outcome_frame() -> pd.DataFrame: + """Two trials: an earned trial at 0.1 and an auto-response trial at 0.4.""" + return pd.DataFrame( + {"data": [_outcome_payload(None), _outcome_payload(True)]}, + index=pd.Index([0.1, 0.4], name="time"), + ) + + +def _empty_manual_water_frame() -> pd.DataFrame: + """Build an empty manual-water stream with the ``data`` side column.""" + return pd.DataFrame({"data": []}, index=pd.Index([], name="time")) + + +def _make_dataset(manual_water=None): + """Build a path-aware fake dataset rooted at ``Behavior``.""" + if manual_water is None: + manual_water = _empty_manual_water_frame() + return _FakeNode( + { + "Behavior": _FakeNode( + { + "HarpBehavior": _FakeNode({"OutputSet": _FakeStream(_make_output_set_frame())}), + "SoftwareEvents": _FakeNode( + { + "TrialOutcome": _FakeStream(_make_trial_outcome_frame()), + "GiveManualWaterRight": _FakeStream(manual_water), + } + ), + } + ) + } + ) + + +def _make_loader(dataset=None) -> MagicMock: + """Build a mock loader backed by a path-aware fake dataset.""" loader = MagicMock() - stream = MagicMock() - stream.data = frame - loader.dataset.at.return_value.at.return_value.at.return_value.load.return_value = stream + loader.dataset = dataset if dataset is not None else _make_dataset() loader.get_all_raw_data.return_value = { "Behavior.RawStream": pd.DataFrame({"x": [1, 2, 3]}), } @@ -39,32 +121,58 @@ def _make_loader(frame: pd.DataFrame) -> MagicMock: def test_init_stores_loader(): """The provided loader is stored on the instance.""" - loader = _make_loader(_make_output_set_frame()) + loader = _make_loader() builder = AcquisitionBuilder(loader=loader) assert builder.loader is loader def test_get_reward_delivery_filters_to_write_messages(): """Only ``MessageType == 'WRITE'`` rows are returned.""" - frame = _make_output_set_frame() - loader = _make_loader(frame) - builder = AcquisitionBuilder(loader=loader) + builder = AcquisitionBuilder(loader=_make_loader()) result = builder.get_reward_delivery() - assert list(result["MessageType"]) == ["WRITE", "WRITE"] - assert list(result.index) == [0.1, 0.3] - ds = loader.dataset - ds.at.assert_called_once_with("Behavior") - ds.at.return_value.at.assert_called_once_with("HarpBehavior") - ds.at.return_value.at.return_value.at.assert_called_once_with("OutputSet") + assert list(result["MessageType"]) == ["WRITE", "WRITE", "WRITE"] + assert list(result.index) == [0.1, 0.3, 0.5] + + +def test_get_manual_water_times_returns_stream(): + """``get_manual_water_times`` returns the GiveManualWaterRight stream.""" + manual = pd.DataFrame({"data": [True]}, index=pd.Index([0.49], name="time")) + builder = AcquisitionBuilder(loader=_make_loader(_make_dataset(manual))) + + result = builder.get_manual_water_times() + + pd.testing.assert_frame_equal(result, manual) + + +def test_get_manual_water_times_returns_empty_when_absent(): + """A missing manual-water stream yields an empty frame with a ``data`` column.""" + dataset = _FakeNode( + { + "Behavior": _FakeNode( + { + "HarpBehavior": _FakeNode({"OutputSet": _FakeStream(_make_output_set_frame())}), + "SoftwareEvents": _FakeNode( + {"TrialOutcome": _FakeStream(_make_trial_outcome_frame())} + ), + } + ) + } + ) + builder = AcquisitionBuilder(loader=_make_loader(dataset)) + + result = builder.get_manual_water_times() + + assert list(result.columns) == ["data"] + assert result.empty def test_build_acquisition_returns_populated_list(): """``build_acquisition`` returns table entries plus both reward port series.""" - frame = _make_output_set_frame() - loader = _make_loader(frame) - builder = AcquisitionBuilder(loader=loader) + # A right-side manual-water event (data=True) near the second right delivery. + manual = pd.DataFrame({"data": [True]}, index=pd.Index([0.49], name="time")) + builder = AcquisitionBuilder(loader=_make_loader(_make_dataset(manual))) acquisition = builder.build_acquisition() @@ -75,20 +183,19 @@ def test_build_acquisition_returns_populated_list(): assert isinstance(table, AcquisitionTable) assert table.name == "Behavior.RawStream" assert table.description == "raw stream desc" - assert list(table.data.columns) == ["x"] + # Left: one valve-open delivery, earned (no auto-response, no left manual). assert isinstance(left, AcquisitionSeries) - assert isinstance(right, AcquisitionSeries) - - expected_timestamps = np.array([0.1, 0.3]) - np.testing.assert_array_equal(left.data, np.array([1, 0])) - np.testing.assert_array_equal(left.timestamps, expected_timestamps) + np.testing.assert_array_equal(left.timestamps, np.array([0.1])) + np.testing.assert_array_equal(left.data, np.array(["earned"])) assert left.name == "left_reward_delivery_time" assert left.unit == "second" assert "left lick port" in left.description - np.testing.assert_array_equal(right.data, np.array([0, 1])) - np.testing.assert_array_equal(right.timestamps, expected_timestamps) + # Right: two deliveries. Both nearest the auto-response trial (0.4); the + # second is overridden to manual by the right-side manual-water event. + assert isinstance(right, AcquisitionSeries) + np.testing.assert_array_equal(right.timestamps, np.array([0.3, 0.5])) + np.testing.assert_array_equal(right.data, np.array(["automatic", "manual"])) assert right.name == "right_reward_delivery_time" - assert right.unit == "second" assert "right lick port" in right.description diff --git a/tests/test_utils/test_rewards.py b/tests/test_utils/test_rewards.py index 89689e3..0876946 100644 --- a/tests/test_utils/test_rewards.py +++ b/tests/test_utils/test_rewards.py @@ -1,74 +1,112 @@ """Tests for ``dynamic_foraging_processing.utils.rewards``.""" +import json + import numpy as np import pandas as pd +from aind_behavior_dynamic_foraging.task_logic.trial_models import TrialOutcome from dynamic_foraging_processing.utils.rewards import get_annotated_rewards -def _trial_payload() -> dict: - """Return a minimal ``Trial`` payload using model defaults.""" +def _outcome_payload(auto=None) -> dict: + """Return a serialized ``TrialOutcome`` payload with the given auto-response.""" return { - "p_reward_left": 1.0, - "p_reward_right": 1.0, - "reward_consumption_duration": 5.0, - "reward_delay_duration": 0.0, - "secondary_reinforcer": None, - "response_deadline_duration": 5.0, - "enable_fast_retract": False, - "quiescence_period_duration": 0.5, - "inter_trial_interval_duration": 5.0, - "is_auto_response_right": None, - "lickspout_offset_delta": 0.0, - "extra_metadata": None, + "trial": { + "p_reward_left": 1.0, + "p_reward_right": 1.0, + "response_deadline_duration": 3.0, + "reward_consumption_duration": 1.0, + "quiescence_period_duration": 0.5, + "inter_trial_interval_duration": 4.0, + "is_auto_response_right": auto, + }, + "is_right_choice": True, + "is_rewarded": True, } -def _trial_outcome_df(trial_times: np.ndarray) -> pd.DataFrame: +def _trial_outcome_df(trial_times: np.ndarray, autos=None) -> pd.DataFrame: """Build a trial outcome DataFrame indexed by ``trial_times``.""" + autos = autos if autos is not None else [None] * len(trial_times) return pd.DataFrame( - {"data": [_trial_payload() for _ in trial_times]}, + {"data": [_outcome_payload(auto) for auto in autos]}, index=pd.Index(trial_times, name="time"), ) -def _auto_water_state_df(state_times: np.ndarray) -> pd.DataFrame: - """Build an auto-water state DataFrame indexed by ``state_times``.""" - return pd.DataFrame( - {"state": [True for _ in state_times]}, - index=pd.Index(state_times, name="time"), - ) - - def test_get_annotated_rewards_marks_default_trials_as_earned(): - """Trials with no auto-response setting are annotated as ``earned``.""" + """Trials with no auto-response setting and no manual water are ``earned``.""" reward_times = np.array([0.15, 0.42, 0.95]) trial_outcome_df = _trial_outcome_df(np.array([0.1, 0.4, 0.9])) - auto_water_state_df = _auto_water_state_df(np.array([0.0, 1.0])) - annotations = get_annotated_rewards(reward_times, trial_outcome_df, auto_water_state_df) + annotations = get_annotated_rewards(reward_times, trial_outcome_df, np.array([])) np.testing.assert_array_equal(annotations, np.array(["earned", "earned", "earned"])) -def test_get_annotated_rewards_skips_trials_with_auto_response_set(): - """Trials with ``is_auto_response_right`` set are not annotated as ``earned``.""" +def test_get_annotated_rewards_marks_auto_response_trials_as_automatic(): + """Trials with ``is_auto_response_right`` set (either side) are ``automatic``.""" + reward_times = np.array([0.15, 0.42]) + trial_outcome_df = _trial_outcome_df(np.array([0.1, 0.4]), autos=[True, False]) + + annotations = get_annotated_rewards(reward_times, trial_outcome_df, np.array([])) + + np.testing.assert_array_equal(annotations, np.array(["automatic", "automatic"])) + + +def test_get_annotated_rewards_marks_manual_water_as_manual(): + """Deliveries closest to a manual-water event are annotated as ``manual``.""" + reward_times = np.array([0.15, 0.42, 0.95]) + trial_outcome_df = _trial_outcome_df(np.array([0.1, 0.4, 0.9])) + # Software event near the second delivery (0.42). + manual_water_times = np.array([0.43]) + + annotations = get_annotated_rewards(reward_times, trial_outcome_df, manual_water_times) + + np.testing.assert_array_equal(annotations, np.array(["earned", "manual", "earned"])) + + +def test_get_annotated_rewards_manual_takes_precedence_over_automatic(): + """A manual delivery is ``manual`` even when the trial has auto-response set.""" + reward_times = np.array([0.15, 0.42]) + trial_outcome_df = _trial_outcome_df(np.array([0.1, 0.4]), autos=[None, True]) + manual_water_times = np.array([0.42]) + + annotations = get_annotated_rewards(reward_times, trial_outcome_df, manual_water_times) + + np.testing.assert_array_equal(annotations, np.array(["earned", "manual"])) + + +def test_get_annotated_rewards_empty_deliveries_returns_empty(): + """No reward deliveries yields an empty annotation array.""" + trial_outcome_df = _trial_outcome_df(np.array([0.0])) + + result = get_annotated_rewards(np.array([]), trial_outcome_df, np.array([])) + + assert isinstance(result, np.ndarray) + assert result.size == 0 + + +def test_get_annotated_rewards_accepts_json_and_model_payloads(): + """``data`` payloads may be JSON strings or already-parsed ``TrialOutcome``.""" reward_times = np.array([0.15, 0.42]) - trial_outcome_df = _trial_outcome_df(np.array([0.1, 0.4])) - trial_outcome_df.iloc[0]["data"]["is_auto_response_right"] = True - auto_water_state_df = _auto_water_state_df(np.array([0.0, 1.0])) + payload = _outcome_payload(True) + trial_outcome_df = pd.DataFrame( + {"data": [json.dumps(payload), TrialOutcome.model_validate(payload)]}, + index=pd.Index([0.1, 0.4], name="time"), + ) - annotations = get_annotated_rewards(reward_times, trial_outcome_df, auto_water_state_df) + annotations = get_annotated_rewards(reward_times, trial_outcome_df, np.array([])) - np.testing.assert_array_equal(annotations, np.array(["earned"])) + np.testing.assert_array_equal(annotations, np.array(["automatic", "automatic"])) def test_get_annotated_rewards_returns_ndarray(): """The return value is a ``numpy.ndarray``.""" reward_times = np.array([0.1]) trial_outcome_df = _trial_outcome_df(np.array([0.0])) - auto_water_state_df = _auto_water_state_df(np.array([0.0])) - result = get_annotated_rewards(reward_times, trial_outcome_df, auto_water_state_df) + result = get_annotated_rewards(reward_times, trial_outcome_df, np.array([])) assert isinstance(result, np.ndarray) From cfb73caa4b83ae1ea637284cbf8fec0da9f8fbc1 Mon Sep 17 00:00:00 2001 From: arjunsridhar12345 Date: Tue, 23 Jun 2026 18:16:58 -0700 Subject: [PATCH 18/36] feat: add lick times to acquisition --- .../nwb/acquisition/acquisition_builder.py | 81 ++++++++++++++- .../test_acquisition_builder.py | 98 +++++++++++++++---- 2 files changed, 159 insertions(+), 20 deletions(-) diff --git a/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py b/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py index daa73bf..b0d7ec6 100644 --- a/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py +++ b/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py @@ -2,6 +2,7 @@ import typing as t +import numpy as np import pandas as pd from dynamic_foraging_processing.nwb.acquisition.models import ( @@ -75,6 +76,71 @@ def get_manual_water_times(self) -> pd.DataFrame: except (KeyError, FileNotFoundError): return pd.DataFrame({"data": []}) + def get_lick_times(self, is_right: bool) -> np.ndarray: + """Get the lick times for one lick port from the behavior board DI ports. + + Licks are read from the HarpBehavior ``DigitalInputState`` stream. Left + licks are on ``DIPort0`` and right licks on ``DIPort1``; a lick time is a + timestamp at which that port's digital input is high. + + Parameters + ---------- + is_right : bool + ``True`` for the right lick port (``DIPort1``), ``False`` for the + left lick port (``DIPort0``). + + Returns + ------- + numpy.ndarray + Sorted lick timestamps for the requested port, or an empty array + when the stream is absent. + """ + port_column = "DIPort1" if is_right else "DIPort0" + try: + data = ( + self.loader.dataset.at("Behavior") + .at("HarpBehavior") + .at("DigitalInputState") + .load() + .data + ) + except (KeyError, FileNotFoundError): + return np.array([]) + licks = data[data[port_column].fillna(False).astype(bool)] + return licks.index.to_numpy() + + def _lick_time_series(self, *, is_right: bool, name: str, side_label: str) -> AcquisitionSeries: + """Build one lick port's lick-time series from the behavior board DI ports. + + Parameters + ---------- + is_right : bool + ``True`` for the right lick port (``DIPort1``), ``False`` for the + left lick port (``DIPort0``). + name : str + Acquisition series name. + side_label : str + Human-readable side label used in the description. + + Returns + ------- + AcquisitionSeries + The lick-time series for this lick port. The ``data`` array marks + each timestamp as a detected lick (``True``). + """ + lick_times = self.get_lick_times(is_right) + port_column = "DIPort1" if is_right else "DIPort0" + return AcquisitionSeries( + name=name, + data=np.ones(lick_times.shape[0], dtype=bool), + timestamps=lick_times, + unit="second", + description=( + f"The lick times of the {side_label} lick port ({port_column} on the " + "behavior board)." + ), + ) + def _reward_delivery_series( self, writes: pd.DataFrame, @@ -162,7 +228,6 @@ def build_acquisition(self) -> t.List[t.Union[AcquisitionSeries, AcquisitionTabl ) ) - # TODO: add left and right lick times acquisiton_entries.append( self._reward_delivery_series( rewards, @@ -185,5 +250,19 @@ def build_acquisition(self) -> t.List[t.Union[AcquisitionSeries, AcquisitionTabl side_label="right", ) ) + acquisiton_entries.append( + self._lick_time_series( + is_right=False, + name="left_lick_time", + side_label="left", + ) + ) + acquisiton_entries.append( + self._lick_time_series( + is_right=True, + name="right_lick_time", + side_label="right", + ) + ) return acquisiton_entries diff --git a/tests/test_nwb/test_acquisition/test_acquisition_builder.py b/tests/test_nwb/test_acquisition/test_acquisition_builder.py index 15f37c5..c822592 100644 --- a/tests/test_nwb/test_acquisition/test_acquisition_builder.py +++ b/tests/test_nwb/test_acquisition/test_acquisition_builder.py @@ -87,6 +87,17 @@ def _empty_manual_water_frame() -> pd.DataFrame: return pd.DataFrame({"data": []}, index=pd.Index([], name="time")) +def _make_digital_input_frame() -> pd.DataFrame: + """Build a DigitalInputState frame: left licks high at 1.0, right at 2.0/2.5.""" + return pd.DataFrame( + { + "DIPort0": [True, False, False], + "DIPort1": [False, True, True], + }, + index=pd.Index([1.0, 2.0, 2.5], name="time"), + ) + + def _make_dataset(manual_water=None): """Build a path-aware fake dataset rooted at ``Behavior``.""" if manual_water is None: @@ -95,7 +106,12 @@ def _make_dataset(manual_water=None): { "Behavior": _FakeNode( { - "HarpBehavior": _FakeNode({"OutputSet": _FakeStream(_make_output_set_frame())}), + "HarpBehavior": _FakeNode( + { + "OutputSet": _FakeStream(_make_output_set_frame()), + "DigitalInputState": _FakeStream(_make_digital_input_frame()), + } + ), "SoftwareEvents": _FakeNode( { "TrialOutcome": _FakeStream(_make_trial_outcome_frame()), @@ -168,8 +184,41 @@ def test_get_manual_water_times_returns_empty_when_absent(): assert result.empty +def test_get_lick_times_selects_di_port_by_side(): + """Left licks come from DIPort0 and right licks from DIPort1.""" + builder = AcquisitionBuilder(loader=_make_loader()) + + np.testing.assert_array_equal(builder.get_lick_times(is_right=False), np.array([1.0])) + np.testing.assert_array_equal(builder.get_lick_times(is_right=True), np.array([2.0, 2.5])) + + +def test_get_lick_times_returns_empty_when_absent(): + """A missing DigitalInputState stream yields an empty array.""" + dataset = _FakeNode( + { + "Behavior": _FakeNode( + { + "HarpBehavior": _FakeNode({"OutputSet": _FakeStream(_make_output_set_frame())}), + "SoftwareEvents": _FakeNode( + { + "TrialOutcome": _FakeStream(_make_trial_outcome_frame()), + "GiveManualWaterRight": _FakeStream(_empty_manual_water_frame()), + } + ), + } + ) + } + ) + builder = AcquisitionBuilder(loader=_make_loader(dataset)) + + result = builder.get_lick_times(is_right=True) + + assert isinstance(result, np.ndarray) + assert result.size == 0 + + def test_build_acquisition_returns_populated_list(): - """``build_acquisition`` returns table entries plus both reward port series.""" + """``build_acquisition`` returns the table plus reward and lick port series.""" # A right-side manual-water event (data=True) near the second right delivery. manual = pd.DataFrame({"data": [True]}, index=pd.Index([0.49], name="time")) builder = AcquisitionBuilder(loader=_make_loader(_make_dataset(manual))) @@ -177,25 +226,36 @@ def test_build_acquisition_returns_populated_list(): acquisition = builder.build_acquisition() assert isinstance(acquisition, list) - assert len(acquisition) == 3 + assert len(acquisition) == 5 - table, left, right = acquisition + table, left_reward, right_reward, left_lick, right_lick = acquisition assert isinstance(table, AcquisitionTable) assert table.name == "Behavior.RawStream" assert table.description == "raw stream desc" - # Left: one valve-open delivery, earned (no auto-response, no left manual). - assert isinstance(left, AcquisitionSeries) - np.testing.assert_array_equal(left.timestamps, np.array([0.1])) - np.testing.assert_array_equal(left.data, np.array(["earned"])) - assert left.name == "left_reward_delivery_time" - assert left.unit == "second" - assert "left lick port" in left.description - - # Right: two deliveries. Both nearest the auto-response trial (0.4); the - # second is overridden to manual by the right-side manual-water event. - assert isinstance(right, AcquisitionSeries) - np.testing.assert_array_equal(right.timestamps, np.array([0.3, 0.5])) - np.testing.assert_array_equal(right.data, np.array(["automatic", "manual"])) - assert right.name == "right_reward_delivery_time" - assert "right lick port" in right.description + # Left reward: one valve-open delivery, earned (no auto-response, no left manual). + assert isinstance(left_reward, AcquisitionSeries) + np.testing.assert_array_equal(left_reward.timestamps, np.array([0.1])) + np.testing.assert_array_equal(left_reward.data, np.array(["earned"])) + assert left_reward.name == "left_reward_delivery_time" + assert left_reward.unit == "second" + assert "left lick port" in left_reward.description + + # Right reward: two deliveries. Both nearest the auto-response trial (0.4); + # the second is overridden to manual by the right-side manual-water event. + assert isinstance(right_reward, AcquisitionSeries) + np.testing.assert_array_equal(right_reward.timestamps, np.array([0.3, 0.5])) + np.testing.assert_array_equal(right_reward.data, np.array(["automatic", "manual"])) + assert right_reward.name == "right_reward_delivery_time" + assert "right lick port" in right_reward.description + + # Lick series: timestamps from the DI ports, data marks each as a lick. + assert left_lick.name == "left_lick_time" + np.testing.assert_array_equal(left_lick.timestamps, np.array([1.0])) + np.testing.assert_array_equal(left_lick.data, np.array([True])) + assert "DIPort0" in left_lick.description + + assert right_lick.name == "right_lick_time" + np.testing.assert_array_equal(right_lick.timestamps, np.array([2.0, 2.5])) + np.testing.assert_array_equal(right_lick.data, np.array([True, True])) + assert "DIPort1" in right_lick.description From 229166b808a5e1f157d64a13c38377ad39666425 Mon Sep 17 00:00:00 2001 From: arjunsridhar12345 Date: Thu, 25 Jun 2026 14:36:57 -0700 Subject: [PATCH 19/36] refactor: add more comments to try to clarify logic --- .../utils/rewards.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/dynamic_foraging_processing/utils/rewards.py b/src/dynamic_foraging_processing/utils/rewards.py index a92a3cd..e975180 100644 --- a/src/dynamic_foraging_processing/utils/rewards.py +++ b/src/dynamic_foraging_processing/utils/rewards.py @@ -73,18 +73,26 @@ def get_annotated_rewards( return np.array([], dtype=object) # Correlate each manual-water software event to its closest reward delivery; - # those deliveries are the manual ones. + # those deliveries are the manual ones. We query with manual_water_times so the + # returned positions index into reward_times -- i.e. the reward deliveries that + # are manual -- giving the set we test reward indices against below. manual_water_times = np.asarray(manual_water_times) if manual_water_times.size: - manual_indices = set(find_closest_timestamps(manual_water_times, reward_times).tolist()) + manual_indices_in_reward_times = set( + find_closest_timestamps(manual_water_times, reward_times).tolist() + ) else: - manual_indices = set() + manual_indices_in_reward_times = set() - trial_indices = find_closest_timestamps(reward_times, trial_outcome_df.index.to_numpy()) + # The opposite direction: query with reward_times so we get one trial position + # per reward delivery (used to look up each delivery's originating trial below). + trial_indices_in_reward_times = find_closest_timestamps( + reward_times, trial_outcome_df.index.to_numpy() + ) annotated_rewards = [] - for i, trial_index in enumerate(trial_indices): - if i in manual_indices: + for i, trial_index in enumerate(trial_indices_in_reward_times): + if i in manual_indices_in_reward_times: annotated_rewards.append("manual") continue From 9b34d15a0381802f2742d68d13185b4e8bf86567 Mon Sep 17 00:00:00 2001 From: arjunsridhar12345 Date: Thu, 25 Jun 2026 14:57:03 -0700 Subject: [PATCH 20/36] refactor: disable getting all streams for now --- .../nwb/acquisition/acquisition_builder.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py b/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py index b0d7ec6..ee21aa5 100644 --- a/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py +++ b/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py @@ -218,15 +218,15 @@ def build_acquisition(self) -> t.List[t.Union[AcquisitionSeries, AcquisitionTabl acquisiton_entries: t.List[t.Union[AcquisitionSeries, AcquisitionTable]] = [] - for stream_name, stream_data in acquisition_streams.items(): - description = acqusition_streams_descriptions.get(stream_name, "") - acquisiton_entries.append( - AcquisitionTable( - name=stream_name, - data=stream_data, - description=description, - ) - ) + # for stream_name, stream_data in acquisition_streams.items(): + # description = acqusition_streams_descriptions.get(stream_name, "") + # acquisiton_entries.append( + # AcquisitionTable( + # name=stream_name, + # data=stream_data, + # description=description, + # ) + # ) acquisiton_entries.append( self._reward_delivery_series( From 7df822606eba728e0e81193a6c33d3bfd1f8baad Mon Sep 17 00:00:00 2001 From: arjunsridhar12345 Date: Thu, 25 Jun 2026 14:58:28 -0700 Subject: [PATCH 21/36] fic: disable more stuff for now --- .../nwb/acquisition/acquisition_builder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py b/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py index ee21aa5..c0feb09 100644 --- a/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py +++ b/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py @@ -213,8 +213,8 @@ def build_acquisition(self) -> t.List[t.Union[AcquisitionSeries, AcquisitionTabl trial_outcomes = self.get_trial_outcomes() manual_water = self.get_manual_water_times() - acquisition_streams = self.loader.get_all_raw_data() - acqusition_streams_descriptions = self.loader.raw_data_stream_descriptions + # acquisition_streams = self.loader.get_all_raw_data() + # acqusition_streams_descriptions = self.loader.raw_data_stream_descriptions acquisiton_entries: t.List[t.Union[AcquisitionSeries, AcquisitionTable]] = [] From 1d5ad6b17336dd83ef18d715e8dfdf9faee42c0d Mon Sep 17 00:00:00 2001 From: arjunsridhar12345 Date: Mon, 29 Jun 2026 17:55:10 -0700 Subject: [PATCH 22/36] build: update lock file --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index ed89671..ee02f85 100644 --- a/uv.lock +++ b/uv.lock @@ -585,7 +585,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "aind-behavior-dynamic-foraging", extras = ["data"], git = "https://github.com/AllenNeuralDynamics/Aind.Behavior.DynamicForaging.git?rev=v0.0.2rc27" }, + { name = "aind-behavior-dynamic-foraging", extras = ["data"], git = "https://github.com/AllenNeuralDynamics/Aind.Behavior.DynamicForaging.git?rev=v0.0.2" }, { name = "aind-data-schema", marker = "extra == 'qc'", specifier = ">=2.4.1" }, { name = "dynamic-foraging-processing", extras = ["qc"], marker = "extra == 'full'" }, { name = "ipykernel" }, From 38fecfcd11c3a2fe74bba5e8cd66b3a9ab44797c Mon Sep 17 00:00:00 2001 From: arjunsridhar12345 Date: Mon, 29 Jun 2026 17:55:47 -0700 Subject: [PATCH 23/36] refactor: PR feedback. add stream name to licks and simplify manual reward logic --- .../nwb/acquisition/acquisition_builder.py | 59 ++++++++++++------- .../utils/rewards.py | 45 +++++++------- 2 files changed, 59 insertions(+), 45 deletions(-) diff --git a/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py b/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py index c0feb09..5185c46 100644 --- a/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py +++ b/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py @@ -76,18 +76,23 @@ def get_manual_water_times(self) -> pd.DataFrame: except (KeyError, FileNotFoundError): return pd.DataFrame({"data": []}) - def get_lick_times(self, is_right: bool) -> np.ndarray: + def get_lick_times(self, stream_name: str, port: str) -> np.ndarray: """Get the lick times for one lick port from the behavior board DI ports. - Licks are read from the HarpBehavior ``DigitalInputState`` stream. Left - licks are on ``DIPort0`` and right licks on ``DIPort1``; a lick time is a - timestamp at which that port's digital input is high. + Licks are read from a HarpBehavior digital-input stream. On the standard + behavior board this is ``DigitalInputState``; LicketySplit boards expose + an equivalent stream under a different name. Left licks are on + ``DIPort0`` and right licks on ``DIPort1``; a lick time is a timestamp at + which that port's digital input is high. Parameters ---------- - is_right : bool - ``True`` for the right lick port (``DIPort1``), ``False`` for the - left lick port (``DIPort0``). + stream_name : str + The HarpBehavior digital-input stream to read licks from (e.g. + ``"DigitalInputState"`` for the standard behavior board). + port : str + The DI port column to read licks from (``"DIPort1"`` for the right + lick port, ``"DIPort0"`` for the left lick port). Returns ------- @@ -95,28 +100,32 @@ def get_lick_times(self, is_right: bool) -> np.ndarray: Sorted lick timestamps for the requested port, or an empty array when the stream is absent. """ - port_column = "DIPort1" if is_right else "DIPort0" try: data = ( self.loader.dataset.at("Behavior") .at("HarpBehavior") - .at("DigitalInputState") + .at(stream_name) .load() .data ) except (KeyError, FileNotFoundError): return np.array([]) - licks = data[data[port_column].fillna(False).astype(bool)] + licks = data[data[port].fillna(False).astype(bool)] return licks.index.to_numpy() - def _lick_time_series(self, *, is_right: bool, name: str, side_label: str) -> AcquisitionSeries: + def _lick_time_series( + self, *, stream_name: str, port: str, name: str, side_label: str + ) -> AcquisitionSeries: """Build one lick port's lick-time series from the behavior board DI ports. Parameters ---------- - is_right : bool - ``True`` for the right lick port (``DIPort1``), ``False`` for the - left lick port (``DIPort0``). + stream_name : str + The HarpBehavior digital-input stream to read licks from (e.g. + ``"DigitalInputState"`` for the standard behavior board). + port : str + The DI port column to read licks from (``"DIPort1"`` for the right + lick port, ``"DIPort0"`` for the left lick port). name : str Acquisition series name. side_label : str @@ -128,15 +137,14 @@ def _lick_time_series(self, *, is_right: bool, name: str, side_label: str) -> Ac The lick-time series for this lick port. The ``data`` array marks each timestamp as a detected lick (``True``). """ - lick_times = self.get_lick_times(is_right) - port_column = "DIPort1" if is_right else "DIPort0" + lick_times = self.get_lick_times(stream_name, port) return AcquisitionSeries( name=name, data=np.ones(lick_times.shape[0], dtype=bool), timestamps=lick_times, unit="second", description=( - f"The lick times of the {side_label} lick port ({port_column} on the " + f"The lick times of the {side_label} lick port ({port} on the " "behavior board)." ), ) @@ -201,9 +209,18 @@ def _reward_delivery_series( ), ) - def build_acquisition(self) -> t.List[t.Union[AcquisitionSeries, AcquisitionTable]]: + def build_acquisition( + self, lick_stream_name: str = "DigitalInputState" + ) -> t.List[t.Union[AcquisitionSeries, AcquisitionTable]]: """Build the NWB acquisition entries. + Parameters + ---------- + lick_stream_name : str, optional + The HarpBehavior digital-input stream to read lick times from. + Defaults to ``"DigitalInputState"`` for the Janelia boards; + pass the equivalent stream name for LicketySplit boards. + Returns ------- list of AcquisitionSeries or AcquisitionTable @@ -252,14 +269,16 @@ def build_acquisition(self) -> t.List[t.Union[AcquisitionSeries, AcquisitionTabl ) acquisiton_entries.append( self._lick_time_series( - is_right=False, + stream_name=lick_stream_name, + port="DIPort0", name="left_lick_time", side_label="left", ) ) acquisiton_entries.append( self._lick_time_series( - is_right=True, + stream_name=lick_stream_name, + port="DIPort1", name="right_lick_time", side_label="right", ) diff --git a/src/dynamic_foraging_processing/utils/rewards.py b/src/dynamic_foraging_processing/utils/rewards.py index e975180..01ac7c9 100644 --- a/src/dynamic_foraging_processing/utils/rewards.py +++ b/src/dynamic_foraging_processing/utils/rewards.py @@ -45,11 +45,9 @@ def get_annotated_rewards( ``GiveManualWater`` software event for this port. The software-event timestamps are correlated to the reward-delivery timestamps with :func:`find_closest_timestamps`. - - ``automatic`` -- otherwise, when the matching trial auto-responded to this - same port (``is_auto_response_right is is_right``: ``True`` for the right - port, ``False`` for the left port). - - ``earned`` -- otherwise (no auto-response, or auto-response to the other - port). + - ``automatic`` -- otherwise, when the matching trial auto-responded + (``is_auto_response_right is not None``). + - ``earned`` -- otherwise (no matching trial, or no auto-response). Parameters ---------- @@ -72,30 +70,14 @@ def get_annotated_rewards( if reward_times.size == 0: return np.array([], dtype=object) - # Correlate each manual-water software event to its closest reward delivery; - # those deliveries are the manual ones. We query with manual_water_times so the - # returned positions index into reward_times -- i.e. the reward deliveries that - # are manual -- giving the set we test reward indices against below. - manual_water_times = np.asarray(manual_water_times) - if manual_water_times.size: - manual_indices_in_reward_times = set( - find_closest_timestamps(manual_water_times, reward_times).tolist() - ) - else: - manual_indices_in_reward_times = set() - - # The opposite direction: query with reward_times so we get one trial position - # per reward delivery (used to look up each delivery's originating trial below). + # Annotate each delivery from its originating trial: query with reward_times so we + # get one trial position per reward delivery. trial_indices_in_reward_times = find_closest_timestamps( reward_times, trial_outcome_df.index.to_numpy() ) annotated_rewards = [] - for i, trial_index in enumerate(trial_indices_in_reward_times): - if i in manual_indices_in_reward_times: - annotated_rewards.append("manual") - continue - + for trial_index in trial_indices_in_reward_times: outcome = _parse_outcome(trial_outcome_df.iloc[trial_index]["data"]) trial = outcome.trial if outcome is not None else None if trial is None or trial.is_auto_response_right is None: @@ -103,4 +85,17 @@ def get_annotated_rewards( else: annotated_rewards.append("automatic") - return np.array(annotated_rewards) + annotated_rewards = np.array(annotated_rewards) + + # Manual water is independent of trials (multiple can occur within a trial) and + # takes precedence, so annotate the manual deliveries directly. Correlate each + # manual-water software event to its closest reward delivery; the returned + # positions index into reward_times, i.e. the deliveries that are manual. + manual_water_times = np.asarray(manual_water_times) + if manual_water_times.size: + manual_indices_in_reward_times = find_closest_timestamps( + manual_water_times, reward_times + ) + annotated_rewards[manual_indices_in_reward_times] = "manual" + + return annotated_rewards From d3498fea44f8d515a4e5f87387cb2ad875b28aab Mon Sep 17 00:00:00 2001 From: arjunsridhar12345 Date: Mon, 29 Jun 2026 17:55:57 -0700 Subject: [PATCH 24/36] test: update tests --- .../test_acquisition/test_acquisition_builder.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/test_nwb/test_acquisition/test_acquisition_builder.py b/tests/test_nwb/test_acquisition/test_acquisition_builder.py index c822592..54c3990 100644 --- a/tests/test_nwb/test_acquisition/test_acquisition_builder.py +++ b/tests/test_nwb/test_acquisition/test_acquisition_builder.py @@ -188,8 +188,12 @@ def test_get_lick_times_selects_di_port_by_side(): """Left licks come from DIPort0 and right licks from DIPort1.""" builder = AcquisitionBuilder(loader=_make_loader()) - np.testing.assert_array_equal(builder.get_lick_times(is_right=False), np.array([1.0])) - np.testing.assert_array_equal(builder.get_lick_times(is_right=True), np.array([2.0, 2.5])) + np.testing.assert_array_equal( + builder.get_lick_times("DigitalInputState", "DIPort0"), np.array([1.0]) + ) + np.testing.assert_array_equal( + builder.get_lick_times("DigitalInputState", "DIPort1"), np.array([2.0, 2.5]) + ) def test_get_lick_times_returns_empty_when_absent(): @@ -211,7 +215,7 @@ def test_get_lick_times_returns_empty_when_absent(): ) builder = AcquisitionBuilder(loader=_make_loader(dataset)) - result = builder.get_lick_times(is_right=True) + result = builder.get_lick_times("DigitalInputState", "DIPort1") assert isinstance(result, np.ndarray) assert result.size == 0 From f3e4fc3f7456b05ebf6426377e0ec0d10f216f7e Mon Sep 17 00:00:00 2001 From: arjunsridhar12345 Date: Tue, 30 Jun 2026 09:56:50 -0700 Subject: [PATCH 25/36] build: upgrade lock --- uv.lock | 693 +++++++++++++++++++++++++------------------------------- 1 file changed, 309 insertions(+), 384 deletions(-) diff --git a/uv.lock b/uv.lock index ee02f85..23aa179 100644 --- a/uv.lock +++ b/uv.lock @@ -152,24 +152,24 @@ wheels = [ [[package]] name = "beautifulsoup4" -version = "4.14.3" +version = "4.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "soupsieve" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, + { url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924, upload-time = "2026-06-07T16:44:21.566Z" }, ] [[package]] name = "certifi" -version = "2026.2.25" +version = "2026.6.17" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, ] [[package]] @@ -304,14 +304,14 @@ wheels = [ [[package]] name = "click" -version = "8.3.2" +version = "8.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, ] [[package]] @@ -400,7 +400,7 @@ wheels = [ [[package]] name = "contraqctor" -version = "0.5.7" +version = "0.5.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aind-behavior-services" }, @@ -417,93 +417,78 @@ dependencies = [ { name = "scipy" }, { name = "semver" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/db/462e9992cf98faf9b5a7eb8ff9906d7812266df377a0d4790c968610fd40/contraqctor-0.5.7.tar.gz", hash = "sha256:04c7540fb6d7f8c4f20d14e8e0f301cd80cff218254e1692b4bcfd8a13a48a7b", size = 55942, upload-time = "2026-05-08T19:58:29.611Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c1/26/c9459dc2036c902a05c9c66358bba9834d3c8d4ecee64c58b3d23d6800e6/contraqctor-0.5.8.tar.gz", hash = "sha256:a91797987f2ebc4ca1c6c4fdfc7867ef8ee7a566382e8ffe1c8c289cca61a9a8", size = 55929, upload-time = "2026-06-18T09:42:15.888Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/ca/7037a0bcbf4431fcce48f6b1033df0c8172e047c35531c0b78438aecb2ff/contraqctor-0.5.7-py3-none-any.whl", hash = "sha256:78b40003a79f1bf8071f89af1c970e856ea2ad10d93baffc8e3a5cd078a2cc62", size = 69247, upload-time = "2026-05-08T19:58:30.692Z" }, + { url = "https://files.pythonhosted.org/packages/19/9e/dc69736ba3f4c852e42299d4e2705202abcac44dc52b272019708628c93c/contraqctor-0.5.8-py3-none-any.whl", hash = "sha256:56d8e4ef87c3cbfe3c7dd6066c519fcb57341b6815ca4f7582290bd210ed4d65", size = 69225, upload-time = "2026-06-18T09:42:14.91Z" }, ] [[package]] name = "coverage" -version = "7.13.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, - { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, - { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, - { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, - { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, - { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, - { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, - { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, - { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, - { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, - { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, - { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, - { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, - { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, - { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, - { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, - { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, - { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, - { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, - { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, - { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, - { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, - { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, - { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, - { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, - { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, - { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, - { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, - { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, - { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, - { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, - { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, - { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, - { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, - { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, - { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, - { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, - { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, - { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, - { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, - { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, - { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, - { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, - { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, - { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, - { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, - { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, - { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, - { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, - { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, - { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, - { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, - { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, - { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, - { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, - { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, - { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, - { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, - { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, - { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, - { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, - { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, - { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, - { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, - { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, - { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, - { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, - { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, - { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, - { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, - { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, - { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, - { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, +version = "7.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/91/0a7c28934e50d8ac9a7b117712d176f2953c3170bccced5eaacfa3e96175/coverage-7.14.3.tar.gz", hash = "sha256:1a7563a443f3d53fdeb040ec8c9f7466aed7ca3dc5891aa09d3ca3625fa4387f", size = 924398, upload-time = "2026-06-22T23:10:25.584Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/b0/8a911f6ffe6974dac4df95b468ab9a2899d0e59f0f99a489afeec39f00bc/coverage-7.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3d74ff26299c4879ce3a4d826f9d3d4d556fd285fde7bbce3c0ef5a8ab1cec24", size = 220672, upload-time = "2026-06-22T23:08:26.621Z" }, + { url = "https://files.pythonhosted.org/packages/36/16/0fc0cb52538783dbbae0934b834f5a58fd5354380ee6cad4a07b15dc845d/coverage-7.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:96150a9cf3468ea20f0bc5d0e21b3df8972c31480ef90fa7614b773cc6429665", size = 221035, upload-time = "2026-06-22T23:08:28.372Z" }, + { url = "https://files.pythonhosted.org/packages/77/e2/421ccfbb48335ac49e93301478cf5d623b0c2bf1c0cadd8e2b2fc6c0c710/coverage-7.14.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:27d07a46500ba23515b838dbcf52512026af04090755cf6cc64166d88c9b9a1a", size = 252540, upload-time = "2026-06-22T23:08:30.226Z" }, + { url = "https://files.pythonhosted.org/packages/06/c2/05b8c890097c61a7f4406b35396b997a635200ded0339eda83dfbe526c5f/coverage-7.14.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:621e13c6108234d7960aaf5762ab5c3c00f33c30c15af06dcbff0c73bf112727", size = 255274, upload-time = "2026-06-22T23:08:31.876Z" }, + { url = "https://files.pythonhosted.org/packages/dc/be/b6d9efe447f8ba3c3c854195f326bd64c54b907d936cd2fdebf8767ec72e/coverage-7.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b60ca6d8af70473491a15a343cbabab2e8f9ea66a4376e81c7aa24876a6f977", size = 256389, upload-time = "2026-06-22T23:08:33.843Z" }, + { url = "https://files.pythonhosted.org/packages/d4/3c/f26e50acc429e608bc534ac06f0a3c169019c798178ec5e9de3dbc0df9c9/coverage-7.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c90a7cdd5e380e1ce02f19792e2ac2fbfbf177e35a27e69fd3e873b30d895c0c", size = 258648, upload-time = "2026-06-22T23:08:35.481Z" }, + { url = "https://files.pythonhosted.org/packages/9e/a2/01c1fabf816c8e1dae197e258edf878a3d3ddc86fbda34b76e5794277d8f/coverage-7.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5d788e5fd55347eef06ca0732c77d04a264de67e8ff24631270cdff3767a60cf", size = 252949, upload-time = "2026-06-22T23:08:37.562Z" }, + { url = "https://files.pythonhosted.org/packages/89/c6/941166dd79c31fd44a13063780ae8d552eee0089a0a0930b9bdb7df554ed/coverage-7.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62c7f79db2851c95ef020e5d28b97afde3daf9f7febcd35b53e05638f729063f", size = 254310, upload-time = "2026-06-22T23:08:39.174Z" }, + { url = "https://files.pythonhosted.org/packages/10/31/80b1fd028201a961033ce95be3cd1e39e521b3762e6b4a1ac1616cb291e7/coverage-7.14.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:90f7608aeb5d9b60b523b9fb2a4ee1973867cc4865a3f26fe6c7577073b70205", size = 252453, upload-time = "2026-06-22T23:08:40.84Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/c3d9addd94c4b524f3f4af0232075f5fe7170ce99a1386edff803e5934db/coverage-7.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1e3b91f9c4740aeb571ecf82e5e8d8e4ab62d34fcb5a5d4e5baa38c6f7d2857c", size = 256522, upload-time = "2026-06-22T23:08:42.494Z" }, + { url = "https://files.pythonhosted.org/packages/91/14/e5a0575f73795af3a7a9ae13dadf812e17d32422896839987dc3f86947e1/coverage-7.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c946099774a7699de03cbd0ff0a64e21aed4525eed9d959adde4afe6d15758ef", size = 252023, upload-time = "2026-06-22T23:08:44.243Z" }, + { url = "https://files.pythonhosted.org/packages/38/9b/9652ee531937ce3b8a63a8896885b2b4a2d56adc30e53c9540c666286d88/coverage-7.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:16b206e521feb8b7133a45754643dead0538489cf8b783b90cf5f4e3299625fd", size = 253893, upload-time = "2026-06-22T23:08:46.113Z" }, + { url = "https://files.pythonhosted.org/packages/b1/05/42678841c8c38e4b08bdfc48269f5a16dfbf5806000fe6a89b4cece3c691/coverage-7.14.3-cp312-cp312-win32.whl", hash = "sha256:ea3169c7116eb6cdf7608c6c7da9ecfcb3da40688e3a510fac2d1d2bafd6dc35", size = 222734, upload-time = "2026-06-22T23:08:47.858Z" }, + { url = "https://files.pythonhosted.org/packages/df/87/07a4fcee55177a25f1b52331a8e92cf4f2c53b1a9c75ce2981fd59c684ad/coverage-7.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:7ea52fc08f007bcc494d4bb3df3851e95843d881860ba38fe2c64dc100db5e7d", size = 223266, upload-time = "2026-06-22T23:08:49.494Z" }, + { url = "https://files.pythonhosted.org/packages/aa/34/2b8b66a989282ea7b370beb49f50bab29470dc30bb0b03935b6b802782f7/coverage-7.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:8cec0ad652ec57790970d817490105bd917d783c2f7b38d6b58a0ca312e1a336", size = 222655, upload-time = "2026-06-22T23:08:51.766Z" }, + { url = "https://files.pythonhosted.org/packages/a9/83/7fefbf5df23ed2b7f489907564a7b34b9b07098128e12e0fdfa92626e456/coverage-7.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:47968988b367990ae4ab17523790c38cd125e02c6bfd379b6022be2d40bdc38c", size = 220699, upload-time = "2026-06-22T23:08:53.522Z" }, + { url = "https://files.pythonhosted.org/packages/31/e6/38c3653ff6d56d704b29241362387ca824e38e15b76fdcb7096538195790/coverage-7.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0ee68f5c34812780f3a7063382c0a9fcbb99985b7ddcdcaa626e4f3fb2e0783a", size = 221068, upload-time = "2026-06-22T23:08:55.571Z" }, + { url = "https://files.pythonhosted.org/packages/20/86/4f5c45d51c5cd10a128933f0fd235393c9146abbfd2ce2dfa68b3267ead3/coverage-7.14.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fa9e5c6857a7e80fa22ace5cf3550ae392bbfc322f1d8dd2d2d5a8be38cec027", size = 252060, upload-time = "2026-06-22T23:08:57.464Z" }, + { url = "https://files.pythonhosted.org/packages/82/50/dfce42eff2cecabcd5a9bbad5489449c87db3415f408d23ffee417ce01f6/coverage-7.14.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:98a0859b0e98e43e1178a9402e19c8127766b14f7109a374d976e5a62c0e5c73", size = 254657, upload-time = "2026-06-22T23:08:59.453Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d2/639ceb1bc8038fd0d66768278d5dc22df3391918b8278c2a21aa2602a531/coverage-7.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69918344541ed9c8368566c2adc03c0e33d4550d7faa87d1b35e49b6a3286ea9", size = 255892, upload-time = "2026-06-22T23:09:01.291Z" }, + { url = "https://files.pythonhosted.org/packages/8b/96/002094a10e113512500dc1e10430a449417e17b0f90f7d496bcb820208b7/coverage-7.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b7f300ac92cd4b570724c8ffbbd0c130fee298d2447f41d5a3abf58976fae1de", size = 258026, upload-time = "2026-06-22T23:09:03.017Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ec/286a5d2fad9c4bee59bd724feeb7d5bf8303c6c9200b51d1dd945a9c72b0/coverage-7.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11a7ec9f97ab950f4c5af62229befc7faf208fdbc0116d3902d7e306cf2c5abd", size = 252285, upload-time = "2026-06-22T23:09:04.773Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7d/a17753a0b12dd48d0d50f5fab079ad99d3be1eac790494d89f3a417ca0b9/coverage-7.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a571bd889cd36c5922ce8e42e059f9d37d02301531d11374afa4c87a578625d5", size = 254023, upload-time = "2026-06-22T23:09:06.513Z" }, + { url = "https://files.pythonhosted.org/packages/86/ef/a76c6ceba6a2c313f905310abf2701d534cada22d372db11731831e9e209/coverage-7.14.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:de76caefc8deabb0dd1678b6a980be97d14c8d87e213ac194dbf8b09e96d63fb", size = 251989, upload-time = "2026-06-22T23:09:08.382Z" }, + { url = "https://files.pythonhosted.org/packages/d9/39/353013a75fec0fb49f7553519f9d52b4441e902e5178c93f38eb6c07cedb/coverage-7.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d20a15c622194234161535459affa8f7905830391c9ccfa060d495dbfe3a1c7f", size = 256144, upload-time = "2026-06-22T23:09:10.369Z" }, + { url = "https://files.pythonhosted.org/packages/29/0e/613878555d734def11c5b20a2701a15cb3781b9e9ea749da27c5f436e928/coverage-7.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b488bd4b23397db62e7a9459129d01ff06a846582a732efd24834b24a6ada498", size = 251808, upload-time = "2026-06-22T23:09:12.057Z" }, + { url = "https://files.pythonhosted.org/packages/af/76/359c058c9cfdcf1e8b107663881225b03b364a320017eda24a2a66e55102/coverage-7.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6a3693b4153394d265f44fb855fdc80e72403024d4d6f91c4871b334d028e4e0", size = 253579, upload-time = "2026-06-22T23:09:13.858Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d9/4ba2f060933a30ebe363cef9f67a365b0a317e580c0d5d9169d56a73ef1c/coverage-7.14.3-cp313-cp313-win32.whl", hash = "sha256:338b19131ab1a6b767b462bfcbaa692e7ae22f24463e39d49b02a83410ff6b37", size = 222741, upload-time = "2026-06-22T23:09:15.636Z" }, + { url = "https://files.pythonhosted.org/packages/76/e8/196ebc25d8f34c06d43a6e9c8513c9266ef8dbf3b5672beb1a00cf5e29fa/coverage-7.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:b3d77f7f196abdef7e01415de1bce09f216189e83e58159cfeef2b92d0464994", size = 223283, upload-time = "2026-06-22T23:09:17.478Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/51d2aac6417523a286f10fb25f09eb9518a84df9f1151e93ff6871f34849/coverage-7.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:e6230e688c7c3e65cedd41a774eb4ec221adc6bfee13768231015b702d5e4150", size = 222678, upload-time = "2026-06-22T23:09:19.7Z" }, + { url = "https://files.pythonhosted.org/packages/61/56/14e3b97facbfa1304dd19e676e26599ad359f04714bed32f7f1c5a88efdc/coverage-7.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:605ab2b566a22bd94834529d66d295c364aba84afd3e5498285c7a524017b1fc", size = 220741, upload-time = "2026-06-22T23:09:21.616Z" }, + { url = "https://files.pythonhosted.org/packages/12/1d/db378b5cca433b90b893f26dab728b280ddd89f272a1fdfed4aeaa05c686/coverage-7.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a3c2134809e80fac091bfed18a6991b5a5eb5df5ae32b17ac4f4f99864b73dd7", size = 221068, upload-time = "2026-06-22T23:09:23.452Z" }, + { url = "https://files.pythonhosted.org/packages/47/f0/3f8421b20d9c4fcd39be9a8ca3c3fda8bc204b44efbd09fede153afd3e2f/coverage-7.14.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c02efd507227bde9969cab0db8f48890eb3b5dcad6afac57a4792df4133543ce", size = 252117, upload-time = "2026-06-22T23:09:25.458Z" }, + { url = "https://files.pythonhosted.org/packages/27/ca/59ea35fb99743549ec8b37eff141ece4431fea590c89e536ed8032ef45cf/coverage-7.14.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1bb93c2aa61d2a5b38f1526546d95cf4132cb681e541a337bf8dfd092be816e5", size = 254622, upload-time = "2026-06-22T23:09:27.523Z" }, + { url = "https://files.pythonhosted.org/packages/c8/25/ec6de51ae7493b92a1cf74d1b763121c29636759167e2a593ba4db5881e4/coverage-7.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f502e948e03e866538048bba081c075caaa62e5bda6ea5b7432e45f587eb462a", size = 255968, upload-time = "2026-06-22T23:09:29.43Z" }, + { url = "https://files.pythonhosted.org/packages/5d/05/c8bfc77823f42b4664fb25842f13b567022f6f84a4c83c8ecbb16734b7cb/coverage-7.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9973ef2463f8e6cfb61a6324126bb3e17d67a85f22f58d856e583ea2e3ca6501", size = 258284, upload-time = "2026-06-22T23:09:31.397Z" }, + { url = "https://files.pythonhosted.org/packages/f6/15/1d1b242027124a32b26ef01f82018b8c4ef34ef174aa6aeba7b1eeef48e8/coverage-7.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9be4e7d4c5ca0427889f8f9d614bd630c2be741b1de7699bca3b2b6c0e41003e", size = 252143, upload-time = "2026-06-22T23:09:33.256Z" }, + { url = "https://files.pythonhosted.org/packages/74/b6/d2a9842fd2a5d7d27f1ac851c043a734a494ad75402c5331db3da79ed691/coverage-7.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a574912f3bde4b0619f6e97d01aa590b70998859244793769eb3a6df78ee56d3", size = 253976, upload-time = "2026-06-22T23:09:35.351Z" }, + { url = "https://files.pythonhosted.org/packages/fd/30/e1600ddf7e226db5558bb5323d2186fff00f505c4b764643ec89ce5d8175/coverage-7.14.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e343fb086c9cd780b38622fea7c369acd64c1a0724312149b5d769c387a2b1f5", size = 251942, upload-time = "2026-06-22T23:09:37.313Z" }, + { url = "https://files.pythonhosted.org/packages/d9/2c/9159de64f9dd648e324328d588a44cfab1e331eb5259ce1141afe2a92dfb/coverage-7.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:3c68df8e61f1e09633fefc7538297145623957a048534368c9d212782aa5e845", size = 256220, upload-time = "2026-06-22T23:09:39.165Z" }, + { url = "https://files.pythonhosted.org/packages/91/67/b7f536cc2c124f48e91b22fbb741d2261f4e3d310faf6f76007f47566e5d/coverage-7.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3e5b550a128419373c2f6cec28a244207013ef15f5cbcff6a5ca09d1dfaaf027", size = 251756, upload-time = "2026-06-22T23:09:41.056Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ec/f3718038e2d4860c715a55428377ca7f6c75872caf98cabd982e1d76967d/coverage-7.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2bfc4dd0a912329eccc7484a7d0b2a38032b38c40663b1e1ac595f10c457954b", size = 253413, upload-time = "2026-06-22T23:09:43.306Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a5/91f11efeef89b3cc9b30461128db15b0511ef813ab889a7b7ab636b3a497/coverage-7.14.3-cp314-cp314-win32.whl", hash = "sha256:0423d64c013057a06e70f070f073cec4b0cbc7d2b27f3c7007292f2ff1d52965", size = 222946, upload-time = "2026-06-22T23:09:45.261Z" }, + { url = "https://files.pythonhosted.org/packages/58/fd/98ac9f524d9ec378de831c034dbdeb544ca7ef7d2d9c9996daf232a037fd/coverage-7.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:92c22e19ce64ca3f2ad751f16f14df1468b4c231bd6af97185063a9c292a0cb3", size = 223436, upload-time = "2026-06-22T23:09:47.177Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a0/7cd612d650a772a0ae80144443406bf61981c896c3d57c9e6e79fb2cdbd1/coverage-7.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:41de778bd41780586e2b04912079c73089ab5d839624e28db3bdb26de638da92", size = 222861, upload-time = "2026-06-22T23:09:49.384Z" }, + { url = "https://files.pythonhosted.org/packages/55/57/017353fab573779c0d00448e47d102edd36c792f7b6f233a4d89a7a08384/coverage-7.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8427f370ca67db4c975d2a26acfc0e5783ca0b52444dbc50278ace0f35445949", size = 221474, upload-time = "2026-06-22T23:09:51.417Z" }, + { url = "https://files.pythonhosted.org/packages/69/92/90cf1f1a5c468a9c1b7ba2716e0e205293ad9b02f5f573a6de4318b15ba1/coverage-7.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d8e88f335544a47e22ae2e45b344772925ec65166555c958720d5ed971880891", size = 221738, upload-time = "2026-06-22T23:09:53.487Z" }, + { url = "https://files.pythonhosted.org/packages/a4/c0/4df964fa539f8399fd7679c09c472d73744de334686fd3f01e3a2465ce4e/coverage-7.14.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:beaab199b9e5ceaf5a225e16a9d4df136f2a1eae0a5c20de1e277c8a5225f388", size = 263101, upload-time = "2026-06-22T23:09:55.895Z" }, + { url = "https://files.pythonhosted.org/packages/06/76/e5d33b2576ae3bf2be2058cd1cae57774b61e400f2c3c58f3783dc2ffb4a/coverage-7.14.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3ff255799f5a1676c71c1c32ec01fd043aa09d57b3d95764b24992757184784", size = 265225, upload-time = "2026-06-22T23:09:57.904Z" }, + { url = "https://files.pythonhosted.org/packages/61/d2/e52419afe391a39ba27fdefaf0737d8e34bf03faef6ab3b3006545bbd0d0/coverage-7.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:878832eaac515b62decfa76965aed558775f86bf1fc8cca76993c0c84ae31aed", size = 267643, upload-time = "2026-06-22T23:09:59.938Z" }, + { url = "https://files.pythonhosted.org/packages/58/7a/f2625d8d5006b6b20fba5afaef00b24a763fe96476ea798a3076cbc1f84e/coverage-7.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:611e62cb9386096d81b63e0a05330750268617231e7bd598e1fe77482a2c58a5", size = 268762, upload-time = "2026-06-22T23:10:01.943Z" }, + { url = "https://files.pythonhosted.org/packages/7d/bf/908024006bba57127354d74e938954b9c3cd765cc2e0412dc9c37b415cda/coverage-7.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:02c41de2a88011b893050fc9830267d927a50a215f7ad5ec17349db7090ccf26", size = 262208, upload-time = "2026-06-22T23:10:03.954Z" }, + { url = "https://files.pythonhosted.org/packages/34/a0/d4f9296441b909817442fdb26bd77a698f08272ec683a7394b00eb2e47a0/coverage-7.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:526ce9721116af23b1065089f0b75046fe521e7772ab94b641cd66b7a0421889", size = 265096, upload-time = "2026-06-22T23:10:05.936Z" }, + { url = "https://files.pythonhosted.org/packages/e8/da/4ae4f3f4e477b56a4ce1e5c48a35eff38a94b50130ce5bdc897024741cfc/coverage-7.14.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e4ed44705ca4bead6fc977a8b741f2145608289b33c8a9b42a95d0f15aedbf4d", size = 262699, upload-time = "2026-06-22T23:10:07.973Z" }, + { url = "https://files.pythonhosted.org/packages/d8/7a/6927148073ff32856d78baa77b4ddc07a9be7e90020f9db0661c4ca523a1/coverage-7.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2415902f385a23dcc4ccd26e0ba803249a169af6a930c003a4c715eeb9a5444e", size = 266433, upload-time = "2026-06-22T23:10:10.145Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a7/774f658dbe9c4c3f5daa86a87e0459ac3832e4e3cc67affe078547f727b9/coverage-7.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b75ee850fc2d7c831e883220c445b035f2224de2ba6103f1e56dbd237ab913f7", size = 261547, upload-time = "2026-06-22T23:10:12.191Z" }, + { url = "https://files.pythonhosted.org/packages/3d/14/a0c18c0376c43cbf973f43ef6ca20019c950597180e6396232f7b6a27102/coverage-7.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dc9b4e35e7c3920e925ba7f14886fd5fbe481232754624e832ddba66c7535635", size = 263859, upload-time = "2026-06-22T23:10:14.492Z" }, + { url = "https://files.pythonhosted.org/packages/10/ac/43a3d0f460af524b131a6191805bc5d18b806ab4e828fbf82e8c8c3af446/coverage-7.14.3-cp314-cp314t-win32.whl", hash = "sha256:7b27c822a8161afbe48e99f1adfb098d270ae7e0f7d7b0555ce110529bdb69cc", size = 223250, upload-time = "2026-06-22T23:10:16.758Z" }, + { url = "https://files.pythonhosted.org/packages/3f/5f/d5e5c56b0712e96ce8f69fe7dbf229ff938b437bc50862743c8a0d2cea84/coverage-7.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:39e1dbbb6ff2c338e0196a482558a792a1de3aa64261196f5cdb3da016ad9cda", size = 224082, upload-time = "2026-06-22T23:10:19.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/35/947cbd5be1d3bcbbdc43d6791de8a56c6501903311d42915ae06a82815f0/coverage-7.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:68520c90babfa2d560eca6d497921ed3a4f469623bd709733124491b2aa8ef3f", size = 223400, upload-time = "2026-06-22T23:10:21.24Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e3/a0aa32bfa3a081951f60a23bc0e7b512891ef0eecda1153cf1d8ba36c6b1/coverage-7.14.3-py3-none-any.whl", hash = "sha256:fb7e18afb6e903c1a92401a2f0501ac277dca527bb9ca6fe1f691a8a0026a0e8", size = 212469, upload-time = "2026-06-22T23:10:23.405Z" }, ] [[package]] @@ -517,23 +502,23 @@ wheels = [ [[package]] name = "debugpy" -version = "1.8.20" +version = "1.8.21" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/b7/cd8080344452e4874aae67c40d8940e2b4d47b01601a8fd9f44786c757c7/debugpy-1.8.20.tar.gz", hash = "sha256:55bc8701714969f1ab89a6d5f2f3d40c36f91b2cbe2f65d98bf8196f6a6a2c33", size = 1645207, upload-time = "2026-01-29T23:03:28.199Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/aa/12037145b7a56eaa5b29b41872f7a21b538e807e13f32c4d3c46e59be084/debugpy-1.8.21.tar.gz", hash = "sha256:a3c53278e84c94e11bd87c53970ec391d1a67396c8b22609fcac576520e611a6", size = 1697577, upload-time = "2026-06-01T19:30:35.156Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/57/7f34f4736bfb6e00f2e4c96351b07805d83c9a7b33d28580ae01374430f7/debugpy-1.8.20-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:4ae3135e2089905a916909ef31922b2d733d756f66d87345b3e5e52b7a55f13d", size = 2550686, upload-time = "2026-01-29T23:03:42.023Z" }, - { url = "https://files.pythonhosted.org/packages/ab/78/b193a3975ca34458f6f0e24aaf5c3e3da72f5401f6054c0dfd004b41726f/debugpy-1.8.20-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:88f47850a4284b88bd2bfee1f26132147d5d504e4e86c22485dfa44b97e19b4b", size = 4310588, upload-time = "2026-01-29T23:03:43.314Z" }, - { url = "https://files.pythonhosted.org/packages/c1/55/f14deb95eaf4f30f07ef4b90a8590fc05d9e04df85ee379712f6fb6736d7/debugpy-1.8.20-cp312-cp312-win32.whl", hash = "sha256:4057ac68f892064e5f98209ab582abfee3b543fb55d2e87610ddc133a954d390", size = 5331372, upload-time = "2026-01-29T23:03:45.526Z" }, - { url = "https://files.pythonhosted.org/packages/a1/39/2bef246368bd42f9bd7cba99844542b74b84dacbdbea0833e610f384fee8/debugpy-1.8.20-cp312-cp312-win_amd64.whl", hash = "sha256:a1a8f851e7cf171330679ef6997e9c579ef6dd33c9098458bd9986a0f4ca52e3", size = 5372835, upload-time = "2026-01-29T23:03:47.245Z" }, - { url = "https://files.pythonhosted.org/packages/15/e2/fc500524cc6f104a9d049abc85a0a8b3f0d14c0a39b9c140511c61e5b40b/debugpy-1.8.20-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:5dff4bb27027821fdfcc9e8f87309a28988231165147c31730128b1c983e282a", size = 2539560, upload-time = "2026-01-29T23:03:48.738Z" }, - { url = "https://files.pythonhosted.org/packages/90/83/fb33dcea789ed6018f8da20c5a9bc9d82adc65c0c990faed43f7c955da46/debugpy-1.8.20-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:84562982dd7cf5ebebfdea667ca20a064e096099997b175fe204e86817f64eaf", size = 4293272, upload-time = "2026-01-29T23:03:50.169Z" }, - { url = "https://files.pythonhosted.org/packages/a6/25/b1e4a01bfb824d79a6af24b99ef291e24189080c93576dfd9b1a2815cd0f/debugpy-1.8.20-cp313-cp313-win32.whl", hash = "sha256:da11dea6447b2cadbf8ce2bec59ecea87cc18d2c574980f643f2d2dfe4862393", size = 5331208, upload-time = "2026-01-29T23:03:51.547Z" }, - { url = "https://files.pythonhosted.org/packages/13/f7/a0b368ce54ffff9e9028c098bd2d28cfc5b54f9f6c186929083d4c60ba58/debugpy-1.8.20-cp313-cp313-win_amd64.whl", hash = "sha256:eb506e45943cab2efb7c6eafdd65b842f3ae779f020c82221f55aca9de135ed7", size = 5372930, upload-time = "2026-01-29T23:03:53.585Z" }, - { url = "https://files.pythonhosted.org/packages/33/2e/f6cb9a8a13f5058f0a20fe09711a7b726232cd5a78c6a7c05b2ec726cff9/debugpy-1.8.20-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9c74df62fc064cd5e5eaca1353a3ef5a5d50da5eb8058fcef63106f7bebe6173", size = 2538066, upload-time = "2026-01-29T23:03:54.999Z" }, - { url = "https://files.pythonhosted.org/packages/c5/56/6ddca50b53624e1ca3ce1d1e49ff22db46c47ea5fb4c0cc5c9b90a616364/debugpy-1.8.20-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:077a7447589ee9bc1ff0cdf443566d0ecf540ac8aa7333b775ebcb8ce9f4ecad", size = 4269425, upload-time = "2026-01-29T23:03:56.518Z" }, - { url = "https://files.pythonhosted.org/packages/c5/d9/d64199c14a0d4c476df46c82470a3ce45c8d183a6796cfb5e66533b3663c/debugpy-1.8.20-cp314-cp314-win32.whl", hash = "sha256:352036a99dd35053b37b7803f748efc456076f929c6a895556932eaf2d23b07f", size = 5331407, upload-time = "2026-01-29T23:03:58.481Z" }, - { url = "https://files.pythonhosted.org/packages/e0/d9/1f07395b54413432624d61524dfd98c1a7c7827d2abfdb8829ac92638205/debugpy-1.8.20-cp314-cp314-win_amd64.whl", hash = "sha256:a98eec61135465b062846112e5ecf2eebb855305acc1dfbae43b72903b8ab5be", size = 5372521, upload-time = "2026-01-29T23:03:59.864Z" }, - { url = "https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl", hash = "sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7", size = 5337658, upload-time = "2026-01-29T23:04:17.404Z" }, + { url = "https://files.pythonhosted.org/packages/a2/df/bf625547431a9cadc9f4cbfeda38866e2b17f6aed147b625377e87834449/debugpy-1.8.21-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:9f96713896f39c3dff0ee841f47320c3f2983d33c341e009361bb0ebc79adc4e", size = 2483609, upload-time = "2026-06-01T19:30:50.794Z" }, + { url = "https://files.pythonhosted.org/packages/bf/09/59324b903599031ff9faaec1758292409f6561a0ec2492fe4b703327705a/debugpy-1.8.21-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:c193d474f0a211191f2b4449d2d06157c689013035bd952f3b617e0ef422b176", size = 3968900, upload-time = "2026-06-01T19:30:52.341Z" }, + { url = "https://files.pythonhosted.org/packages/14/cd/27f65b805d7fe005c44e1a36b9183ecdfbcdbf9d3e721a5115d461ecc7ee/debugpy-1.8.21-cp312-cp312-win32.whl", hash = "sha256:4743373c1cac7f9e74a1b9915bf1dbe0e900eca657ffb170ae07ac8363205ae9", size = 5336340, upload-time = "2026-06-01T19:30:54.047Z" }, + { url = "https://files.pythonhosted.org/packages/77/1d/c84e30c0c674184948b66f076ab271c01d940618a2824c23cd035a27bc20/debugpy-1.8.21-cp312-cp312-win_amd64.whl", hash = "sha256:bd7ba9dd3daa7c2f942c6ca8d4695a16bf9ac16b63615261c7982bc74f7ed20c", size = 5374751, upload-time = "2026-06-01T19:30:55.891Z" }, + { url = "https://files.pythonhosted.org/packages/77/6b/d817e1f8cc77aa055d37fba092e0febfdff40fe652d8d53d4cd7a86ad98d/debugpy-1.8.21-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:13678151fc401e2d68c9880b91e28714f797d40422994572b24560ef80910a88", size = 2477398, upload-time = "2026-06-01T19:30:57.644Z" }, + { url = "https://files.pythonhosted.org/packages/48/57/412421516afc3055fa577516f00beec3d663f9b0ab330639547ae6c57720/debugpy-1.8.21-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:ecbd158386c31ffe71d46f72d44d56e66331ab9b16cad649156d514368f23ab2", size = 3962096, upload-time = "2026-06-01T19:30:59.235Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/2c616337cf6ba7b07ebbc97f02c6c945a8e2f76b365e33ee809c32ee36d1/debugpy-1.8.21-cp313-cp313-win32.whl", hash = "sha256:2c2ae706dec41d99a9ca1f7ebc987a83e65578363be6f6b3ac9067504917fae1", size = 5336288, upload-time = "2026-06-01T19:31:00.79Z" }, + { url = "https://files.pythonhosted.org/packages/f8/99/9175103392f84c4b1bf7622888cdc68da07f0ff7d9e581266428f6776033/debugpy-1.8.21-cp313-cp313-win_amd64.whl", hash = "sha256:aa648733047443eb1d07682c4ef287d36a54507b643ffdf38b09a3ef002c72a0", size = 5376567, upload-time = "2026-06-01T19:31:02.56Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3d/f4bbb323a548bfab2af3d6b4ffd9bf22636e55956a1285d317a1de643aad/debugpy-1.8.21-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9bb2a685287a2ac9b181cde89edcec64845cb51de7faaa75badb9a698bc24782", size = 2477209, upload-time = "2026-06-01T19:31:04.157Z" }, + { url = "https://files.pythonhosted.org/packages/8c/2d/6e7ec524984a1702777868de49a4c53202bddac2a432a76a093469587750/debugpy-1.8.21-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:3d6922439bf33fd38a3e2c447869ebc7b97da5cd3d329ff1ef9bc06c4903437e", size = 3927115, upload-time = "2026-06-01T19:31:05.863Z" }, + { url = "https://files.pythonhosted.org/packages/97/47/d1aa6d64005a98a9144647d99306b419396f9ad7bf1d73c119e17a81fb4d/debugpy-1.8.21-cp314-cp314-win32.whl", hash = "sha256:15d4963bd5ffa48f0da0947fd06757fa7621945048a14ad7705431566d3c0e7c", size = 5336724, upload-time = "2026-06-01T19:31:07.711Z" }, + { url = "https://files.pythonhosted.org/packages/5f/67/b905b90d163af11878c1af8abafa4a25206335e112e284e413454543a6da/debugpy-1.8.21-cp314-cp314-win_amd64.whl", hash = "sha256:fe0744a12353406de0ae8ccff0d0a4a666f00801a3db8fd04e7a5f761cd520e8", size = 5373803, upload-time = "2026-06-01T19:31:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/95/51/67e7cf11a53e40694f720457d5b3a1cdaaa3d5a9a633e482f225456b93ff/debugpy-1.8.21-py2.py3-none-any.whl", hash = "sha256:b1e37d333663c8851516a47364ef473da127f9caebe4417e6df6f5825a7e9a92", size = 5352888, upload-time = "2026-06-01T19:31:25.186Z" }, ] [[package]] @@ -708,11 +693,11 @@ wheels = [ [[package]] name = "idna" -version = "3.11" +version = "3.18" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, ] [[package]] @@ -760,7 +745,7 @@ wheels = [ [[package]] name = "ipykernel" -version = "7.2.0" +version = "7.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "appnope", marker = "sys_platform == 'darwin'" }, @@ -770,21 +755,21 @@ dependencies = [ { name = "jupyter-client" }, { name = "jupyter-core" }, { name = "matplotlib-inline" }, - { name = "nest-asyncio" }, + { name = "nest-asyncio2" }, { name = "packaging" }, { name = "psutil" }, { name = "pyzmq" }, { name = "tornado" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ca/8d/b68b728e2d06b9e0051019640a40a9eb7a88fcd82c2e1b5ce70bef5ff044/ipykernel-7.2.0.tar.gz", hash = "sha256:18ed160b6dee2cbb16e5f3575858bc19d8f1fe6046a9a680c708494ce31d909e", size = 176046, upload-time = "2026-02-06T16:43:27.403Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/c4/e4a38f579de4225a561305666f7541cdabb30075def2aa1ac17bd73c1fb5/ipykernel-7.3.0.tar.gz", hash = "sha256:9acaaaf97d16355166e4085afe9d225bfbdf2b7ef520f9df3be8f2b248275e09", size = 184899, upload-time = "2026-06-10T08:41:25.481Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/b9/e73d5d9f405cba7706c539aa8b311b49d4c2f3d698d9c12f815231169c71/ipykernel-7.2.0-py3-none-any.whl", hash = "sha256:3bbd4420d2b3cc105cbdf3756bfc04500b1e52f090a90716851f3916c62e1661", size = 118788, upload-time = "2026-02-06T16:43:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/3d/02/77b271f5dc58bfbc0b577c877b2365d1ffea2afe66a80c13f2312820348c/ipykernel-7.3.0-py3-none-any.whl", hash = "sha256:897eb64da762549ef610698fca5e9675195ec6ac8ec7f19d81ce1ca20c876057", size = 120583, upload-time = "2026-06-10T08:41:23.648Z" }, ] [[package]] name = "ipython" -version = "9.13.0" +version = "9.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -794,14 +779,14 @@ dependencies = [ { name = "matplotlib-inline" }, { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, { name = "prompt-toolkit" }, - { name = "psutil" }, + { name = "psutil", marker = "sys_platform != 'cygwin' and sys_platform != 'emscripten'" }, { name = "pygments" }, { name = "stack-data" }, { name = "traitlets" }, ] -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/53/59/165d3b4d75cc34add3122c4417ecb229085140ac573103c223cd01dde96f/ipython-9.15.0.tar.gz", hash = "sha256:da2819ce2aa83135257df830660b1176d986c3d2876db24df01974fa955b2756", size = 4442580, upload-time = "2026-06-26T11:03:35.913Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/86/3060e8029b7cc505cce9a0137431dda81d0a3fde93a8f0f50ee0bf37a795/ipython-9.13.0-py3-none-any.whl", hash = "sha256:57f9d4639e20818d328d287c7b549af3d05f12486ea8f2e7f73e52a36ec4d201", size = 627274, upload-time = "2026-04-24T12:24:53.038Z" }, + { url = "https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl", hash = "sha256:515ad9c3cdf0c932a5a9f6245419e8aba706b7bd03c3e1d3a1c83d9351d6aa6e", size = 630895, upload-time = "2026-06-26T11:03:33.809Z" }, ] [[package]] @@ -851,7 +836,7 @@ wheels = [ [[package]] name = "jupyter-client" -version = "8.8.0" +version = "8.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-core" }, @@ -859,10 +844,11 @@ dependencies = [ { name = "pyzmq" }, { name = "tornado" }, { name = "traitlets" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/05/e4/ba649102a3bc3fbca54e7239fb924fd434c766f855693d86de0b1f2bec81/jupyter_client-8.8.0.tar.gz", hash = "sha256:d556811419a4f2d96c869af34e854e3f059b7cc2d6d01a9cd9c85c267691be3e", size = 348020, upload-time = "2026-01-08T13:55:47.938Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/dc/5512503b088997c2250b8bf18258fba9d9ce5ead641183700960d3c9d342/jupyter_client-8.9.1.tar.gz", hash = "sha256:a58f730dd9e728ba16ba1d62ebccf7ffe1ebbdbce4e95cfae941b7321ae1f4fa", size = 359256, upload-time = "2026-06-09T13:15:01.033Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl", hash = "sha256:f93a5b99c5e23a507b773d3a1136bd6e16c67883ccdbd9a829b0bbdb98cd7d7a", size = 107371, upload-time = "2026-01-08T13:55:45.562Z" }, + { url = "https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl", hash = "sha256:0b7a295bc46e8751e9adae84781f726c851c1d911bd793edc4a3bde942e3da81", size = 109828, upload-time = "2026-06-09T13:14:58.835Z" }, ] [[package]] @@ -1041,7 +1027,7 @@ wheels = [ [[package]] name = "matplotlib" -version = "3.10.9" +version = "3.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "contourpy" }, @@ -1054,43 +1040,43 @@ dependencies = [ { name = "pyparsing" }, { name = "python-dateutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/1b/4be5be87d43d327a0cf4de1a56e86f7f84c89312452406cf122efe2839e6/matplotlib-3.10.9.tar.gz", hash = "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358", size = 34811233, upload-time = "2026-04-24T00:14:13.539Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/35/c6/5581e26c72233ebb2a2a6fed2d24fb7c66b4700120b813f51b0555acf0b6/matplotlib-3.10.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f0c3c28d9fbcc1fe7a03be236d73430cf6409c41fb2383a7ac52fe932b072cb1", size = 8319908, upload-time = "2026-04-24T00:12:21.323Z" }, - { url = "https://files.pythonhosted.org/packages/b7/18/4880dd762e40cd360c1bf06e890c5a97b997e91cb324602b1a19950ad5ce/matplotlib-3.10.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cb28c2bd769aa3e98322c6ab09854cbcc52ab69d2759d681bba3e327b2b320", size = 8216016, upload-time = "2026-04-24T00:12:23.4Z" }, - { url = "https://files.pythonhosted.org/packages/32/91/d024616abdba99e83120e07a20658976f6a343646710760c4a51df126029/matplotlib-3.10.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae20801130378b82d647ff5047c07316295b68dc054ca6b3c13519d0ea624285", size = 8789336, upload-time = "2026-04-24T00:12:26.096Z" }, - { url = "https://files.pythonhosted.org/packages/5c/04/030a2f61ef2158f5e4c259487a92ac877732499fb33d871585d89e03c42d/matplotlib-3.10.9-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c63ebcd8b4b169eb2f5c200552ae6b8be8999a005b6b507ed76fb8d7d674fe2", size = 9604602, upload-time = "2026-04-24T00:12:29.052Z" }, - { url = "https://files.pythonhosted.org/packages/fc/c2/541e4d09d87bb6b5830fc28b4c887a9a8cf4e1c6cee698a8c05552ae2003/matplotlib-3.10.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d75d11c949914165976c621b2324f9ef162af7ebf4b057ddf95dd1dba7e5edcf", size = 9670966, upload-time = "2026-04-24T00:12:32.131Z" }, - { url = "https://files.pythonhosted.org/packages/04/a1/4571fc46e7702de8d0c2dc54ad1b2f8e29328dea3ee90831181f7353d93c/matplotlib-3.10.9-cp312-cp312-win_amd64.whl", hash = "sha256:d091f9d758b34aaaaa6331d13574bf01891d903b3dec59bfff458ef7551de5d6", size = 8217462, upload-time = "2026-04-24T00:12:35.226Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d0/2269edb12aa30c13c8bcc9382892e39943ce1d28aab4ec296e0381798e81/matplotlib-3.10.9-cp312-cp312-win_arm64.whl", hash = "sha256:10cc5ce06d10231c36f40e875f3c7e8050362a4ee8f0ee5d29a6b3277d57bb42", size = 8136688, upload-time = "2026-04-24T00:12:37.442Z" }, - { url = "https://files.pythonhosted.org/packages/aa/d3/8d4f6afbecb49fc04e060a57c0fce39ea51cc163a6bd87303ccd698e4fa6/matplotlib-3.10.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b580440f1ff81a0e34122051a3dfabb7e4b7f9e380629929bde0eff9af72165f", size = 8320331, upload-time = "2026-04-24T00:12:39.688Z" }, - { url = "https://files.pythonhosted.org/packages/63/d9/9e14bc7564bf92d5ffa801ae5fac819ce74b925dfb55e3ebde61a3bbad3e/matplotlib-3.10.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b1b745c489cd1a77a0dc1120a05dc87af9798faebc913601feb8c73d89bf2d1e", size = 8216461, upload-time = "2026-04-24T00:12:42.494Z" }, - { url = "https://files.pythonhosted.org/packages/8a/17/4402d0d14ccf1dfc70932600b68097fbbf9c898a4871d2cbbe79c7801a32/matplotlib-3.10.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8f3bcac1ca5ed000a6f4337d47ba67dfddf37ed6a46c15fd7f014997f7bf865f", size = 8790091, upload-time = "2026-04-24T00:12:44.789Z" }, - { url = "https://files.pythonhosted.org/packages/3e/0b/322aeec06dd9b91411f92028b37d447342770a24392aa4813e317064dad5/matplotlib-3.10.9-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a8d66a55def891c33147ba3ba9bfcabf0b526a43764c818acbb4525e5ed0838", size = 9605027, upload-time = "2026-04-24T00:12:47.583Z" }, - { url = "https://files.pythonhosted.org/packages/74/88/5f13482f55e7b00bcfc09838b093c2456e1379978d2a146844aae05350ad/matplotlib-3.10.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d843374407c4017a6403b59c6c81606773d136f3259d5b6da3131bc814542cc2", size = 9671269, upload-time = "2026-04-24T00:12:50.878Z" }, - { url = "https://files.pythonhosted.org/packages/c5/e0/0840fd2f93da988ec660b8ad1984abe9f25d2aed22a5e394ff1c68c88307/matplotlib-3.10.9-cp313-cp313-win_amd64.whl", hash = "sha256:f4399f64b3e94cd500195490972ae1ee81170df1636fa15364d157d5bdd7b921", size = 8217588, upload-time = "2026-04-24T00:12:53.784Z" }, - { url = "https://files.pythonhosted.org/packages/47/b9/d706d06dd605c49b9f83a2aed8c13e3e5db70697d7a80b7e3d7915de6b17/matplotlib-3.10.9-cp313-cp313-win_arm64.whl", hash = "sha256:ba7b3b8ef09eab7df0e86e9ae086faa433efbfbdb46afcb3aa16aabf779469a8", size = 8136913, upload-time = "2026-04-24T00:12:56.501Z" }, - { url = "https://files.pythonhosted.org/packages/9b/45/6e32d96978264c8ca8c4b1010adb955a1a49cfaf314e212bbc8908f04a61/matplotlib-3.10.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:09218df8a93712bd6ea133e83a153c755448cf7868316c531cffcc43f69d1cc9", size = 8368019, upload-time = "2026-04-24T00:12:58.896Z" }, - { url = "https://files.pythonhosted.org/packages/86/0a/c8e3d3bba245f0f7fc424937f8ff7ef77291a36af3edb97ccd78aa93d84f/matplotlib-3.10.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:82368699727bfb7b0182e1aa13082e3c08e092fa1a25d3e1fd92405bff96f6d4", size = 8264645, upload-time = "2026-04-24T00:13:01.406Z" }, - { url = "https://files.pythonhosted.org/packages/3d/aa/5bf5a14fe4fed73a4209a155606f8096ff797aad89c6c35179026571133e/matplotlib-3.10.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3225f4e1edcb8c86c884ddf79ebe20ecd0a67d30188f279897554ccd8fded4dc", size = 8802194, upload-time = "2026-04-24T00:13:03.702Z" }, - { url = "https://files.pythonhosted.org/packages/dd/5e/b4be852d6bba6fd15893fadf91ff26ae49cb91aac789e95dde9d342e664f/matplotlib-3.10.9-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de2445a0c6690d21b7eb6ce071cebad6d40a2e9bdf10d039074a96ba19797b99", size = 9622684, upload-time = "2026-04-24T00:13:06.647Z" }, - { url = "https://files.pythonhosted.org/packages/4c/3d/ed428c971139112ef730f62770654d609467346d09d4b62617e1afd68a5a/matplotlib-3.10.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b2b9516251cb89ff618d757daec0e2ed1bf21248013844a853d87ef85ab3081d", size = 9680790, upload-time = "2026-04-24T00:13:10.009Z" }, - { url = "https://files.pythonhosted.org/packages/e7/09/052e884aaf2b985c63cb79f715f1d5b6a3eaa7de78f6a52b9dbc077d5b53/matplotlib-3.10.9-cp313-cp313t-win_amd64.whl", hash = "sha256:e9fae004b941b23ff2edcf1567a857ed77bafc8086ffa258190462328434faf8", size = 8287571, upload-time = "2026-04-24T00:13:13.087Z" }, - { url = "https://files.pythonhosted.org/packages/f4/38/ae27288e788c35a4250491422f3db7750366fc8c97d6f36fbdecfc1f5518/matplotlib-3.10.9-cp313-cp313t-win_arm64.whl", hash = "sha256:6b63d9c7c769b88ab81e10dc86e4e0607cf56817b9f9e6cf24b2a5f1693b8e38", size = 8188292, upload-time = "2026-04-24T00:13:15.546Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e6/3bd8afd04949f02eabc1c17115ea5255e19cacd4d06fc5abdde4eeb0052c/matplotlib-3.10.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:172db52c9e683f5d12eaf57f0f54834190e12581fe1cc2a19595a8f5acb4e77d", size = 8321276, upload-time = "2026-04-24T00:13:18.318Z" }, - { url = "https://files.pythonhosted.org/packages/41/86/86231232fff41c9f8e4a1a7d7a597d349a02527109c3af7d618366122139/matplotlib-3.10.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97e35e8d39ccc85859095e01a53847432ba9a53ddf7986f7a54a11b73d0e143f", size = 8218218, upload-time = "2026-04-24T00:13:20.974Z" }, - { url = "https://files.pythonhosted.org/packages/85/8f/becc9722cafc64f5d2eb0b7c1bf5f585271c618a45dbd8fabeb021f898b6/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aba1615dabe83188e19d4f75a253c6a08423e04c1425e64039f800050a69de6b", size = 9608145, upload-time = "2026-04-24T00:13:23.228Z" }, - { url = "https://files.pythonhosted.org/packages/32/5d/f7e914f7d9325abff4057cee62c0fa70263683189f774473cbfb534cd13b/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34cf8167e023ad956c15f36302911d5406bd99a9862c1a8499ea6f7c0e015dc2", size = 9885085, upload-time = "2026-04-24T00:13:25.849Z" }, - { url = "https://files.pythonhosted.org/packages/a5/fd/fa69f2221534e80cc5772ac2b7d222011a2acafc2ec7216d5dd174c864ae/matplotlib-3.10.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59476c6d29d612b8e9bb6ce8c5b631be6ba8f9e3a2421f22a02b192c7dd28716", size = 9672358, upload-time = "2026-04-24T00:13:28.906Z" }, - { url = "https://files.pythonhosted.org/packages/ab/1a/5a4f747a8b271cbb024946d2dd3c913ab5032ba430626f8c3528ada96b4b/matplotlib-3.10.9-cp314-cp314-win_amd64.whl", hash = "sha256:336b9acc64d309063126edcdaca00db9373af3c476bb94388fe9c5a53ad13e6f", size = 8349970, upload-time = "2026-04-24T00:13:31.904Z" }, - { url = "https://files.pythonhosted.org/packages/64/dc/95d60ecaefe30680a154b52ea96ab4b0dab547f1fd6aa12f5fb655e89cae/matplotlib-3.10.9-cp314-cp314-win_arm64.whl", hash = "sha256:2dc9477819ffd78ad12a20df1d9d6a6bd4fec6aaa9072681465fddca052f1456", size = 8272785, upload-time = "2026-04-24T00:13:34.511Z" }, - { url = "https://files.pythonhosted.org/packages/70/a0/005d68bc8b8418300ce6591f18586910a8526806e2ab663933d9f20a41e9/matplotlib-3.10.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:da4e09638420548f31c354032a6250e473c68e5a4e96899b4844cf39ddea23fe", size = 8367999, upload-time = "2026-04-24T00:13:36.962Z" }, - { url = "https://files.pythonhosted.org/packages/22/05/1236cc9290be70b2498af20ca348add76e3fffe7f67b477db5133a84f3ea/matplotlib-3.10.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:345f6f68ecc8da0ca56fad2ea08fde1a115eda530079eca185d50a7bc3e146c6", size = 8264543, upload-time = "2026-04-24T00:13:39.851Z" }, - { url = "https://files.pythonhosted.org/packages/cd/c2/071f5a5ff6c5bd63aaaf2f45c811d9bf2ced94bde188d9e1a519e21d0cba/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4edcfbd8565339aa62f1cd4012f7180926fdbe71850f7b0d3c379c175cd6b66c", size = 9622800, upload-time = "2026-04-24T00:13:42.296Z" }, - { url = "https://files.pythonhosted.org/packages/95/57/da7d1f10a85624b9e7db68e069dd94e58dc41dbf9463c5921632ecbe3661/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6be157fe17fc37cb95ac1d7374cf717ce9259616edec911a78d9d26dae8522d4", size = 9888561, upload-time = "2026-04-24T00:13:45.026Z" }, - { url = "https://files.pythonhosted.org/packages/67/b2/ef8d6bb59b0edb6c16c968b70f548aa13b54348972def5aa6ac85df67145/matplotlib-3.10.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4e42042d54db34fda4e95a7bd3e5789c2a995d2dad3eb8850232ee534092fbbf", size = 9680884, upload-time = "2026-04-24T00:13:48.066Z" }, - { url = "https://files.pythonhosted.org/packages/61/1c/d21bfeb9931881ebe96bcfcff27c7ae4b160ae0ec291a714c42641a56d75/matplotlib-3.10.9-cp314-cp314t-win_amd64.whl", hash = "sha256:c27df8b3848f32a83d1767566595e43cfaa4460380974da06f4279a7ec143c39", size = 8432333, upload-time = "2026-04-24T00:13:51.008Z" }, - { url = "https://files.pythonhosted.org/packages/78/23/92493c3e6e1b635ccfff146f7b99e674808787915420373ac399283764c2/matplotlib-3.10.9-cp314-cp314t-win_arm64.whl", hash = "sha256:a49f1eadc84ca85fd72fa4e89e70e61bf86452df6f971af04b12c60761a0772c", size = 8324785, upload-time = "2026-04-24T00:13:53.633Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/1f/24/080c99d223d158d3a8902769269ab6da5b50f7a0e6e072513907e02b7a6c/matplotlib-3.11.0.tar.gz", hash = "sha256:68c0c7be01b30dcca3638934f7f591df73401235cbdbf0d1ab1c71e7db7f8b57", size = 33251176, upload-time = "2026-06-12T02:29:15.508Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/17/f5276b496c61477a6c4fc5e7401f4bfe1c2e5ef7c6cd67896f2ade3809cb/matplotlib-3.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:06b5872e9cf11adc8f589ded3ce11bc3e1061ad498259664fabc1f6615beb918", size = 9449976, upload-time = "2026-06-12T02:27:50.989Z" }, + { url = "https://files.pythonhosted.org/packages/82/34/bdd77418adb2178a1d59f044bd67bfebb115896e91b840b8a197eb3f4f4e/matplotlib-3.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0515d495124be3124340e59f164d901ed4484e2246a5b74cfa483cac3b80bd97", size = 9279307, upload-time = "2026-06-12T02:27:53.247Z" }, + { url = "https://files.pythonhosted.org/packages/94/95/7f522393c88313336b20d70fc849555757b2e5febc22b83b3a3f0fd4bce9/matplotlib-3.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be5f93a1d21981bfb802ded0d77a0caa92d4342a47d45754fac77e314a506344", size = 10031353, upload-time = "2026-06-12T02:27:55.215Z" }, + { url = "https://files.pythonhosted.org/packages/87/ce/8f25a0e3186aefd61913e7467d1b999465bcd0d0c03ac695c1b26ca559b7/matplotlib-3.11.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41635d7909d19e52e924a521dde6d8f670b0f53ab1d0e8c331fa831554f681d1", size = 10839232, upload-time = "2026-06-12T02:27:57.746Z" }, + { url = "https://files.pythonhosted.org/packages/85/c2/db15da2bbdf9e3ca66df7db8e2c33a1dfed67be24a24d2c878efaaff01d6/matplotlib-3.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:94f5000f67ca9faa300863ea17f8bce9175cb67b88bec4bc7780502d53dd7c9e", size = 10923899, upload-time = "2026-06-12T02:28:00.223Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2f/a58a4443a4d052a4ea77557478336aefc26c7981f6408d37adba763aa758/matplotlib-3.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ac6f1ef39f3d0f9e2463303013094992cdbe0f85f43bc54155bc472b2042768e", size = 9329528, upload-time = "2026-06-12T02:28:02.27Z" }, + { url = "https://files.pythonhosted.org/packages/61/0f/4b669589d47733b97ab9df4b58d6fc1e68acb5ea42a928dc7cbdd6bf5871/matplotlib-3.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:9dd11fb612ce7bc60b1de5b4fc87ff959d22317b5de42aabf392f66f97af22eb", size = 9003413, upload-time = "2026-06-12T02:28:04.49Z" }, + { url = "https://files.pythonhosted.org/packages/55/41/aa47f156b061d14c98b906f76c428507397708ec63ff94f410ae1752b426/matplotlib-3.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ce3b839b34ae1f430b4616893a2945a2999debaa7e94e7e29a2a8bbf286f7b5", size = 9450532, upload-time = "2026-06-12T02:28:06.769Z" }, + { url = "https://files.pythonhosted.org/packages/8c/4f/5a9eb0375e81413953febf8af7b012a6b6357f53438a15c4f5ad86c6bbb5/matplotlib-3.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:373db8f91214e8ccaf35ac833cc1dd59dd961e148bbd55dd027141591dde1313", size = 9279760, upload-time = "2026-06-12T02:28:09.152Z" }, + { url = "https://files.pythonhosted.org/packages/a4/c0/1117d53077e3ac3152503a84e9cf7a5c239576805ee71276e80c2aaa7471/matplotlib-3.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be152b7570324dc8d01574cc9474dd2d803237acf528bcbb5b211fa347461a09", size = 10031623, upload-time = "2026-06-12T02:28:11.26Z" }, + { url = "https://files.pythonhosted.org/packages/92/7e/e937138daffad65b71bf831a377809dcbc830fb4f31a31e067dc1faa2575/matplotlib-3.11.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:126f256df600652d7e4b394cf3164ff75210a00038f287c95a012a6f58d0e83f", size = 10839372, upload-time = "2026-06-12T02:28:14.102Z" }, + { url = "https://files.pythonhosted.org/packages/1d/c2/438ecc197ffb8023b6b9922915542f2172f5fd45b76703b0b4fc47322243/matplotlib-3.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:03acfeddf87b0dddb11b081ef7740ad445a3ca8bcb6b8e3011b08f2cf802b75c", size = 10924099, upload-time = "2026-06-12T02:28:16.383Z" }, + { url = "https://files.pythonhosted.org/packages/40/2e/395883da416f378b3ed2c9f3e843ac477eae1ce731b671b79adaa6f0bacd/matplotlib-3.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:ab3722f04f3ff34c23b5012c5873d2894174e06c3822fcdac3610965a5ac7d06", size = 9329727, upload-time = "2026-06-12T02:28:18.581Z" }, + { url = "https://files.pythonhosted.org/packages/61/82/2c388956abf8bf392dfb5b8917c502f1082df6a941b781ab8c8e5ba2474b/matplotlib-3.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:c945824670fb8915b4ac879e5e61f3c58e0913022f70a0de4c082b17372f8771", size = 9003506, upload-time = "2026-06-12T02:28:20.474Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c1/34454baa44da7975ada82e9aea37105ec47059514dc967d3be14426ba8dc/matplotlib-3.11.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3489c3dc487669b4a980bc3068f87856de7a1564248d3f6c629efb2a58b03f24", size = 9499838, upload-time = "2026-06-12T02:28:22.713Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c3/98fe79a398cf232219f090163a7fa7e6766e9f2e0ad26df54d6f8934d8ee/matplotlib-3.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6a98f5476ce784a50ce09998f4ae1e6a9f25043cef8a480c98949902eda74620", size = 9332298, upload-time = "2026-06-12T02:28:24.796Z" }, + { url = "https://files.pythonhosted.org/packages/95/e4/b4b7c33151e74e5c802f3cde1ba807ebfc38401e329b44e215a5888dd76d/matplotlib-3.11.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:565af866fd63e4bd3f987d580afe27c44c2552a3b3305f4ecbb85133601ea6f3", size = 10045491, upload-time = "2026-06-12T02:28:27.141Z" }, + { url = "https://files.pythonhosted.org/packages/71/28/394548efd68354110c1a1be11fe6b6e559e06d1a23da35908a0e316c55a9/matplotlib-3.11.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e6b3e64dea5062c570f04358e2711859f3531b459f29516274fbad889079e4f3", size = 10857059, upload-time = "2026-06-12T02:28:29.222Z" }, + { url = "https://files.pythonhosted.org/packages/c8/44/e7922e6e2a4d63bdfbc9dc4a53e3850ab438d46cf42e6779bb15ec92c948/matplotlib-3.11.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:942b37c5db1899610bd1543ce8e13e4ecff9a4633e7f63bb6aa9205d2644ebd1", size = 10939576, upload-time = "2026-06-12T02:28:31.66Z" }, + { url = "https://files.pythonhosted.org/packages/3d/be/b1ca96003a441d619b727fee21d671fdff7a5ce2f1bb797b2521aa2f679a/matplotlib-3.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:c08e649a6313e1291e713623b97a38e5bb4aa580b2a100a94a3309bc6b9c8eb3", size = 9379519, upload-time = "2026-06-12T02:28:33.888Z" }, + { url = "https://files.pythonhosted.org/packages/e3/72/4bf3b91821c34596dd6a7bdac5836d94f744144c8208939ef49d8ec43f7e/matplotlib-3.11.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2746cd2c113742ff6ce37a864c5ac5fd7aa644568f445e66166e457ac78e40e0", size = 9055456, upload-time = "2026-06-12T02:28:35.878Z" }, + { url = "https://files.pythonhosted.org/packages/57/52/a94102ac99eb78e2fe9b826674f9ef9ee23327110ea6ab4776c1b4eb6209/matplotlib-3.11.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3338e3e3de128cf50d0d2fb92a122815daf9c755bd882a474343c05f8fd7ec79", size = 9452137, upload-time = "2026-06-12T02:28:37.93Z" }, + { url = "https://files.pythonhosted.org/packages/7c/03/b8cdb625a21f710dfa11bbca1f48fb4057d2c0286975f8b415bf80942c99/matplotlib-3.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:25c2e5455efd8d99f41fb79871a31feb7d301569642e332ec58d72cfe9282bc3", size = 9281514, upload-time = "2026-06-12T02:28:40.028Z" }, + { url = "https://files.pythonhosted.org/packages/b7/2d/4e1240ea82ee197dfb3851e71f71c87eeeb975f1753b56a0588e4e80739a/matplotlib-3.11.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9695457a467ff86d23f35037a43deb6f1134dd6d3e2ac8ce1e2087cff09ffb9", size = 10843005, upload-time = "2026-06-12T02:28:42.39Z" }, + { url = "https://files.pythonhosted.org/packages/29/dc/6377ecfaa5fef79430f74a1a16638b4e2aa30d4692bae2c19f9d76fe3b01/matplotlib-3.11.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19c16c61dea63b3582918503e6b294193961261d9daa806d4ae2151f1ad05430", size = 11127459, upload-time = "2026-06-12T02:28:44.483Z" }, + { url = "https://files.pythonhosted.org/packages/6f/41/795c405aa7560443a3b01309424cde4a1113b85c90b8a63417444a749617/matplotlib-3.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2d72ea8b7924f3cb955e61518d21e43b3df1e6c8a793b480a0c1214f185d30ba", size = 10925160, upload-time = "2026-06-12T02:28:46.564Z" }, + { url = "https://files.pythonhosted.org/packages/1a/f7/3a9e6389a7cfaeff76c56e40c2dabcb13110e21e82f837228c834ebe748c/matplotlib-3.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:1c02da0a629dfa9debf52725ea06866b74c1fb70a895bae05e4493d34074f9f2", size = 9485186, upload-time = "2026-06-12T02:28:49.344Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c0/396478ee7cf2091d182db8b4a8695f6a37f1ddb978989cf9dbb84cd5c123/matplotlib-3.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aa55d73b3117d4b07f959cd9eb6f69b375d8df3414139c479388e551aa5d999d", size = 9160349, upload-time = "2026-06-12T02:28:51.382Z" }, + { url = "https://files.pythonhosted.org/packages/c5/6f/1c3bd51bb2b34eaacdcf3c3d859dbb357f952fc8020c617dc118ad7c9e38/matplotlib-3.11.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a9d8c6e7cd2f0ddf11d8d92e520dd1d9d2abb0cf6ac8831e338666c81e905847", size = 9500921, upload-time = "2026-06-12T02:28:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/4d861d0121840cb1a3fd4a10deb211efd6fccd481ed23e553f31f4f4da4a/matplotlib-3.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:be050fcf32f729eda99f7f75a80bf67612ce16ab9ac1c23a387dcaede95cb70e", size = 9332190, upload-time = "2026-06-12T02:28:55.623Z" }, + { url = "https://files.pythonhosted.org/packages/4b/cb/22f6bc35711a0b5639a784e74e653e77c86210bd4304449dd399a482f74e/matplotlib-3.11.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfabef0230d0697aa0d717385194dd41162e00207a68bf4abf94c2bf4c27dca0", size = 10854181, upload-time = "2026-06-12T02:28:57.856Z" }, + { url = "https://files.pythonhosted.org/packages/3f/7e/9a9eaca731a2939589da520f0ebe8fd8753d0f51fca98c7d20af6dbe261a/matplotlib-3.11.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1644db30e759199443493ac5e5caec24fdb775a8f6123021f85ba47c4133c3cb", size = 11137715, upload-time = "2026-06-12T02:29:00.555Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f9/9b030b6088354acb0296871bb624b25befc1c42509d3c6cd17420c83a5b8/matplotlib-3.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:15b0d160079cb10699a0e98b5989c70677b2df7cacdc62af67c30f2facec46d9", size = 10939427, upload-time = "2026-06-12T02:29:02.527Z" }, + { url = "https://files.pythonhosted.org/packages/59/94/6b273eaee4ee250863567d100865da61a5c1527fa67f527b7ed22e0dd29c/matplotlib-3.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:446307e6b04b57b1f1239e228a1ec2af0d589a1008cebc3dfa3f5441d095cfb6", size = 9535809, upload-time = "2026-06-12T02:29:04.994Z" }, + { url = "https://files.pythonhosted.org/packages/60/95/1d36bddf2b7e2692c1540e78a6e5bc88bc1496b137e3e35a611f91b65ac3/matplotlib-3.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:652fb5696271d4c50f196d22a5ff4f8e4444c74f847423570d7dc0aa2bbd0159", size = 9209226, upload-time = "2026-06-12T02:29:07.033Z" }, ] [[package]] @@ -1124,73 +1110,63 @@ wheels = [ ] [[package]] -name = "nest-asyncio" -version = "1.6.0" +name = "nest-asyncio2" +version = "1.7.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/73/731debf26e27e0a0323d7bda270dc2f634b398e38f040a09da1f4351d0aa/nest_asyncio2-1.7.2.tar.gz", hash = "sha256:1921d70b92cc4612c374928d081552efb59b83d91b2b789d935c665fa01729a8", size = 14743, upload-time = "2026-02-13T00:34:04.386Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3c/3179b85b0e1c3659f0369940200cd6d0fa900e6cefcc7ea0bc6dd0e29ffb/nest_asyncio2-1.7.2-py3-none-any.whl", hash = "sha256:f5dfa702f3f81f6a03857e9a19e2ba578c0946a4ad417b4c50a24d7ba641fe01", size = 7843, upload-time = "2026-02-13T00:34:02.691Z" }, ] [[package]] name = "numpy" -version = "2.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" }, - { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" }, - { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" }, - { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" }, - { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" }, - { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" }, - { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" }, - { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" }, - { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" }, - { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" }, - { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" }, - { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" }, - { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" }, - { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" }, - { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" }, - { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" }, - { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" }, - { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" }, - { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" }, - { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" }, - { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" }, - { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" }, - { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" }, - { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" }, - { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" }, - { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" }, - { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" }, - { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" }, - { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" }, - { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" }, - { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" }, - { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" }, - { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" }, - { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" }, - { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" }, - { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" }, - { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" }, - { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" }, - { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" }, - { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" }, - { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" }, - { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" }, - { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" }, - { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" }, - { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" }, - { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" }, - { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" }, - { url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" }, - { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" }, - { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" }, +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/05/3d27272d30698dc0ecb7fdfaa41ad70303b444f81722bb99bce1d818638a/numpy-2.5.0.tar.gz", hash = "sha256:5a129578019311b6e56bdd714250f19b518f7dceeeb8d1af5490f4942d3f891c", size = 20652461, upload-time = "2026-06-21T20:57:51.95Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/0a/11486d02add7b1384dff7374d124b1cfbb0ee864dcc9f6a2c0380638cf84/numpy-2.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:489780423903667933b4ed6197b6ec3b75ea5dd17d1d8f0f38d798feb6921561", size = 16789987, upload-time = "2026-06-21T20:56:16.657Z" }, + { url = "https://files.pythonhosted.org/packages/55/b2/285f48640a181947b4587a3766d21ec1eaa7fea833d4b49957e09da467a2/numpy-2.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ece55976ced6bca95a03ae2839e2e5ccffe8eb6a3e7022415645eb154a81e4e6", size = 11760322, upload-time = "2026-06-21T20:56:19.813Z" }, + { url = "https://files.pythonhosted.org/packages/dd/67/b032db1eb03ca30d16eda3b0c22aaa615338b9263c2fd559d0f29451aca4/numpy-2.5.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:c83b664b0e6eee9594fa920cf0639d8af796606d3fad6cc70180c87e4b97c7be", size = 5319605, upload-time = "2026-06-21T20:56:22.173Z" }, + { url = "https://files.pythonhosted.org/packages/b9/83/03fc7300c7c6b6c84c487b1dc80d322817b95fbd1f4dd57a85e23b7198de/numpy-2.5.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:bf80333980bf37f523341ddd72c783f39d6829ec7736b9eb99086388a2d52cc2", size = 6653628, upload-time = "2026-06-21T20:56:23.914Z" }, + { url = "https://files.pythonhosted.org/packages/82/49/2ec21730bc63ccfda829323f7040a8ed4715b3852ce658689cf74ee96a8c/numpy-2.5.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1a4874217b36d5ac8fc876f52e39df56f8182c88463e9e2dceabf7ca8b7efb8", size = 15153691, upload-time = "2026-06-21T20:56:25.631Z" }, + { url = "https://files.pythonhosted.org/packages/bb/6b/f4a3d0637692c49da8ef99d72d52526f92e0a8d6ac4f0ca9f31441b9d9ea/numpy-2.5.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aaa760137137e8d3c920d27927748215b56014f92667dc9b6c27dfc61249255a", size = 16660066, upload-time = "2026-06-21T20:56:28.009Z" }, + { url = "https://files.pythonhosted.org/packages/3a/2f/c354ec86d1f3f5c19649463b0d39652e160736e5b0a4cd18dff0576715c4/numpy-2.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7174ce8265fc7f7417d171c9ea8fe905220748893ea67a2a7abe726ec331c4b0", size = 16514638, upload-time = "2026-06-21T20:56:30.26Z" }, + { url = "https://files.pythonhosted.org/packages/06/34/43efdcb319988648580f93c11f1ae82cf7e2faa74925e98e454ae3aa95f8/numpy-2.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b8c3daaf99de52415d20b42f8e8155c78642cb04207d02f9d317a0dcf1b3fb54", size = 18419647, upload-time = "2026-06-21T20:56:32.41Z" }, + { url = "https://files.pythonhosted.org/packages/71/e2/f5d1676b1d7fb682eb5e9a1641e7ebd2414b3216c370661d1029778908b4/numpy-2.5.0-cp312-cp312-win32.whl", hash = "sha256:6206db0af545d73d068add6d992279145f158428d1da6cc49adc4b630c5d6ee5", size = 6056688, upload-time = "2026-06-21T20:56:34.657Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7c/48f115d1c58a34032facebcd51fdf2d02df2c51d4a46a81dd1197bb2ea6b/numpy-2.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:6f2d6873e2940c860a309d21e25b1e69af6aaffdd80aa056b04c16380db1c4f2", size = 12419237, upload-time = "2026-06-21T20:56:36.24Z" }, + { url = "https://files.pythonhosted.org/packages/86/26/2e0882f4044d1b1a1b63e875151fb2393389032022a8b7f5657a7996d3b2/numpy-2.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:a55e1eb2bca2cfd17a16b213c99dfc8502d47b0d494224d2122277d0400935ca", size = 10339912, upload-time = "2026-06-21T20:56:38.733Z" }, + { url = "https://files.pythonhosted.org/packages/8a/33/07675aaad7f26ea013d5e884d9a0d784b79c6bd7566c333f5a52fa3c610b/numpy-2.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:520e6b8be0a4b65840ac8090d4f51cef4bed66e2b0894d5a520f099adc24a9b2", size = 16784890, upload-time = "2026-06-21T20:56:40.799Z" }, + { url = "https://files.pythonhosted.org/packages/85/4b/953118a730ee3b35e28645e0eb4cf9beec5bdbb954e1ac2f5fcefba6bbc3/numpy-2.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:146b81cdd3967fdb6beca8ba25f00c58741d8f3cbd797f55af0fbe0bfec3469c", size = 11754584, upload-time = "2026-06-21T20:56:43.094Z" }, + { url = "https://files.pythonhosted.org/packages/44/9b/56dd530c367c74ae17411027cea4135ca57e1e0583bf5594cee18bd83217/numpy-2.5.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:126b88d95e8ff9b00c9e717aa540469f21d6180162f84c0caec51b16215d49cd", size = 5313904, upload-time = "2026-06-21T20:56:45.503Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b0/bcd672edad27ecca7da1f7bb0ce72cd1706a4f2d79ae94990afc97c13e1c/numpy-2.5.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d4313cef1594c5ce46c31b6e54e918338f63f16ee9322304e8c9114d6d81c8bd", size = 6648504, upload-time = "2026-06-21T20:56:47.567Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/15cdfcbd30a1544a46c9e487a00df331c4672450216538705a9e51fa6710/numpy-2.5.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:750fb097caf26fa878746d9d119f6f9da12dedcbff1eea966c3e3447647c4a9e", size = 15150086, upload-time = "2026-06-21T20:56:49.352Z" }, + { url = "https://files.pythonhosted.org/packages/32/4e/8d7656ccaab3e81e97258b8a9bc5f0c8502513a92fb4ceb0a2cbfebc17bf/numpy-2.5.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3893adc2dc7c0412ba76777db55a049215d99c9aa3113003be8f49f4f1290ab9", size = 16647250, upload-time = "2026-06-21T20:56:51.542Z" }, + { url = "https://files.pythonhosted.org/packages/3c/81/97060281b602ed07f21b12f4ec409eac1f75a2f91fbc829ed8b2becf3ad4/numpy-2.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:835e454dd99b238cdc5a3f63bce2371296f5ebc53ca1e0f8e6ddbb6d92a29aab", size = 16512864, upload-time = "2026-06-21T20:56:55.401Z" }, + { url = "https://files.pythonhosted.org/packages/33/ab/4496208146911f8d8ddb54f68a972aafa6c8d44babcb2ea03b0e5cc87c9d/numpy-2.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f9836778081a0a3c02a6a21493f3e9f5b311f8d2541934f31f05583dc999ea4", size = 18408407, upload-time = "2026-06-21T20:56:57.75Z" }, + { url = "https://files.pythonhosted.org/packages/d4/9f/a4df67c181e4ee8b467aa3332dc2db10fd5c515136831302f3ca48bc0a01/numpy-2.5.0-cp313-cp313-win32.whl", hash = "sha256:0b525be4744b60bb0557ac872d53ef07d085b5f39622bc579c98d3809d05b988", size = 6054431, upload-time = "2026-06-21T20:57:00.016Z" }, + { url = "https://files.pythonhosted.org/packages/30/53/491e1c47c55b62ccc6a63c1c5b8635c73fc2258dddeb9bda27cae4a0ae96/numpy-2.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:44353e2878930039db472b99dc353d749826e4010bd4d2a7f835e94a97a5c748", size = 12414420, upload-time = "2026-06-21T20:57:01.815Z" }, + { url = "https://files.pythonhosted.org/packages/eb/4a/25c2906f541e9d9f4c5769764db732e6627be91a13f4724fa10634d77db4/numpy-2.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:48f54b00711f83a5f796b70c518e8c2b3c5848dda03a54911f23eb68519b9b60", size = 10339533, upload-time = "2026-06-21T20:57:03.961Z" }, + { url = "https://files.pythonhosted.org/packages/86/ad/abc44aaceaf7b17ee1edde2bbb4458da591bc79574cffff50c4bb35f00d1/numpy-2.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f27582c55ba4c750b7c58c8faf021d2cd9324a662b466229db8a417b41368af9", size = 16783807, upload-time = "2026-06-21T20:57:06.253Z" }, + { url = "https://files.pythonhosted.org/packages/5d/39/b72e168daf9c00fb20c9fc996d00437ccecdef3102387775d29d7a62576d/numpy-2.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:28e7137057d551e4a83c4ae414e3451f50568409db7569aacc7f9811ee06a446", size = 11765215, upload-time = "2026-06-21T20:57:08.547Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a0/8400a9c0e3625182347593f5e1f57da9a617a534794805c8df5518154ddc/numpy-2.5.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:e1da54b53e75cd9fcfc23efcc7edab2c6aecf97b6037566d8a0fe804af8ec57c", size = 5324493, upload-time = "2026-06-21T20:57:11.012Z" }, + { url = "https://files.pythonhosted.org/packages/f6/8c/0d104deaa0401c93395a629ec902891618a2eff76d19229139cb5a887bfc/numpy-2.5.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:694d8f74e156f7fd01179f1aa8faa2f648ab6ae0f70b6c3fe57a03249aea2303", size = 6645211, upload-time = "2026-06-21T20:57:12.919Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d9/4a4a628c812750363786afc3d33492709a5cd64b215469c16b0f6c7bb811/numpy-2.5.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a7569a7b53c77716f036bb28cb1c91f166a26ec7d9502cd1e4bdfe502fdec22", size = 15166004, upload-time = "2026-06-21T20:57:14.717Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5e/2a902317d7fc4aa93236e80c932662dadfc459b323d758329e01775125e1/numpy-2.5.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39a0433bd4086ebd462960cf375e19195bb07b53dc1d87dd5fcf47ad78576f03", size = 16650797, upload-time = "2026-06-21T20:57:16.906Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a0/a0090e6329f4ca5992c07847bb579c5259a19953dc57255bb08793142ffb/numpy-2.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:929f0c79ac38bcbd7154fe631dc907abfeddbcc5027a896bd1f7767323271e7a", size = 16524647, upload-time = "2026-06-21T20:57:19.165Z" }, + { url = "https://files.pythonhosted.org/packages/5e/7d/6caf27734c42b65837e7461ed0dbbd6b6fc835060c9714ec59d673bb383a/numpy-2.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cc4f247a47bbf070bfd70be53ccdcf47b800af563535e7bbe172322197c30e21", size = 18411841, upload-time = "2026-06-21T20:57:21.638Z" }, + { url = "https://files.pythonhosted.org/packages/13/dc/26edadbd812536769a82c2e9e002234e33feb5da43061d47a044f6d309b7/numpy-2.5.0-cp314-cp314-win32.whl", hash = "sha256:5dc71423499fab3f46f7a7201155ade1669ea101f2f429d332df9e72f8161731", size = 6106361, upload-time = "2026-06-21T20:57:23.844Z" }, + { url = "https://files.pythonhosted.org/packages/f2/9e/4dd1459282229a72d92dece2ae9138e5cac94a72263a7ceb48f37434c925/numpy-2.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:ebb81d9d5443e0309d6c54894c3fbed74ad7da0714352a67b6d773cd189eae73", size = 12551749, upload-time = "2026-06-21T20:57:25.945Z" }, + { url = "https://files.pythonhosted.org/packages/05/a7/6bc6384c080b86c7f6c85c5bc5b540b24f4f679cd144791d99574e90d462/numpy-2.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:3b94d0d0deceebfad3e67ae5c0e5eb87371e8f7a0581cd04a779928c2450cf1e", size = 10617072, upload-time = "2026-06-21T20:57:28.175Z" }, + { url = "https://files.pythonhosted.org/packages/86/6b/4a2b71d66ada5608ae02b63f150dfad520f6940721cb7f029ad270befc0e/numpy-2.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:22f3d43e362d650bc39db1f17851302874a148ca95ba6981c1dfb5fa6862f35b", size = 11881067, upload-time = "2026-06-21T20:57:30.104Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b2/d365eb40a20efb49d67e9feb90494ed8511282ee1f5fa16006675c65397d/numpy-2.5.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:243563efb4cd7528a264567e9fd206c87826457322521d06206a00bfa316c927", size = 5440290, upload-time = "2026-06-21T20:57:32.193Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5e/e9c03188de5f9b767e46a8fe988bcfd3efad066a4a3fda8b9cb11a93f895/numpy-2.5.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:84881d825ca75249b189bbee875fcfe3238aa5c479e6100893cda566e8e86826", size = 6748371, upload-time = "2026-06-21T20:57:33.933Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1d/68c186a38a5027bae2c4ddd5ea681fdaf8b4d30fb7301def6d8ad270390f/numpy-2.5.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cda12aa4779d42b8771180aba759c96f527d43446d8f380ab59e2b35e8489efd", size = 15214643, upload-time = "2026-06-21T20:57:35.677Z" }, + { url = "https://files.pythonhosted.org/packages/8c/67/73f67b7c7e20635baae9c4c3ead4ae7326a005900297a6110971abd62eb5/numpy-2.5.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c0121101093d2bd74981b10f8837d78e794a8ff57834eb27179f49e1ba11ac6", size = 16690128, upload-time = "2026-06-21T20:57:38.159Z" }, + { url = "https://files.pythonhosted.org/packages/eb/05/d4c1fb0c46d02a27d6b2b8b319a78c90937acec8631c1641874670b31e6f/numpy-2.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d371c92cfa09da00022f501ab67fafaea813d752eb30ac44336d45b1e5b0268a", size = 16577902, upload-time = "2026-06-21T20:57:40.447Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1d/771c797d50fa26e4888989cccf1d50ee51f530d4e455ad2692dcb64fa711/numpy-2.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9990713e9c38154c6861e7547f1e3fc7a87e75ff09bab24ef1cc81d81c2835e9", size = 18452814, upload-time = "2026-06-21T20:57:42.875Z" }, + { url = "https://files.pythonhosted.org/packages/e8/46/52fc0d2a68d7643f0f149eeea5a5d8ea2a3507056ac8afa83c9212606e8b/numpy-2.5.0-cp314-cp314t-win32.whl", hash = "sha256:edadfbd4794b1086c0d822f81863e8a68fc129d132fd0bb9e31e955d7fbbbdb7", size = 6253168, upload-time = "2026-06-21T20:57:45.101Z" }, + { url = "https://files.pythonhosted.org/packages/2a/be/6c8d1118b5f13b2881dc095d5b345de19c6638b8959c17409b6eff84c8aa/numpy-2.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f7e5fa4382967ae6548bd2f174219afb908e294b0d5f625af01166edd5f7d9aa", size = 12736286, upload-time = "2026-06-21T20:57:46.935Z" }, + { url = "https://files.pythonhosted.org/packages/fd/6a/d3a169aaf8536cf228d56a09e04bcb713a2fe4410d4e2105b9419b5a9c89/numpy-2.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:016623417bb330d719d579daf2d6b9a01ddc52e41a9ed61a47f39fde46dcd865", size = 10686451, upload-time = "2026-06-21T20:57:49.313Z" }, ] [[package]] @@ -1213,11 +1189,11 @@ wheels = [ [[package]] name = "packaging" -version = "26.0" +version = "26.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] [[package]] @@ -1286,7 +1262,7 @@ name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess", marker = "sys_platform != 'emscripten' and 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" } wheels = [ @@ -1364,11 +1340,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.9.6" +version = "4.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, + { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" }, ] [[package]] @@ -1528,30 +1504,30 @@ wheels = [ [[package]] name = "pydantic-settings" -version = "2.14.1" +version = "2.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/07/60/1d1e59c9c90d54591469ada7d268251f71c24bdb765f1a8a832cee8c6653/pydantic_settings-2.14.1.tar.gz", hash = "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa", size = 235551, upload-time = "2026-05-08T13:40:06.542Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" }, + { url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" }, ] [[package]] name = "pydantic-yaml" -version = "1.6.0" +version = "1.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "ruamel-yaml" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/6c/6bc8f39406cdeed864578df88f52a63db27bd24aa8473206d650bc4fa1d8/pydantic_yaml-1.6.0.tar.gz", hash = "sha256:ce5f10b65d95ca45846a36ea8dae54e550fa3058e7d6218e0179184d9bf6f660", size = 25782, upload-time = "2025-08-08T21:01:13.097Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c1/c9/1952345a95c9699783ea138a0cc47a151189adbbc2be5106fdab3788eaab/pydantic_yaml-1.7.0.tar.gz", hash = "sha256:b7c9420647537166c58ff9b150176a434fbdbae843d076e37011dd7e3e2e0044", size = 96334, upload-time = "2026-06-21T14:11:04.492Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/39/8d263fbcb409a8f5dd78ac8f89f1e6af1d4e4d9fbb7f856ca3245b354809/pydantic_yaml-1.6.0-py3-none-any.whl", hash = "sha256:02cb800b455b68daeeb74ad736c252a94a0b203da5fbbeef02539d468e1d98f8", size = 22511, upload-time = "2025-08-08T21:01:11.425Z" }, + { url = "https://files.pythonhosted.org/packages/53/a5/198079eb0445eeaa28c732e2b87df7e2bf3452a8823a8774398b4104b674/pydantic_yaml-1.7.0-py3-none-any.whl", hash = "sha256:794867172285e444653605c16adfc4b874938ce60132eff24cda4e3c590f75f3", size = 25106, upload-time = "2026-06-21T14:11:03.192Z" }, ] [[package]] @@ -1574,7 +1550,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.0.3" +version = "9.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -1583,9 +1559,9 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] [[package]] @@ -1700,7 +1676,7 @@ wheels = [ [[package]] name = "requests" -version = "2.33.1" +version = "2.34.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -1708,9 +1684,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] [[package]] @@ -1737,77 +1713,36 @@ wheels = [ [[package]] name = "ruamel-yaml" -version = "0.18.17" +version = "0.19.1" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ruamel-yaml-clib", marker = "python_full_version < '3.15' and platform_python_implementation == 'CPython'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3a/2b/7a1f1ebcd6b3f14febdc003e658778d81e76b40df2267904ee6b13f0c5c6/ruamel_yaml-0.18.17.tar.gz", hash = "sha256:9091cd6e2d93a3a4b157ddb8fabf348c3de7f1fb1381346d985b6b247dcd8d3c", size = 149602, upload-time = "2025-12-17T20:02:55.757Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/af/fe/b6045c782f1fd1ae317d2a6ca1884857ce5c20f59befe6ab25a8603c43a7/ruamel_yaml-0.18.17-py3-none-any.whl", hash = "sha256:9c8ba9eb3e793efdf924b60d521820869d5bf0cb9c6f1b82d82de8295e290b9d", size = 121594, upload-time = "2025-12-17T20:02:07.657Z" }, -] - -[[package]] -name = "ruamel-yaml-clib" -version = "0.2.15" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ea/97/60fda20e2fb54b83a61ae14648b0817c8f5d84a3821e40bfbdae1437026a/ruamel_yaml_clib-0.2.15.tar.gz", hash = "sha256:46e4cc8c43ef6a94885f72512094e482114a8a706d3c555a34ed4b0d20200600", size = 225794, upload-time = "2025-11-16T16:12:59.761Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/72/4b/5fde11a0722d676e469d3d6f78c6a17591b9c7e0072ca359801c4bd17eee/ruamel_yaml_clib-0.2.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb15a2e2a90c8475df45c0949793af1ff413acfb0a716b8b94e488ea95ce7cff", size = 149088, upload-time = "2025-11-16T16:13:22.836Z" }, - { url = "https://files.pythonhosted.org/packages/85/82/4d08ac65ecf0ef3b046421985e66301a242804eb9a62c93ca3437dc94ee0/ruamel_yaml_clib-0.2.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:64da03cbe93c1e91af133f5bec37fd24d0d4ba2418eaf970d7166b0a26a148a2", size = 134553, upload-time = "2025-11-16T16:13:24.151Z" }, - { url = "https://files.pythonhosted.org/packages/b9/cb/22366d68b280e281a932403b76da7a988108287adff2bfa5ce881200107a/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f6d3655e95a80325b84c4e14c080b2470fe4f33b6846f288379ce36154993fb1", size = 737468, upload-time = "2025-11-16T20:22:47.335Z" }, - { url = "https://files.pythonhosted.org/packages/71/73/81230babf8c9e33770d43ed9056f603f6f5f9665aea4177a2c30ae48e3f3/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71845d377c7a47afc6592aacfea738cc8a7e876d586dfba814501d8c53c1ba60", size = 753349, upload-time = "2025-11-16T16:13:26.269Z" }, - { url = "https://files.pythonhosted.org/packages/61/62/150c841f24cda9e30f588ef396ed83f64cfdc13b92d2f925bb96df337ba9/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e5499db1ccbc7f4b41f0565e4f799d863ea720e01d3e99fa0b7b5fcd7802c9", size = 788211, upload-time = "2025-11-16T16:13:27.441Z" }, - { url = "https://files.pythonhosted.org/packages/30/93/e79bd9cbecc3267499d9ead919bd61f7ddf55d793fb5ef2b1d7d92444f35/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4b293a37dc97e2b1e8a1aec62792d1e52027087c8eea4fc7b5abd2bdafdd6642", size = 743203, upload-time = "2025-11-16T16:13:28.671Z" }, - { url = "https://files.pythonhosted.org/packages/8d/06/1eb640065c3a27ce92d76157f8efddb184bd484ed2639b712396a20d6dce/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:512571ad41bba04eac7268fe33f7f4742210ca26a81fe0c75357fa682636c690", size = 747292, upload-time = "2025-11-16T20:22:48.584Z" }, - { url = "https://files.pythonhosted.org/packages/a5/21/ee353e882350beab65fcc47a91b6bdc512cace4358ee327af2962892ff16/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5e9f630c73a490b758bf14d859a39f375e6999aea5ddd2e2e9da89b9953486a", size = 771624, upload-time = "2025-11-16T16:13:29.853Z" }, - { url = "https://files.pythonhosted.org/packages/57/34/cc1b94057aa867c963ecf9ea92ac59198ec2ee3a8d22a126af0b4d4be712/ruamel_yaml_clib-0.2.15-cp312-cp312-win32.whl", hash = "sha256:f4421ab780c37210a07d138e56dd4b51f8642187cdfb433eb687fe8c11de0144", size = 100342, upload-time = "2025-11-16T16:13:31.067Z" }, - { url = "https://files.pythonhosted.org/packages/b3/e5/8925a4208f131b218f9a7e459c0d6fcac8324ae35da269cb437894576366/ruamel_yaml_clib-0.2.15-cp312-cp312-win_amd64.whl", hash = "sha256:2b216904750889133d9222b7b873c199d48ecbb12912aca78970f84a5aa1a4bc", size = 119013, upload-time = "2025-11-16T16:13:32.164Z" }, - { url = "https://files.pythonhosted.org/packages/17/5e/2f970ce4c573dc30c2f95825f2691c96d55560268ddc67603dc6ea2dd08e/ruamel_yaml_clib-0.2.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dcec721fddbb62e60c2801ba08c87010bd6b700054a09998c4d09c08147b8fb", size = 147450, upload-time = "2025-11-16T16:13:33.542Z" }, - { url = "https://files.pythonhosted.org/packages/d6/03/a1baa5b94f71383913f21b96172fb3a2eb5576a4637729adbf7cd9f797f8/ruamel_yaml_clib-0.2.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:65f48245279f9bb301d1276f9679b82e4c080a1ae25e679f682ac62446fac471", size = 133139, upload-time = "2025-11-16T16:13:34.587Z" }, - { url = "https://files.pythonhosted.org/packages/dc/19/40d676802390f85784235a05788fd28940923382e3f8b943d25febbb98b7/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:46895c17ead5e22bea5e576f1db7e41cb273e8d062c04a6a49013d9f60996c25", size = 731474, upload-time = "2025-11-16T20:22:49.934Z" }, - { url = "https://files.pythonhosted.org/packages/ce/bb/6ef5abfa43b48dd55c30d53e997f8f978722f02add61efba31380d73e42e/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3eb199178b08956e5be6288ee0b05b2fb0b5c1f309725ad25d9c6ea7e27f962a", size = 748047, upload-time = "2025-11-16T16:13:35.633Z" }, - { url = "https://files.pythonhosted.org/packages/ff/5d/e4f84c9c448613e12bd62e90b23aa127ea4c46b697f3d760acc32cb94f25/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d1032919280ebc04a80e4fb1e93f7a738129857eaec9448310e638c8bccefcf", size = 782129, upload-time = "2025-11-16T16:13:36.781Z" }, - { url = "https://files.pythonhosted.org/packages/de/4b/e98086e88f76c00c88a6bcf15eae27a1454f661a9eb72b111e6bbb69024d/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab0df0648d86a7ecbd9c632e8f8d6b21bb21b5fc9d9e095c796cacf32a728d2d", size = 736848, upload-time = "2025-11-16T16:13:37.952Z" }, - { url = "https://files.pythonhosted.org/packages/0c/5c/5964fcd1fd9acc53b7a3a5d9a05ea4f95ead9495d980003a557deb9769c7/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:331fb180858dd8534f0e61aa243b944f25e73a4dae9962bd44c46d1761126bbf", size = 741630, upload-time = "2025-11-16T20:22:51.718Z" }, - { url = "https://files.pythonhosted.org/packages/07/1e/99660f5a30fceb58494598e7d15df883a07292346ef5696f0c0ae5dee8c6/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd4c928ddf6bce586285daa6d90680b9c291cfd045fc40aad34e445d57b1bf51", size = 766619, upload-time = "2025-11-16T16:13:39.178Z" }, - { url = "https://files.pythonhosted.org/packages/36/2f/fa0344a9327b58b54970e56a27b32416ffbcfe4dcc0700605516708579b2/ruamel_yaml_clib-0.2.15-cp313-cp313-win32.whl", hash = "sha256:bf0846d629e160223805db9fe8cc7aec16aaa11a07310c50c8c7164efa440aec", size = 100171, upload-time = "2025-11-16T16:13:40.456Z" }, - { url = "https://files.pythonhosted.org/packages/06/c4/c124fbcef0684fcf3c9b72374c2a8c35c94464d8694c50f37eef27f5a145/ruamel_yaml_clib-0.2.15-cp313-cp313-win_amd64.whl", hash = "sha256:45702dfbea1420ba3450bb3dd9a80b33f0badd57539c6aac09f42584303e0db6", size = 118845, upload-time = "2025-11-16T16:13:41.481Z" }, - { url = "https://files.pythonhosted.org/packages/3e/bd/ab8459c8bb759c14a146990bf07f632c1cbec0910d4853feeee4be2ab8bb/ruamel_yaml_clib-0.2.15-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:753faf20b3a5906faf1fc50e4ddb8c074cb9b251e00b14c18b28492f933ac8ef", size = 147248, upload-time = "2025-11-16T16:13:42.872Z" }, - { url = "https://files.pythonhosted.org/packages/69/f2/c4cec0a30f1955510fde498aac451d2e52b24afdbcb00204d3a951b772c3/ruamel_yaml_clib-0.2.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:480894aee0b29752560a9de46c0e5f84a82602f2bc5c6cde8db9a345319acfdf", size = 133764, upload-time = "2025-11-16T16:13:43.932Z" }, - { url = "https://files.pythonhosted.org/packages/82/c7/2480d062281385a2ea4f7cc9476712446e0c548cd74090bff92b4b49e898/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d3b58ab2454b4747442ac76fab66739c72b1e2bb9bd173d7694b9f9dbc9c000", size = 730537, upload-time = "2025-11-16T20:22:52.918Z" }, - { url = "https://files.pythonhosted.org/packages/75/08/e365ee305367559f57ba6179d836ecc3d31c7d3fdff2a40ebf6c32823a1f/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bfd309b316228acecfa30670c3887dcedf9b7a44ea39e2101e75d2654522acd4", size = 746944, upload-time = "2025-11-16T16:13:45.338Z" }, - { url = "https://files.pythonhosted.org/packages/a1/5c/8b56b08db91e569d0a4fbfa3e492ed2026081bdd7e892f63ba1c88a2f548/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2812ff359ec1f30129b62372e5f22a52936fac13d5d21e70373dbca5d64bb97c", size = 778249, upload-time = "2025-11-16T16:13:46.871Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1d/70dbda370bd0e1a92942754c873bd28f513da6198127d1736fa98bb2a16f/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7e74ea87307303ba91073b63e67f2c667e93f05a8c63079ee5b7a5c8d0d7b043", size = 737140, upload-time = "2025-11-16T16:13:48.349Z" }, - { url = "https://files.pythonhosted.org/packages/5b/87/822d95874216922e1120afb9d3fafa795a18fdd0c444f5c4c382f6dac761/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:713cd68af9dfbe0bb588e144a61aad8dcc00ef92a82d2e87183ca662d242f524", size = 741070, upload-time = "2025-11-16T20:22:54.151Z" }, - { url = "https://files.pythonhosted.org/packages/b9/17/4e01a602693b572149f92c983c1f25bd608df02c3f5cf50fd1f94e124a59/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:542d77b72786a35563f97069b9379ce762944e67055bea293480f7734b2c7e5e", size = 765882, upload-time = "2025-11-16T16:13:49.526Z" }, - { url = "https://files.pythonhosted.org/packages/9f/17/7999399081d39ebb79e807314de6b611e1d1374458924eb2a489c01fc5ad/ruamel_yaml_clib-0.2.15-cp314-cp314-win32.whl", hash = "sha256:424ead8cef3939d690c4b5c85ef5b52155a231ff8b252961b6516ed7cf05f6aa", size = 102567, upload-time = "2025-11-16T16:13:50.78Z" }, - { url = "https://files.pythonhosted.org/packages/d2/67/be582a7370fdc9e6846c5be4888a530dcadd055eef5b932e0e85c33c7d73/ruamel_yaml_clib-0.2.15-cp314-cp314-win_amd64.whl", hash = "sha256:ac9b8d5fa4bb7fd2917ab5027f60d4234345fd366fe39aa711d5dca090aa1467", size = 122847, upload-time = "2025-11-16T16:13:51.807Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709, upload-time = "2026-01-02T16:50:31.84Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" }, ] [[package]] name = "ruff" -version = "0.15.14" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/8a/8bce2894573e9dae6ff4d77fe34ad727d79b9e6238ad288c5638990d90f6/ruff-0.15.14.tar.gz", hash = "sha256:48e866b165be4a9bdbf310f7d3c9a07edef2fe8cd63ffeb4e00bb590506ebf9f", size = 4700910, upload-time = "2026-05-21T14:34:55.177Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/c8/74a92c6ff9fcfb4f1f947126d3ebee8389276e161ecc85de5bda7cda51bd/ruff-0.15.14-py3-none-linux_armv6l.whl", hash = "sha256:8dd2db9416e487c8d4b01fa7056bb02c4d05969d4f8d17a08c229c2f4ff3c108", size = 10739177, upload-time = "2026-05-21T14:34:37.332Z" }, - { url = "https://files.pythonhosted.org/packages/45/91/254a35c20acc38a7223c9d2d594af12e794432464f2cdeb52af1dc4a892d/ruff-0.15.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:be4ff55af755bd71a00ab3dc6bd7ffc467bd76e0df6881e286c2e3d23e8fb43b", size = 11144969, upload-time = "2026-05-21T14:34:43.978Z" }, - { url = "https://files.pythonhosted.org/packages/56/9e/d13e40f83b8d0a94430e6778ce1d94a43b38cf2efe63278bdd2b4c65abbf/ruff-0.15.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:48d5909d7d06276ce7dde6d32bfa4b0d4cb2651145cd8ee4b440722cbc77832f", size = 10478207, upload-time = "2026-05-21T14:34:48.378Z" }, - { url = "https://files.pythonhosted.org/packages/8d/f1/b15a7839fa4f332f8acec78e20564f26bb2d866e3d21710b877fd0263000/ruff-0.15.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca8cbfa94c4f90984a67561978602746d4cd27103568f745fa90eee3f0d4107d", size = 10818459, upload-time = "2026-05-21T14:34:22.318Z" }, - { url = "https://files.pythonhosted.org/packages/45/33/53d651177f84f94b400a0e27f8824eeada3dddc9d5ee8aeb048f4352a520/ruff-0.15.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a6bbc0333f1ab053423bcbf6226477d266ca7cec7738c4c8e3f55647803f3c4", size = 10541800, upload-time = "2026-05-21T14:34:20.209Z" }, - { url = "https://files.pythonhosted.org/packages/b8/a6/868f87e0bf9786ed24b5d0d0ad8676b8a94fd1912f42cddf9cfc7857818a/ruff-0.15.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a24a4f7605d7003a6674d4387651effd939dead3fddd0f36561eb77a9a2e542", size = 11342149, upload-time = "2026-05-21T14:34:46.365Z" }, - { url = "https://files.pythonhosted.org/packages/a7/8b/38cd5c19faffdcc05a408d2b78edccc69492ab9720eadb49ea15ef80d768/ruff-0.15.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:049b5326e53ed80978f2fc041a280603f69dd6b0c95464342a2bb4572d9d9e2f", size = 12212563, upload-time = "2026-05-21T14:34:28.579Z" }, - { url = "https://files.pythonhosted.org/packages/3e/4d/a3c5b874a556d5731e3e657aaf04311bb76f0a5c3ec220ed43051be6b64b/ruff-0.15.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4ed42e6696c8dfa5f06728e6441993901f548eb92d73bc472cb5a38d1395fbf", size = 11493299, upload-time = "2026-05-21T14:34:41.836Z" }, - { url = "https://files.pythonhosted.org/packages/1e/c0/56472c251d09858a53e51efbd485b09e1995d8731668b76d52e5dd6ee0f1/ruff-0.15.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:715c543cf450c4888251f91c52f1942a800541d9bddd7ac060aa4e6b77ae7cba", size = 11455931, upload-time = "2026-05-21T14:34:57.276Z" }, - { url = "https://files.pythonhosted.org/packages/2c/4a/e2e7b4d8dbf233d4eace59c75bc3435fa6d8bd3bae82d351d4e4300c0fd1/ruff-0.15.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ebab6013ec887d439d8b7593737a0a4ffb06d45d209d4e4bf2e92813082d3f", size = 11400794, upload-time = "2026-05-21T14:34:39.773Z" }, - { url = "https://files.pythonhosted.org/packages/97/c7/83c0539fe34c3e09136204d1e75d6052492364e0b3cb05e9465423f567d7/ruff-0.15.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:49072d36abdbe97a8dd7f480afe9c675699c0c495d4c84076e2c1203c4550581", size = 10804759, upload-time = "2026-05-21T14:34:31.045Z" }, - { url = "https://files.pythonhosted.org/packages/86/a6/18f2bfc095a2ab4a78745644e428205532ce6653a5d0fa8501572891534d/ruff-0.15.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:958522aee105068640c2c2ceae08f413ae44d922f52a1374ac13d6a96032fc93", size = 10539517, upload-time = "2026-05-21T14:34:53.064Z" }, - { url = "https://files.pythonhosted.org/packages/54/3a/5a8b3b69c654d4e4bf1d246ac5b49cbcdac6eaab6905925f8915f31e3b80/ruff-0.15.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f3707da619a143a2e8830e2abab8224478d69ace2d28cb6c20543ae97c36bf61", size = 11065169, upload-time = "2026-05-21T14:34:24.484Z" }, - { url = "https://files.pythonhosted.org/packages/ed/c5/8864e4e7925b836ea354b31d57641ec03830564e281a8b6f061f8c3e0ec1/ruff-0.15.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bb01d645694e3ec0102105d07ef2d53703970407d59c04e59d3ba0b7a1d53553", size = 11560214, upload-time = "2026-05-21T14:34:50.975Z" }, - { url = "https://files.pythonhosted.org/packages/36/38/012bf76752e1f89ed50b77b99532d90f3a3e287bc7918e1fc0948ac866ac/ruff-0.15.14-py3-none-win32.whl", hash = "sha256:6d0c1ad2a0ab718d39b6d8fd2217981ce4d625cd96a720095f798fb47d8b13e6", size = 10805548, upload-time = "2026-05-21T14:34:33.453Z" }, - { url = "https://files.pythonhosted.org/packages/d1/b7/4ea2c170f10ad760fff2a5250beb18897719dc8b52b53a24cddbb9dd3f19/ruff-0.15.14-py3-none-win_amd64.whl", hash = "sha256:802342981e056db3851a7836e5b070f8f15f67d4a685ae2a6160939d364b2902", size = 11939523, upload-time = "2026-05-21T14:34:18.077Z" }, - { url = "https://files.pythonhosted.org/packages/62/d5/bc97ff895ec35cf3925d4bd60f3b39d822f377a446906ec9bcc87405e59b/ruff-0.15.14-py3-none-win_arm64.whl", hash = "sha256:ff47b90a9ef6a40c9e2f3b479c1fb78531adf055b94c1eba0a7ba04b31951826", size = 11208607, upload-time = "2026-05-21T14:34:26.525Z" }, +version = "0.15.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/dc/35b341fc554ba02f217fc10da57d1a75168cfbcf75b0ef2202176d4c4f2d/ruff-0.15.20.tar.gz", hash = "sha256:1416eb04349192646b54de98f146c4f59afe37d0decfc02c3cbbf396f3a28566", size = 4755489, upload-time = "2026-06-25T17:20:37.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/d9/2d5014f0253ba541d2061d9fa7193f48e941c8b21bb88a7ff9bbe0bd0596/ruff-0.15.20-py3-none-linux_armv6l.whl", hash = "sha256:00e188c53e499c3c1637f73c91dcf2fb56d576cab76ce1be50a27c4e80e37078", size = 10839665, upload-time = "2026-06-25T17:19:44.702Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d3/ac1798ba64f670698867fcfc591d50e7e421bef137db564858f619a30fcf/ruff-0.15.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9ebd1fd9b9c95fc0bd7b2761aebec1f030013d2e193a2901b224af68fe47251b", size = 11208649, upload-time = "2026-06-25T17:19:48.787Z" }, + { url = "https://files.pythonhosted.org/packages/47/47/d3ac899991202095dfcf3d5176be4272642be3cf981a2f1a30f72a2afb95/ruff-0.15.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b16cdd67ca108185cd36dce98c576350c03b1660a751de725fb049193a0632", size = 10622638, upload-time = "2026-06-25T17:19:51.354Z" }, + { url = "https://files.pythonhosted.org/packages/33/13/4e043fe30aa94d4ff5213a9881fc296d12960f5971b234a5263fdc225312/ruff-0.15.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3413bb3c3d2ca6a8208f1f4809cd2dca3c6de6d0b491c0e70847672bde6e6efd", size = 10984227, upload-time = "2026-06-25T17:19:54.044Z" }, + { url = "https://files.pythonhosted.org/packages/76/e6/92e7bf40388bc5800073b96564f56264f7e48bfd1a498f5ced6ae6d5a769/ruff-0.15.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd7ec42b3bb3da066488db093308a69c4ac5ee6d2af333a86ba6e2eb2e7dd44b", size = 10622882, upload-time = "2026-06-25T17:19:57.037Z" }, + { url = "https://files.pythonhosted.org/packages/13/7a/43460be3f24495a3aa46d4b16873e2c4941b3b5f0b00cf88c03b7b94b339/ruff-0.15.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1a36ad0eb77fba9aabfb69ede54de6f376d04ac18ebea022847046d340a8267", size = 11474808, upload-time = "2026-06-25T17:20:00.357Z" }, + { url = "https://files.pythonhosted.org/packages/27/a0/f37077884873221c6b33b4ab49eb18f9f88e54a16a25a5bca59bef46dd66/ruff-0.15.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6df3b1e4610432f0386dba04d853b5f08cbbc903410c6fcc02f620f05aff53c", size = 12293094, upload-time = "2026-06-25T17:20:03.446Z" }, + { url = "https://files.pythonhosted.org/packages/a6/74/165545b60256a9704c21ac0ec4a0d07933b320812f9584836c9f4aca4292/ruff-0.15.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e89f198a1ea6ef0d727c1cf16088bc91a6cb0ab947dedc966715691647186eae", size = 11526176, upload-time = "2026-06-25T17:20:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/86/b1/a976a136d40ade83ce743578399865f57001003a409acadc0ecbb3051082/ruff-0.15.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309809086c2acb67624950a3c8133e80f32d0d3e27106c0cd60ff26657c9f24b", size = 11520767, upload-time = "2026-06-25T17:20:09.191Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/f032696cb01c9b54c0263fa393474d7758f1cdc021a01b04e3cbc2500999/ruff-0.15.20-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d2374caa2f2c2f9e2b7da0a50802cfb8b79f55a9b5e49379f564544fbf56487", size = 11500132, upload-time = "2026-06-25T17:20:13.602Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f4/51b1a14bc69e8c224b15dab9cce8e99b425e0455d462caa2b3c9be2b6a8e/ruff-0.15.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a1ed17b65293e0c2f22fc387bc13198a5de94bf4429589b0ff6946b0feaf21a3", size = 10943828, upload-time = "2026-06-25T17:20:16.635Z" }, + { url = "https://files.pythonhosted.org/packages/71/4b/fe267640783cd02bf6c5cc290b1df1051be2ec294c678b5c15fe19e52343/ruff-0.15.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f701305e66b38ea6c91882490eb73459796808e4c6362a1b765255e0cdcd4053", size = 10645418, upload-time = "2026-06-25T17:20:19.4Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c0/a65aa4ec2f5e87a1df32dc3ec1fede434fe3dfd5cbcf3b503cafc676ab54/ruff-0.15.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b9c0c367ad8e5d0d5b5b8537864c469a0a0e55417aadfbeca41fa61333be9f4", size = 11211770, upload-time = "2026-06-25T17:20:22.033Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/0caa331d954ae2723d729d351c989cb4ca8b6077d5c6c2cb6de75e98c041/ruff-0.15.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:01cc00dd58f0df339d0e902219dd53990ea99996a0344e5d9cc8d45d5307e460", size = 11618698, upload-time = "2026-06-25T17:20:25.259Z" }, + { url = "https://files.pythonhosted.org/packages/10/9b/5f14927848d2fd4aa891fd88d883788c5a7baba561c7874732364045708c/ruff-0.15.20-py3-none-win32.whl", hash = "sha256:ed65ef510e43a137207e0f01cfcf998aeddb1aeeda5c9d35023e910284d7cf21", size = 10857322, upload-time = "2026-06-25T17:20:28.612Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/fe47c501f9dea92a26d788ff98bb5d92ed4cb4c88792c5c88af6b697dc8e/ruff-0.15.20-py3-none-win_amd64.whl", hash = "sha256:a525c81c70fb0380344dd1d8745d8cc1c890b7fc94a58d5a07bd8eb9557b8415", size = 11993274, upload-time = "2026-06-25T17:20:31.871Z" }, + { url = "https://files.pythonhosted.org/packages/d7/2b/9555445e1201d92b3195f45cdb153a0b68f24e0a4273f6e3d5ab46e212bb/ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca", size = 11343498, upload-time = "2026-06-25T17:20:35.03Z" }, ] [[package]] @@ -1851,63 +1786,53 @@ wheels = [ [[package]] name = "scipy" -version = "1.17.1" +version = "1.18.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -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 = [ - { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, - { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, - { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, - { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, - { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, - { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, - { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, - { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, - { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, - { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, - { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, - { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, - { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, - { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, - { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, - { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, - { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, - { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, - { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, - { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, - { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, - { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, - { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, - { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, - { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, - { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, - { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, - { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, - { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, - { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, - { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, - { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, - { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, - { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, - { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, - { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, - { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, - { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, - { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, - { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, - { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, - { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, - { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, - { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, - { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, - { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, - { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, - { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, - { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, - { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/19/ca10ead60b0acc80b2b833c2c4a4f2ff753d0f58b811f70d911c7e94a25c/scipy-1.18.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:7bd21faaf5a1a3b2eff922d02db5f191b99a6518db9078a8fb23169f6d22259a", size = 31056519, upload-time = "2026-06-19T14:59:45.203Z" }, + { url = "https://files.pythonhosted.org/packages/96/72/1e6442a00cd2924d361aa1b642ab6373ec35c6fabf311a760be9f76e0f13/scipy-1.18.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:265915e79107de9f946b855e50d7470d5893ec3f54b342e1aa6201cbdcd8bb6b", size = 28681889, upload-time = "2026-06-19T14:59:48.103Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2d/11dd93d21e147a73ba22bd75c0b9208d3a2e0ec76d53170ce7d9029b1015/scipy-1.18.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9ab7b758be6940954a713ee466e2043e9f6e2ed965c1fce5c91039f4be3d90a9", size = 20423580, upload-time = "2026-06-19T14:59:50.665Z" }, + { url = "https://files.pythonhosted.org/packages/9c/01/93552f75e0d2a7dd115a45e59209c51e8d514daff02fc887d2623be06fe1/scipy-1.18.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:97b6cddaaee0a779ef6b5ca83c9604b27cc16b2b8fc22c142652df8793319fb8", size = 23054441, upload-time = "2026-06-19T14:59:53.564Z" }, + { url = "https://files.pythonhosted.org/packages/3c/23/21f5e703643d66f21faa6b4c73195bfcad70c55efcb4f1ab327cd7c4101a/scipy-1.18.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:52a96e21517c7292375c0e27dd796a811f03fcea5fd4d108fdfea8145dcf17ab", size = 33968720, upload-time = "2026-06-19T14:59:56.415Z" }, + { url = "https://files.pythonhosted.org/packages/dd/aa/1b939f6c67ed68635bb538e6752d3dacc02f66535182e939a89581a44e9c/scipy-1.18.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f55797419e16e7f30cf88ffb3113ce0467f00cfe3f70d5c281730b21769bfc2", size = 35287115, upload-time = "2026-06-19T14:59:59.411Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ff/eec46be7e9234208f801062b53e1983085eddebd693f6c9bfb03b459830d/scipy-1.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ad033410e2e0672ffdc1042110cef20e1c46f8fd0616cee1d44d8d58fad8fc11", size = 35577989, upload-time = "2026-06-19T15:00:02.235Z" }, + { url = "https://files.pythonhosted.org/packages/84/ca/210d4759c7210bb7d269437421959b39a33434e2776b60c5cb8a763bb30a/scipy-1.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a55985d54c769c872e64b7f4c8a81cc30ef700cc04296abbbf3705439c126de", size = 37421717, upload-time = "2026-06-19T15:00:05.102Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/9a9edb45345bd6744da5ddfb6628e5d5185920494c6a67ec45b6381004cb/scipy-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:71ccc8faa2dd16ac310233203474a8b5cb67f10dedd54a3116d34943f4b19132", size = 36597428, upload-time = "2026-06-19T15:00:08.112Z" }, + { url = "https://files.pythonhosted.org/packages/99/0e/33f32a2a58987e26aec0f7df252cbbad1e90ae77bdbc76f40dd4ed0cf0ea/scipy-1.18.0-cp312-cp312-win_arm64.whl", hash = "sha256:d88363fd9d8fbd3511bd273f1a49efb2a540773ddf92a91d57498ce7dd7f3e76", size = 24351481, upload-time = "2026-06-19T15:00:11.103Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/9c0136c2de7ae0779b7b366447766cec6d9f0702c56bb8ffeb04c8fd3af4/scipy-1.18.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:09143f676d157d9f546d663504ef9c1becb819824f1afc018814176411942446", size = 31036107, upload-time = "2026-06-19T15:00:14.03Z" }, + { url = "https://files.pythonhosted.org/packages/02/73/0291a64843270f4efb86cdcf2ee0f2048631b65ec6b405398b2b4dbf11bf/scipy-1.18.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5efe260f69417b97ddae455bfb5a95e8359f7f66ad7fa9522a60feb66f169520", size = 28663303, upload-time = "2026-06-19T15:00:16.819Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0f/10ffa0b697a572f4e0d48b92a88895d366422f019f723e7e14a84c050dac/scipy-1.18.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:68363b7eaacd8b5dd426df56d782cc156468ac79a127a1b87ca597d6e2e82197", size = 20404960, upload-time = "2026-06-19T15:00:19.635Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d2/e896cea21ba8edd6c81d4c55b1ffcc717e79698dcbebf9641b4cfb4c6622/scipy-1.18.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:c5557d8be5da8e41353fcd4d21491fdbab83b062fc579e94dc09a7c8ab4f669b", size = 23034074, upload-time = "2026-06-19T15:00:22.107Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b2/e83ea34279a52c03374477c74006256ec78df65fc877baa4617d6de1d202/scipy-1.18.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d13bca67c096d89fb95ced0d8921807300fce0275643aef9533cc63a0773468", size = 33942038, upload-time = "2026-06-19T15:00:24.964Z" }, + { url = "https://files.pythonhosted.org/packages/f6/af/e8fe5fb136f51e2b01678b92cb4106d10d8cd68ec147ead2e7cb0ac75398/scipy-1.18.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a46f9273dbd0eb1cefba61c9b8648b4dfe3cbc14a080176f9a73e44b8336dc7f", size = 35266390, upload-time = "2026-06-19T15:00:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/3a/49/2c5cbb907b56695fc67517811d1db234dfd83381a84814ec220aded2794d/scipy-1.18.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5aba46108853ddfc77906b6557aac839d2b52e900c1d72a1180adaaab58d265f", size = 35551324, upload-time = "2026-06-19T15:00:31.014Z" }, + { url = "https://files.pythonhosted.org/packages/bb/73/eda39f7a2d306ff0ffc574afd13c0bbb6d10a603d9a413998ee269487a80/scipy-1.18.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b6f758e35f12757b5d95c00bc6de2438e229c2664b7a92e96f205959d9f2dfa4", size = 37404785, upload-time = "2026-06-19T15:00:34.072Z" }, + { url = "https://files.pythonhosted.org/packages/b7/d2/ae881ee28d014f38e0ccbfd974a06a919ba9af34f1f74bf42b5301891d63/scipy-1.18.0-cp313-cp313-win_amd64.whl", hash = "sha256:1afac4a847207c7ff8efd321734a50b06d0280b3b2a2c0fc2f413101747ad7c7", size = 36554943, upload-time = "2026-06-19T15:00:36.903Z" }, + { url = "https://files.pythonhosted.org/packages/70/3a/21154e2d54eb3639c6bf4dbae2e531c68356bfe95990daa30df33b30d556/scipy-1.18.0-cp313-cp313-win_arm64.whl", hash = "sha256:c5dbddf60e58c2312316d097271a8e73d40eaf2eabfa4d95ed7d3695bbf2ce7b", size = 24350911, upload-time = "2026-06-19T15:00:40.062Z" }, + { url = "https://files.pythonhosted.org/packages/78/b5/915a19b3de2f7430062b509653563db1633ddbb6f021b06731521115d4e2/scipy-1.18.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4c256ee70c0d1a8a2ace807e199ccd4e3f57037433842abb3fb36bc17eaa9578", size = 31036253, upload-time = "2026-06-19T15:00:43.216Z" }, + { url = "https://files.pythonhosted.org/packages/d7/88/b72def7262e150d16be13fca37a96481138d624e700340bc3362a7588929/scipy-1.18.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:2ef3abc54a4ffc53765374b0d5728532dfdd2585ed23f6b11c206a1f0b1b9af8", size = 28673758, upload-time = "2026-06-19T15:00:46.663Z" }, + { url = "https://files.pythonhosted.org/packages/91/02/2e636a61a525632c373cf6a9c24442a3ffb79e364d38e98b32042964ac32/scipy-1.18.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2a6af57bd9e4a75d70e4117e78a1bbee84f79ae3fbb6d0111005d6ebcc4cb8d", size = 20415514, upload-time = "2026-06-19T15:00:49.399Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b6/2135974442f6aba159d9d39d774a1c8cb19947016725d69fecc685df45bf/scipy-1.18.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:3f1ac564d3bf6c03d861d2cd87a1bea0da2887136f7fb1bf519c05a8971452d6", size = 23034398, upload-time = "2026-06-19T15:00:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e6/ba89ec5abf6ee9257c0d1ec985573f3ae32742c24bc03e016388a40b1b15/scipy-1.18.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40395a5fcd1abee49a5c7aaa98c29db393eedc835138560a588c47ec16156690", size = 33998032, upload-time = "2026-06-19T15:00:54.838Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c4/bc41eb19b0fd0db868f4132920879019318d80cc522ad8f2bca4611af808/scipy-1.18.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ca01e8ae69f1b18e9a58d91afead31be3cef0dd905a10249dac559ee15460a0", size = 35283333, upload-time = "2026-06-19T15:00:58.152Z" }, + { url = "https://files.pythonhosted.org/packages/53/a4/cbdeef6eb3830a8462a9d4ada814de5fc984345cc9ecf17cbec51a036f1e/scipy-1.18.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7a7f3b01647384dbc3a711e8c6778e0aabbe93959249fef5c7393396bcac0867", size = 35610216, upload-time = "2026-06-19T15:01:01.155Z" }, + { url = "https://files.pythonhosted.org/packages/80/4d/b2b82502b65f661d1b789c1665dcdf315d5f12194e06fc0b37946294ebae/scipy-1.18.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6aa94e78ec192a30063a5e72e561c28af769dc311190b24fe91774eff1969709", size = 37418960, upload-time = "2026-06-19T15:01:04.155Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/902d836831474b0ab5a37d16404f7bc5fafd9efba632890e271ba952635f/scipy-1.18.0-cp314-cp314-win_amd64.whl", hash = "sha256:2d8bbdc6c817f5b4006a54d799d4f5bab6f910193cbb9a1ff310833d4d270f61", size = 37288845, upload-time = "2026-06-19T15:01:07.822Z" }, + { url = "https://files.pythonhosted.org/packages/b6/43/8d73b337a3bdb14daa0314f0434210747c02d79d729ce1777574a817dcf6/scipy-1.18.0-cp314-cp314-win_arm64.whl", hash = "sha256:18e9575f1569b2c54174e6159d32942e03731177f63dce7975f0a0c88d102f5b", size = 24988971, upload-time = "2026-06-19T15:01:11.076Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b4/f11918b0508a2787031a0499a03fbe3546f3bb5ca05d01038c45b278c09a/scipy-1.18.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f351e0dd702687d12a402b867a1b4146a256923e1c38317cbc472f6372b94707", size = 31399325, upload-time = "2026-06-19T15:01:13.723Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d1/1f287b57c0ff0ee5185dff3946d92c8017d39b0e431f0ae79a3ff1859512/scipy-1.18.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7c7a51b33ce387193c97f228320cf8e87361daa1bba750638677729598b3e677", size = 29092110, upload-time = "2026-06-19T15:01:16.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1a/7b74eb6c392fdcb27d414c0e7558a6d0231eb3b6d73571f479bb81ea8794/scipy-1.18.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:84031d7b052a54fae2f8632e0ec802073d385476eb9a63079bce6e23ef9283d4", size = 20833811, upload-time = "2026-06-19T15:01:20.488Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ad/f3941716320a7b9cb4d68734a903b45fe16eff5fb7da7e16f2e619304979/scipy-1.18.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:56abf29a7c067dde59be8b9a22d606a4ea1b2f2a4b756d9d903c62818f5dacce", size = 23396644, upload-time = "2026-06-19T15:01:23.364Z" }, + { url = "https://files.pythonhosted.org/packages/22/22/1446b62ffe07f9719b7d9b1b6a4e05a772833ae8f441fe4c22c34c9b250f/scipy-1.18.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ad44305cfa24b1ba5803cbbebf033590ccbac1aa5d612d727b785325ab408b0", size = 34079318, upload-time = "2026-06-19T15:01:26.002Z" }, + { url = "https://files.pythonhosted.org/packages/56/3b/b87da667098bb470fa30c7011b0ba351ee976dd395c78798c66e941665a3/scipy-1.18.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:945c1761b93f38d7f99ae81ae80c63e621471608c7eeead563f6df025585cd58", size = 35324320, upload-time = "2026-06-19T15:01:28.881Z" }, + { url = "https://files.pythonhosted.org/packages/f8/a1/c7932f91909759b0267f75fdea34e91309f96b895757534b76a90b6b4344/scipy-1.18.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1a4441f15d620578772a49e5ab48c0ee1f7a0220e387110283062729136b2553", size = 35699541, upload-time = "2026-06-19T15:01:31.968Z" }, + { url = "https://files.pythonhosted.org/packages/f7/86/5185061a1fcc41d18c5dc2463969b3a3964b31d9ac67b2fb05d4c7ff7670/scipy-1.18.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9aac6192fac56bf2ca534389d24623f07b39ff83317d58287285e7fbd622ff76", size = 37472480, upload-time = "2026-06-19T15:01:35.136Z" }, + { url = "https://files.pythonhosted.org/packages/31/8e/f04c68e39919a010d34f2ee1367fd705b0a25a02f609d755f0bfbc0a15fc/scipy-1.18.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e40baea28ae7f5475c779741e2d90b1247c78531207b49c7030e698ff81cee3f", size = 37365390, upload-time = "2026-06-19T15:01:38.091Z" }, + { url = "https://files.pythonhosted.org/packages/d5/19/969dc072906c84dd0a3b05dcf57ea750936087d7873549e408b35cfc3f97/scipy-1.18.0-cp314-cp314t-win_arm64.whl", hash = "sha256:368e0a705903c466aa5f08eefb39e6b1b6b2d659e7352a31fd9e2438365be0f8", size = 25279661, upload-time = "2026-06-19T15:01:40.817Z" }, ] [[package]] @@ -1939,20 +1864,20 @@ wheels = [ [[package]] name = "snowballstemmer" -version = "3.0.1" +version = "3.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/f8/0a71edf031f03c40db17503cb8ca78a69a171254e568e7db241b0ab57ea1/snowballstemmer-3.1.1.tar.gz", hash = "sha256:e07bbc54a0d798fe6010a12398422e62a8bfbba95c394fd0956ef58cb4d3e260", size = 123314, upload-time = "2026-06-03T00:56:40.194Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl", hash = "sha256:7e207fa178741da09cdee59d3ecec3827ad5f92b1fc5c9ff3755b639f71f5752", size = 104164, upload-time = "2026-06-03T00:56:38.614Z" }, ] [[package]] name = "soupsieve" -version = "2.8.3" +version = "2.8.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } +sdist = { url = "https://files.pythonhosted.org/packages/47/2c/0a5f6f8ee0d5589e48c7640213ed5175d52cf540a06725b628cc1a45d6ce/soupsieve-2.8.4.tar.gz", hash = "sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e", size = 121110, upload-time = "2026-05-24T13:55:57.154Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, + { url = "https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65", size = 37304, upload-time = "2026-05-24T13:55:55.406Z" }, ] [[package]] @@ -2083,28 +2008,28 @@ wheels = [ [[package]] name = "tornado" -version = "6.5.5" +version = "6.5.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f8/f1/3173dfa4a18db4a9b03e5d55325559dab51ee653763bb8745a75af491286/tornado-6.5.5.tar.gz", hash = "sha256:192b8f3ea91bd7f1f50c06955416ed76c6b72f96779b962f07f911b91e8d30e9", size = 516006, upload-time = "2026-03-10T21:31:02.067Z" } +sdist = { url = "https://files.pythonhosted.org/packages/64/24/95ec527ad67b76d59299e5465b3935d05e4294b7e0290a3924b7487df30b/tornado-6.5.7.tar.gz", hash = "sha256:66c513a76cda70d53907bc27cf1447557699c2e95aa48ba27a442ff61c3ddfc2", size = 519252, upload-time = "2026-06-08T17:34:51.232Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/59/8c/77f5097695f4dd8255ecbd08b2a1ed8ba8b953d337804dd7080f199e12bf/tornado-6.5.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:487dc9cc380e29f58c7ab88f9e27cdeef04b2140862e5076a66fb6bb68bb1bfa", size = 445983, upload-time = "2026-03-10T21:30:44.28Z" }, - { url = "https://files.pythonhosted.org/packages/ab/5e/7625b76cd10f98f1516c36ce0346de62061156352353ef2da44e5c21523c/tornado-6.5.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:65a7f1d46d4bb41df1ac99f5fcb685fb25c7e61613742d5108b010975a9a6521", size = 444246, upload-time = "2026-03-10T21:30:46.571Z" }, - { url = "https://files.pythonhosted.org/packages/b2/04/7b5705d5b3c0fab088f434f9c83edac1573830ca49ccf29fb83bf7178eec/tornado-6.5.5-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e74c92e8e65086b338fd56333fb9a68b9f6f2fe7ad532645a290a464bcf46be5", size = 447229, upload-time = "2026-03-10T21:30:48.273Z" }, - { url = "https://files.pythonhosted.org/packages/34/01/74e034a30ef59afb4097ef8659515e96a39d910b712a89af76f5e4e1f93c/tornado-6.5.5-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:435319e9e340276428bbdb4e7fa732c2d399386d1de5686cb331ec8eee754f07", size = 448192, upload-time = "2026-03-10T21:30:51.22Z" }, - { url = "https://files.pythonhosted.org/packages/be/00/fe9e02c5a96429fce1a1d15a517f5d8444f9c412e0bb9eadfbe3b0fc55bf/tornado-6.5.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3f54aa540bdbfee7b9eb268ead60e7d199de5021facd276819c193c0fb28ea4e", size = 448039, upload-time = "2026-03-10T21:30:53.52Z" }, - { url = "https://files.pythonhosted.org/packages/82/9e/656ee4cec0398b1d18d0f1eb6372c41c6b889722641d84948351ae19556d/tornado-6.5.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36abed1754faeb80fbd6e64db2758091e1320f6bba74a4cf8c09cd18ccce8aca", size = 447445, upload-time = "2026-03-10T21:30:55.541Z" }, - { url = "https://files.pythonhosted.org/packages/5a/76/4921c00511f88af86a33de770d64141170f1cfd9c00311aea689949e274e/tornado-6.5.5-cp39-abi3-win32.whl", hash = "sha256:dd3eafaaeec1c7f2f8fdcd5f964e8907ad788fe8a5a32c4426fbbdda621223b7", size = 448582, upload-time = "2026-03-10T21:30:57.142Z" }, - { url = "https://files.pythonhosted.org/packages/2c/23/f6c6112a04d28eed765e374435fb1a9198f73e1ec4b4024184f21faeb1ad/tornado-6.5.5-cp39-abi3-win_amd64.whl", hash = "sha256:6443a794ba961a9f619b1ae926a2e900ac20c34483eea67be4ed8f1e58d3ef7b", size = 448990, upload-time = "2026-03-10T21:30:58.857Z" }, - { url = "https://files.pythonhosted.org/packages/b7/c8/876602cbc96469911f0939f703453c1157b0c826ecb05bdd32e023397d4e/tornado-6.5.5-cp39-abi3-win_arm64.whl", hash = "sha256:2c9a876e094109333f888539ddb2de4361743e5d21eece20688e3e351e4990a6", size = 448016, upload-time = "2026-03-10T21:31:00.43Z" }, + { url = "https://files.pythonhosted.org/packages/02/dc/c7043cab6fed8ae159fc1923ce829ada35c4dbd797d408a43858ffaf9639/tornado-6.5.7-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:148b2eb15c2c765a50796172c1e499649b35f30d2e3c3d3e15913cfa56bfb163", size = 448543, upload-time = "2026-06-08T17:34:38.052Z" }, + { url = "https://files.pythonhosted.org/packages/92/4f/090b1431e5a43df696feceffc268c5383cc079ecb5f08ce58f917109aafe/tornado-6.5.7-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9da38de27f1da3b78a966f0dae12b5a1ea9afe72ca805d84ff06508272ddf100", size = 446707, upload-time = "2026-06-08T17:34:39.594Z" }, + { url = "https://files.pythonhosted.org/packages/37/d8/ef374952fd5da67d4463122c2b8e5a96536ec10b4b339254c6dcde81d01c/tornado-6.5.7-cp39-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8d759e71906ee783f8867b93bf26a265743da4c1e2f4a018464c1ba019862972", size = 449774, upload-time = "2026-06-08T17:34:41.204Z" }, + { url = "https://files.pythonhosted.org/packages/35/37/d434c73f4c6e014b745b9b37085f34f40c022f007efff3d7fe65991899f3/tornado-6.5.7-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a46347a18f23fb92b396beebe0fb78f61dda0cc302445202c16203d8a18848b", size = 450745, upload-time = "2026-06-08T17:34:42.531Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2b/56b9aff361d7f1ab728a805ec7d7ea835f8807afa9f5cc690ea0e630efb9/tornado-6.5.7-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7778b30bef919231265e91c69963ce0f49a1e9c07ac900bbe75b19ce2575ba92", size = 450578, upload-time = "2026-06-08T17:34:43.787Z" }, + { url = "https://files.pythonhosted.org/packages/02/30/a7444fb23aa76860a14198fab96ac79f1866b0a6e19e26c4381b0938e50f/tornado-6.5.7-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e726f0c75da7726eec023aa62751ff8878bd2737e34fbdd33b1ae5897d2200f5", size = 449985, upload-time = "2026-06-08T17:34:45.326Z" }, + { url = "https://files.pythonhosted.org/packages/5c/42/5f0e56c01e8d9d36f4e23f367b85ae6cae0c1ecddd5e6977d8388ad27488/tornado-6.5.7-cp39-abi3-win32.whl", hash = "sha256:f8de3bf12d3efdd0cbe7c8887868198f8a91415e3f29fcf258d9b8eb7b1d9ae4", size = 451047, upload-time = "2026-06-08T17:34:46.784Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a4/b393076ffb21b469eec5b328a0534cf03a3b90bfc6b1f09507cdd075d938/tornado-6.5.7-cp39-abi3-win_amd64.whl", hash = "sha256:de942f843533a039ef9fa3d9c88c7cd8a7c94553fb5ad0154270989b3d99a2c4", size = 451485, upload-time = "2026-06-08T17:34:48.248Z" }, + { url = "https://files.pythonhosted.org/packages/71/2e/7b1c769803121b809112cf9a00681c472eae1d80e32d7ec0e0bd61d0d0e1/tornado-6.5.7-cp39-abi3-win_arm64.whl", hash = "sha256:ff934fce95643af5f11efdae618eaa73d469dc588641e5c8d19295a0c65c4796", size = 450506, upload-time = "2026-06-08T17:34:49.702Z" }, ] [[package]] name = "traitlets" -version = "5.15.0" +version = "5.15.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1b/22/40f55b26baeab80c2d7b3f1db0682f8954e4617fee7d90ce634022ef05c6/traitlets-5.15.0.tar.gz", hash = "sha256:4fead733f81cf1c4c938e06f8ca4633896833c9d89eff878159457f4d4392971", size = 163197, upload-time = "2026-05-06T08:05:58.016Z" } +sdist = { url = "https://files.pythonhosted.org/packages/57/a9/a2584b8313b89f94869ddb3c4074617a691de1812a614d2d50e32ca5a7a6/traitlets-5.15.1.tar.gz", hash = "sha256:7b1c07854fe25acb39e009bae49f11b79ff6cbb2f27999104e9110e7a6b53722", size = 163344, upload-time = "2026-06-03T12:26:06.181Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/98/a9937a969d018a23badfea0b381f66783649d48e0ea6c41923265c3cbeb3/traitlets-5.15.0-py3-none-any.whl", hash = "sha256:fb36a18867a6803deab09f3c5e0fa81bb7b26a5c9e82501c9933f759166eff40", size = 85877, upload-time = "2026-05-06T08:05:55.853Z" }, + { url = "https://files.pythonhosted.org/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl", hash = "sha256:770a53705f84b81ac107e83a1b3328ff2dae16094d8fc3cfc004e4b22dfd8e92", size = 85858, upload-time = "2026-06-03T12:26:04.395Z" }, ] [[package]] @@ -2139,18 +2064,18 @@ wheels = [ [[package]] name = "urllib3" -version = "2.6.3" +version = "2.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] [[package]] name = "wcwidth" -version = "0.7.0" +version = "0.8.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2c/ee/afaf0f85a9a18fe47a67f1e4422ed6cf1fe642f0ae0a2f81166231303c52/wcwidth-0.7.0.tar.gz", hash = "sha256:90e3a7ea092341c44b99562e75d09e4d5160fe7a3974c6fb842a101a95e7eed0", size = 182132, upload-time = "2026-05-02T16:04:12.653Z" } +sdist = { url = "https://files.pythonhosted.org/packages/34/74/c6428f875774288bec1396f5bfcbc2d925700a4dad61727fd5f2b12f249d/wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda", size = 1466253, upload-time = "2026-06-29T18:11:11.601Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/52/e465037f5375f43533d1a80b6923955201596a99142ed524d77b571a1418/wcwidth-0.7.0-py3-none-any.whl", hash = "sha256:5d69154c429a82910e241c738cd0e2976fac8a2dd47a1a805f4afed1c0f136f2", size = 110825, upload-time = "2026-05-02T16:04:11.033Z" }, + { url = "https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85", size = 323166, upload-time = "2026-06-29T18:11:09.888Z" }, ] From 91df8e9e9f94533a53c872dfa8ad3f33b74995c1 Mon Sep 17 00:00:00 2001 From: arjunsridhar12345 Date: Tue, 30 Jun 2026 10:19:10 -0700 Subject: [PATCH 26/36] refactor: try to make flexible lick series generation --- .../nwb/acquisition/acquisition_builder.py | 93 +++++++++++-------- 1 file changed, 56 insertions(+), 37 deletions(-) diff --git a/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py b/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py index 5185c46..2c1ba0d 100644 --- a/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py +++ b/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py @@ -13,6 +13,28 @@ from dynamic_foraging_processing.utils.rewards import get_annotated_rewards +class LickSource(t.NamedTuple): + """Location of one lick port's signal in the raw dataset. + + Attributes + ---------- + device : str + The Harp device node under ``Behavior`` (``"HarpBehavior"`` for the + standard behavior board, ``"HarpLickometerLeft"`` / + ``"HarpLickometerRight"`` for the lickometer board). + stream : str + The digital-input stream on that device (``"DigitalInputState"`` for + the behavior board, ``"LickState"`` for the lickometer board). + port : str + The column (``"DIPort0"`` / ``"DIPort1"`` + for the behavior board, ``"Channel0"`` for the lickometer board). + """ + + device: str + stream: str + port: str + + class AcquisitionBuilder: """Builds the NWB acquisition module from raw dynamic foraging data.""" @@ -76,23 +98,27 @@ def get_manual_water_times(self) -> pd.DataFrame: except (KeyError, FileNotFoundError): return pd.DataFrame({"data": []}) - def get_lick_times(self, stream_name: str, port: str) -> np.ndarray: - """Get the lick times for one lick port from the behavior board DI ports. + def get_lick_times(self, device: str, stream_name: str, port: str) -> np.ndarray: + """Get the lick times for one lick port from a Harp digital-input stream. - Licks are read from a HarpBehavior digital-input stream. On the standard - behavior board this is ``DigitalInputState``; LicketySplit boards expose - an equivalent stream under a different name. Left licks are on - ``DIPort0`` and right licks on ``DIPort1``; a lick time is a timestamp at - which that port's digital input is high. + On the standard behavior board licks are read from + ``HarpBehavior``/``DigitalInputState``, with left licks on ``DIPort0`` + and right licks on ``DIPort1``. The lickometer board exposes each side + as its own device (``HarpLickometerLeft`` / ``HarpLickometerRight``) + with a ``LickState`` stream and a ``Channel0`` column. A lick time is a + timestamp at which the selected column's digital input is high. Parameters ---------- + device : str + The Harp device node under ``Behavior`` (e.g. ``"HarpBehavior"`` for + the standard behavior board). stream_name : str - The HarpBehavior digital-input stream to read licks from (e.g. + The digital-input stream to read licks from (e.g. ``"DigitalInputState"`` for the standard behavior board). port : str - The DI port column to read licks from (``"DIPort1"`` for the right - lick port, ``"DIPort0"`` for the left lick port). + The column to read licks from (``"DIPort1"`` for the right lick + port, ``"DIPort0"`` for the left lick port on the behavior board). Returns ------- @@ -101,31 +127,23 @@ def get_lick_times(self, stream_name: str, port: str) -> np.ndarray: when the stream is absent. """ try: - data = ( - self.loader.dataset.at("Behavior") - .at("HarpBehavior") - .at(stream_name) - .load() - .data - ) + data = self.loader.dataset.at("Behavior").at(device).at(stream_name).load().data except (KeyError, FileNotFoundError): return np.array([]) licks = data[data[port].fillna(False).astype(bool)] return licks.index.to_numpy() def _lick_time_series( - self, *, stream_name: str, port: str, name: str, side_label: str + self, *, source: LickSource, name: str, side_label: str ) -> AcquisitionSeries: - """Build one lick port's lick-time series from the behavior board DI ports. + """Build one lick port's lick-time series from a Harp digital-input stream. Parameters ---------- - stream_name : str - The HarpBehavior digital-input stream to read licks from (e.g. - ``"DigitalInputState"`` for the standard behavior board). - port : str - The DI port column to read licks from (``"DIPort1"`` for the right - lick port, ``"DIPort0"`` for the left lick port). + source : LickSource + The device, stream, and column locating this lick port's signal + (e.g. ``LickSource("HarpBehavior", "DigitalInputState", "DIPort0")`` + for the standard behavior board's left port). name : str Acquisition series name. side_label : str @@ -137,15 +155,14 @@ def _lick_time_series( The lick-time series for this lick port. The ``data`` array marks each timestamp as a detected lick (``True``). """ - lick_times = self.get_lick_times(stream_name, port) + lick_times = self.get_lick_times(source.device, source.stream, source.port) return AcquisitionSeries( name=name, data=np.ones(lick_times.shape[0], dtype=bool), timestamps=lick_times, unit="second", description=( - f"The lick times of the {side_label} lick port ({port} on the " - "behavior board)." + f"The lick times of the {side_label} lick port ({source.port} on {source.device})." ), ) @@ -210,16 +227,20 @@ def _reward_delivery_series( ) def build_acquisition( - self, lick_stream_name: str = "DigitalInputState" + self, + left_lick: LickSource, + right_lick: LickSource, ) -> t.List[t.Union[AcquisitionSeries, AcquisitionTable]]: """Build the NWB acquisition entries. Parameters ---------- - lick_stream_name : str, optional - The HarpBehavior digital-input stream to read lick times from. - Defaults to ``"DigitalInputState"`` for the Janelia boards; - pass the equivalent stream name for LicketySplit boards. + left_lick, right_lick : LickSource, optional + Where to read each side's lick times. Defaults to the standard + behavior board (``HarpBehavior``/``DigitalInputState`` on + ``DIPort0``/``DIPort1``). For the lickometer board pass, e.g., + ``LickSource("HarpLickometerLeft", "LickState", "Channel0")`` and + ``LickSource("HarpLickometerRight", "LickState", "Channel0")``. Returns ------- @@ -269,16 +290,14 @@ def build_acquisition( ) acquisiton_entries.append( self._lick_time_series( - stream_name=lick_stream_name, - port="DIPort0", + source=left_lick, name="left_lick_time", side_label="left", ) ) acquisiton_entries.append( self._lick_time_series( - stream_name=lick_stream_name, - port="DIPort1", + source=right_lick, name="right_lick_time", side_label="right", ) From a3e222a9d580b2c0f8e38f01e669fb4633bb4913 Mon Sep 17 00:00:00 2001 From: arjunsridhar12345 Date: Tue, 30 Jun 2026 10:19:19 -0700 Subject: [PATCH 27/36] test: update tests --- tests/test_nwb/test_acquisition/test_acquisition_builder.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_nwb/test_acquisition/test_acquisition_builder.py b/tests/test_nwb/test_acquisition/test_acquisition_builder.py index 54c3990..0ea85c0 100644 --- a/tests/test_nwb/test_acquisition/test_acquisition_builder.py +++ b/tests/test_nwb/test_acquisition/test_acquisition_builder.py @@ -189,10 +189,10 @@ def test_get_lick_times_selects_di_port_by_side(): builder = AcquisitionBuilder(loader=_make_loader()) np.testing.assert_array_equal( - builder.get_lick_times("DigitalInputState", "DIPort0"), np.array([1.0]) + builder.get_lick_times("HarpBehavior", "DigitalInputState", "DIPort0"), np.array([1.0]) ) np.testing.assert_array_equal( - builder.get_lick_times("DigitalInputState", "DIPort1"), np.array([2.0, 2.5]) + builder.get_lick_times("HarpBehavior", "DigitalInputState", "DIPort1"), np.array([2.0, 2.5]) ) @@ -215,7 +215,7 @@ def test_get_lick_times_returns_empty_when_absent(): ) builder = AcquisitionBuilder(loader=_make_loader(dataset)) - result = builder.get_lick_times("DigitalInputState", "DIPort1") + result = builder.get_lick_times("HarpBehavior", "DigitalInputState", "DIPort1") assert isinstance(result, np.ndarray) assert result.size == 0 From 56179140888f91a3fe0cd30fbde1cd7877758c8d Mon Sep 17 00:00:00 2001 From: arjunsridhar12345 Date: Tue, 30 Jun 2026 10:27:04 -0700 Subject: [PATCH 28/36] refactor: minor update to passing in lick data --- .../nwb/acquisition/acquisition_builder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py b/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py index 2c1ba0d..3ac173c 100644 --- a/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py +++ b/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py @@ -228,8 +228,8 @@ def _reward_delivery_series( def build_acquisition( self, - left_lick: LickSource, - right_lick: LickSource, + left_lick: LickSource = LickSource("HarpBehavior", "DigitalInputState", "DIPort0"), + right_lick: LickSource = LickSource("HarpBehavior", "DigitalInputState", "DIPort1"), ) -> t.List[t.Union[AcquisitionSeries, AcquisitionTable]]: """Build the NWB acquisition entries. From 351e980e7ae620879e4454aad0a3152bf74d9a46 Mon Sep 17 00:00:00 2001 From: arjunsridhar12345 Date: Tue, 30 Jun 2026 16:34:40 -0700 Subject: [PATCH 29/36] build: update dependecies --- pyproject.toml | 2 +- uv.lock | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0aca370..81203f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ readme = "README.md" version = "0.0.0" dependencies = [ - "aind-behavior-dynamic-foraging[data] @ git+https://github.com/AllenNeuralDynamics/Aind.Behavior.DynamicForaging.git@v0.0.2", + "aind-behavior-dynamic-foraging[data] @ git+https://github.com/AllenNeuralDynamics/Aind.Behavior.DynamicForaging.git@f517d14ff965763a8f713b8a1be5dfb26b9312e1", "ipykernel", ] diff --git a/uv.lock b/uv.lock index 23aa179..3f9b950 100644 --- a/uv.lock +++ b/uv.lock @@ -39,7 +39,7 @@ wheels = [ [[package]] name = "aind-behavior-dynamic-foraging" version = "0.0.2" -source = { git = "https://github.com/AllenNeuralDynamics/Aind.Behavior.DynamicForaging.git?rev=v0.0.2#87a3b9d22cbe49b14c98d894622ac92be4ff1707" } +source = { git = "https://github.com/AllenNeuralDynamics/Aind.Behavior.DynamicForaging.git?rev=f517d14ff965763a8f713b8a1be5dfb26b9312e1#f517d14ff965763a8f713b8a1be5dfb26b9312e1" } dependencies = [ { name = "aind-behavior-services" }, { name = "pydantic-settings" }, @@ -400,7 +400,7 @@ wheels = [ [[package]] name = "contraqctor" -version = "0.5.8" +version = "0.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aind-behavior-services" }, @@ -417,9 +417,9 @@ dependencies = [ { name = "scipy" }, { name = "semver" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c1/26/c9459dc2036c902a05c9c66358bba9834d3c8d4ecee64c58b3d23d6800e6/contraqctor-0.5.8.tar.gz", hash = "sha256:a91797987f2ebc4ca1c6c4fdfc7867ef8ee7a566382e8ffe1c8c289cca61a9a8", size = 55929, upload-time = "2026-06-18T09:42:15.888Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/b3/60d189079b7f2f022d3eb5fc6ee2179b8c91e28a39e0d4b0e01dcb0effb6/contraqctor-0.6.0.tar.gz", hash = "sha256:a33b3337a687f18869bcfcafd5782d78e512d32ed2b0c42fd37df221f4d642a0", size = 58108, upload-time = "2026-06-30T21:23:56.72Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/9e/dc69736ba3f4c852e42299d4e2705202abcac44dc52b272019708628c93c/contraqctor-0.5.8-py3-none-any.whl", hash = "sha256:56d8e4ef87c3cbfe3c7dd6066c519fcb57341b6815ca4f7582290bd210ed4d65", size = 69225, upload-time = "2026-06-18T09:42:14.91Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/b733cd351469c8bd2fb2b5679d58132e68463f04193697986f7f731929f8/contraqctor-0.6.0-py3-none-any.whl", hash = "sha256:b62d21227487219a5745189b109002d548f3cb936df3698b278cdac4675d9cd4", size = 71990, upload-time = "2026-06-30T21:23:55.615Z" }, ] [[package]] @@ -570,7 +570,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "aind-behavior-dynamic-foraging", extras = ["data"], git = "https://github.com/AllenNeuralDynamics/Aind.Behavior.DynamicForaging.git?rev=v0.0.2" }, + { name = "aind-behavior-dynamic-foraging", extras = ["data"], git = "https://github.com/AllenNeuralDynamics/Aind.Behavior.DynamicForaging.git?rev=f517d14ff965763a8f713b8a1be5dfb26b9312e1" }, { name = "aind-data-schema", marker = "extra == 'qc'", specifier = ">=2.4.1" }, { name = "dynamic-foraging-processing", extras = ["qc"], marker = "extra == 'full'" }, { name = "ipykernel" }, @@ -1262,7 +1262,7 @@ name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess" }, + { name = "ptyprocess", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] 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 = [ From 5f7d7b242b4e7f8f12d4194339313a6f06f2a3b5 Mon Sep 17 00:00:00 2001 From: arjunsridhar12345 Date: Tue, 30 Jun 2026 16:35:57 -0700 Subject: [PATCH 30/36] feat: add utils to make data types nwb compatible --- .../nwb/acquisition/acquisition_builder.py | 23 ++--- src/dynamic_foraging_processing/nwb/utils.py | 90 +++++++++++++++++++ 2 files changed, 102 insertions(+), 11 deletions(-) create mode 100644 src/dynamic_foraging_processing/nwb/utils.py diff --git a/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py b/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py index 3ac173c..a9d104f 100644 --- a/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py +++ b/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py @@ -9,6 +9,7 @@ AcquisitionSeries, AcquisitionTable, ) +from dynamic_foraging_processing.nwb.utils import clean_dataframe_for_nwb from dynamic_foraging_processing.raw_data_loader import RawDataLoader from dynamic_foraging_processing.utils.rewards import get_annotated_rewards @@ -251,20 +252,20 @@ def build_acquisition( trial_outcomes = self.get_trial_outcomes() manual_water = self.get_manual_water_times() - # acquisition_streams = self.loader.get_all_raw_data() - # acqusition_streams_descriptions = self.loader.raw_data_stream_descriptions + acquisition_streams = self.loader.get_all_raw_data() + acqusition_streams_descriptions = self.loader.raw_data_stream_descriptions acquisiton_entries: t.List[t.Union[AcquisitionSeries, AcquisitionTable]] = [] - # for stream_name, stream_data in acquisition_streams.items(): - # description = acqusition_streams_descriptions.get(stream_name, "") - # acquisiton_entries.append( - # AcquisitionTable( - # name=stream_name, - # data=stream_data, - # description=description, - # ) - # ) + for stream_name, stream_data in acquisition_streams.items(): + description = acqusition_streams_descriptions.get(stream_name, "") + acquisiton_entries.append( + AcquisitionTable( + name=stream_name, + data=clean_dataframe_for_nwb(stream_data), + description=description, + ) + ) acquisiton_entries.append( self._reward_delivery_series( diff --git a/src/dynamic_foraging_processing/nwb/utils.py b/src/dynamic_foraging_processing/nwb/utils.py new file mode 100644 index 0000000..65e438a --- /dev/null +++ b/src/dynamic_foraging_processing/nwb/utils.py @@ -0,0 +1,90 @@ +"""Make raw stream frames safe to write to NWB.""" + +import json +from datetime import datetime +from enum import Enum +from typing import Any, Callable, Union + +import numpy as np +import pandas as pd + +_NestedStructureType = Union[dict, list, Any] + + +def convert_values_in_nested_structure( + data: _NestedStructureType, + check_fn: Callable[[Any], bool], + convert_fn: Callable[[Any], Any], +) -> _NestedStructureType: + """ + Recursively convert values in nested dictionaries/lists based on a condition. + + Parameters + ---------- + data : _NestedStructureType + Input data structure which may contain nested dictionaries and lists. + check_fn : Callable + Function that returns True if value should be converted. + convert_fn : Callable + Function that converts the value. + + Returns + ------- + _NestedStructureType + Data structure with converted values. + """ + if isinstance(data, dict): + return { + k: convert_values_in_nested_structure(v, check_fn, convert_fn) for k, v in data.items() + } + if isinstance(data, list): + return [convert_values_in_nested_structure(item, check_fn, convert_fn) for item in data] + return convert_fn(data) if check_fn(data) else data + + +def convert_datetimes_to_iso_string( + data: _NestedStructureType, +) -> _NestedStructureType: + """ + Convert datetime objects in a nested structure to ISO format strings. + + Parameters + ---------- + data : _NestedStructureType + Input data structure which may contain nested dictionaries and lists. + + Returns + ------- + _NestedStructureType + Data structure with datetime objects converted to ISO format strings. + """ + return convert_values_in_nested_structure( + data, + check_fn=lambda x: isinstance(x, datetime), + convert_fn=lambda x: x.isoformat(), + ) + + +def clean_dataframe_for_nwb(data: pd.DataFrame) -> pd.DataFrame: + """ + Clean a pandas DataFrame to ensure compatibility with NWB format. + + Parameters + ---------- + data : pd.DataFrame + The cleaned input DataFrame for NWB compatibility + + Returns + ------- + pd.DataFrame + A cleaned DataFrame that adheres to NWB data types + """ + for column in data.columns: + # convert to nwb allowable types + data[column] = data[column].replace({None: np.nan}) + data[column] = data[column].apply(lambda x: x.value if isinstance(x, Enum) else x) + data[column] = data[column].apply( + lambda x: json.dumps(convert_datetimes_to_iso_string(x)) if isinstance(x, dict) else x + ) + + return data From f53c6d0b50e0cb61e39a55bc560e235603ef9c8a Mon Sep 17 00:00:00 2001 From: arjunsridhar12345 Date: Tue, 30 Jun 2026 16:36:05 -0700 Subject: [PATCH 31/36] test: update tests --- tests/test_nwb/test_utils.py | 90 ++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 tests/test_nwb/test_utils.py diff --git a/tests/test_nwb/test_utils.py b/tests/test_nwb/test_utils.py new file mode 100644 index 0000000..16f0576 --- /dev/null +++ b/tests/test_nwb/test_utils.py @@ -0,0 +1,90 @@ +"""Tests for ``dynamic_foraging_processing.nwb.utils``.""" + +import json +from datetime import datetime +from enum import Enum + +import numpy as np +import pandas as pd + +from dynamic_foraging_processing.nwb.utils import ( + clean_dataframe_for_nwb, + convert_datetimes_to_iso_string, + convert_values_in_nested_structure, +) + + +class _Side(Enum): + """Sample enum used to check enum-to-value conversion.""" + + LEFT = "left" + RIGHT = "right" + + +def test_convert_values_in_nested_structure_recurses_dicts_and_lists(): + """Conversion reaches values nested inside dicts and lists, leaving others.""" + data = {"a": [1, 2], "b": {"c": 3, "d": 4}} + + result = convert_values_in_nested_structure( + data, + check_fn=lambda x: isinstance(x, int) and x % 2 == 0, + convert_fn=lambda x: x * 10, + ) + + assert result == {"a": [1, 20], "b": {"c": 3, "d": 40}} + + +def test_convert_values_in_nested_structure_scalar_passthrough(): + """A scalar that fails the check is returned unchanged.""" + assert ( + convert_values_in_nested_structure(5, check_fn=lambda x: False, convert_fn=lambda x: 0) == 5 + ) + + +def test_convert_datetimes_to_iso_string_nested(): + """Datetimes nested in dicts/lists become ISO strings; other values stay.""" + dt = datetime(2026, 6, 30, 12, 0, 0) + data = {"when": dt, "tags": [dt, "x"]} + + result = convert_datetimes_to_iso_string(data) + + assert result == {"when": "2026-06-30T12:00:00", "tags": ["2026-06-30T12:00:00", "x"]} + + +def test_clean_dataframe_replaces_none_with_nan(): + """``None`` values become ``NaN`` for NWB compatibility.""" + df = pd.DataFrame({"x": [1.0, None, 3.0]}) + + cleaned = clean_dataframe_for_nwb(df) + + assert cleaned["x"].isna().tolist() == [False, True, False] + assert cleaned["x"].dropna().tolist() == [1.0, 3.0] + + +def test_clean_dataframe_unwraps_enums(): + """Enum cells are replaced by their ``.value``.""" + df = pd.DataFrame({"side": [_Side.LEFT, _Side.RIGHT]}) + + cleaned = clean_dataframe_for_nwb(df) + + assert cleaned["side"].tolist() == ["left", "right"] + + +def test_clean_dataframe_serializes_dicts_with_datetimes(): + """Dict cells are JSON-encoded with nested datetimes converted to ISO.""" + df = pd.DataFrame({"payload": [{"t": datetime(2026, 6, 30), "n": 1}]}) + + cleaned = clean_dataframe_for_nwb(df) + + assert cleaned["payload"].iloc[0] == json.dumps({"t": "2026-06-30T00:00:00", "n": 1}) + + +def test_clean_dataframe_leaves_plain_columns(): + """Scalar non-dict, non-enum columns are left as-is.""" + df = pd.DataFrame({"n": [1, 2, 3], "s": ["a", "b", "c"]}) + + cleaned = clean_dataframe_for_nwb(df) + + assert cleaned["n"].tolist() == [1, 2, 3] + assert cleaned["s"].tolist() == ["a", "b", "c"] + assert not any(isinstance(v, np.ndarray) for v in cleaned["s"]) From 23006d1020fbe1b3274f09b600c0d9ad58b473bc Mon Sep 17 00:00:00 2001 From: arjunsridhar12345 Date: Tue, 30 Jun 2026 16:39:15 -0700 Subject: [PATCH 32/36] chore: lint --- src/dynamic_foraging_processing/utils/rewards.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/dynamic_foraging_processing/utils/rewards.py b/src/dynamic_foraging_processing/utils/rewards.py index 01ac7c9..e31d0fc 100644 --- a/src/dynamic_foraging_processing/utils/rewards.py +++ b/src/dynamic_foraging_processing/utils/rewards.py @@ -93,9 +93,7 @@ def get_annotated_rewards( # positions index into reward_times, i.e. the deliveries that are manual. manual_water_times = np.asarray(manual_water_times) if manual_water_times.size: - manual_indices_in_reward_times = find_closest_timestamps( - manual_water_times, reward_times - ) + manual_indices_in_reward_times = find_closest_timestamps(manual_water_times, reward_times) annotated_rewards[manual_indices_in_reward_times] = "manual" return annotated_rewards From 70384936ae16b175d3b28f165b3eaf8978724790 Mon Sep 17 00:00:00 2001 From: arjunsridhar12345 Date: Tue, 30 Jun 2026 17:01:10 -0700 Subject: [PATCH 33/36] fix: adapt to clean dict for nwb --- src/dynamic_foraging_processing/nwb/utils.py | 25 +++++++++++++++----- tests/test_nwb/test_utils.py | 14 +++++++++++ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/dynamic_foraging_processing/nwb/utils.py b/src/dynamic_foraging_processing/nwb/utils.py index 65e438a..846334b 100644 --- a/src/dynamic_foraging_processing/nwb/utils.py +++ b/src/dynamic_foraging_processing/nwb/utils.py @@ -65,26 +65,39 @@ def convert_datetimes_to_iso_string( ) -def clean_dataframe_for_nwb(data: pd.DataFrame) -> pd.DataFrame: +def _dict_to_json(value: dict) -> str: + """JSON-encode a dict, converting nested enums/datetimes first.""" + value = convert_values_in_nested_structure( + value, + check_fn=lambda x: isinstance(x, Enum), + convert_fn=lambda x: x.value, + ) + value = convert_datetimes_to_iso_string(value) + return json.dumps(value, default=str) + + +def clean_dataframe_for_nwb(data: Union[pd.DataFrame, dict]) -> pd.DataFrame: """ Clean a pandas DataFrame to ensure compatibility with NWB format. Parameters ---------- - data : pd.DataFrame - The cleaned input DataFrame for NWB compatibility + data : pd.DataFrame or dict + The input to clean for NWB compatibility. A dict is treated as a single + table row and wrapped into a one-row DataFrame. Returns ------- pd.DataFrame A cleaned DataFrame that adheres to NWB data types """ + if isinstance(data, dict): + data = pd.DataFrame([data]) + for column in data.columns: # convert to nwb allowable types data[column] = data[column].replace({None: np.nan}) data[column] = data[column].apply(lambda x: x.value if isinstance(x, Enum) else x) - data[column] = data[column].apply( - lambda x: json.dumps(convert_datetimes_to_iso_string(x)) if isinstance(x, dict) else x - ) + data[column] = data[column].apply(lambda x: _dict_to_json(x) if isinstance(x, dict) else x) return data diff --git a/tests/test_nwb/test_utils.py b/tests/test_nwb/test_utils.py index 16f0576..9db0390 100644 --- a/tests/test_nwb/test_utils.py +++ b/tests/test_nwb/test_utils.py @@ -79,6 +79,20 @@ def test_clean_dataframe_serializes_dicts_with_datetimes(): assert cleaned["payload"].iloc[0] == json.dumps({"t": "2026-06-30T00:00:00", "n": 1}) +def test_clean_dataframe_accepts_dict_as_single_row(): + """A dict input becomes a one-row DataFrame with cleaned values.""" + data = {"n": 1, "side": _Side.LEFT, "payload": {"t": datetime(2026, 6, 30), "k": _Side.RIGHT}} + + cleaned = clean_dataframe_for_nwb(data) + + assert isinstance(cleaned, pd.DataFrame) + assert len(cleaned) == 1 + assert cleaned["n"].iloc[0] == 1 + assert cleaned["side"].iloc[0] == "left" + # Nested enum and datetime inside the dict are converted before JSON-encoding. + assert cleaned["payload"].iloc[0] == json.dumps({"t": "2026-06-30T00:00:00", "k": "right"}) + + def test_clean_dataframe_leaves_plain_columns(): """Scalar non-dict, non-enum columns are left as-is.""" df = pd.DataFrame({"n": [1, 2, 3], "s": ["a", "b", "c"]}) From 9360d7e0e8ab531bf036e504dd61df67a33a683b Mon Sep 17 00:00:00 2001 From: arjunsridhar12345 Date: Tue, 30 Jun 2026 17:12:24 -0700 Subject: [PATCH 34/36] fix: allow for none type descriptions --- .../nwb/acquisition/acquisition_builder.py | 4 +++- .../test_acquisition/test_acquisition_builder.py | 11 +++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py b/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py index a9d104f..042b4cc 100644 --- a/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py +++ b/src/dynamic_foraging_processing/nwb/acquisition/acquisition_builder.py @@ -258,7 +258,9 @@ def build_acquisition( acquisiton_entries: t.List[t.Union[AcquisitionSeries, AcquisitionTable]] = [] for stream_name, stream_data in acquisition_streams.items(): - description = acqusition_streams_descriptions.get(stream_name, "") + description = acqusition_streams_descriptions.get(stream_name) + if description is None: + description = "" acquisiton_entries.append( AcquisitionTable( name=stream_name, diff --git a/tests/test_nwb/test_acquisition/test_acquisition_builder.py b/tests/test_nwb/test_acquisition/test_acquisition_builder.py index 0ea85c0..29d9b1c 100644 --- a/tests/test_nwb/test_acquisition/test_acquisition_builder.py +++ b/tests/test_nwb/test_acquisition/test_acquisition_builder.py @@ -263,3 +263,14 @@ def test_build_acquisition_returns_populated_list(): np.testing.assert_array_equal(right_lick.timestamps, np.array([2.0, 2.5])) np.testing.assert_array_equal(right_lick.data, np.array([True, True])) assert "DIPort1" in right_lick.description + + +def test_build_acquisition_defaults_none_description_to_empty_string(): + """A None stream description falls back to "" so the table validates.""" + loader = _make_loader() + loader.raw_data_stream_descriptions = {"Behavior.RawStream": None} + builder = AcquisitionBuilder(loader=loader) + + table = builder.build_acquisition()[0] + + assert table.description == "" From 1176632bc66a772b08cee2f328a61adda6e4c5eb Mon Sep 17 00:00:00 2001 From: arjunsridhar12345 Date: Tue, 30 Jun 2026 17:27:25 -0700 Subject: [PATCH 35/36] fix: convert pydantic model to nwb com --- src/dynamic_foraging_processing/nwb/utils.py | 12 ++++++++---- tests/test_nwb/test_utils.py | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/dynamic_foraging_processing/nwb/utils.py b/src/dynamic_foraging_processing/nwb/utils.py index 846334b..c714f62 100644 --- a/src/dynamic_foraging_processing/nwb/utils.py +++ b/src/dynamic_foraging_processing/nwb/utils.py @@ -7,6 +7,7 @@ import numpy as np import pandas as pd +from pydantic import BaseModel _NestedStructureType = Union[dict, list, Any] @@ -76,21 +77,24 @@ def _dict_to_json(value: dict) -> str: return json.dumps(value, default=str) -def clean_dataframe_for_nwb(data: Union[pd.DataFrame, dict]) -> pd.DataFrame: +def clean_dataframe_for_nwb(data: Union[pd.DataFrame, dict, BaseModel]) -> pd.DataFrame: """ Clean a pandas DataFrame to ensure compatibility with NWB format. Parameters ---------- - data : pd.DataFrame or dict - The input to clean for NWB compatibility. A dict is treated as a single - table row and wrapped into a one-row DataFrame. + data : pd.DataFrame, dict, or pydantic BaseModel + The input to clean for NWB compatibility. A pydantic model is dumped to + a dict, and a dict is treated as a single table row and wrapped into a + one-row DataFrame. Returns ------- pd.DataFrame A cleaned DataFrame that adheres to NWB data types """ + if isinstance(data, BaseModel): + data = data.model_dump() if isinstance(data, dict): data = pd.DataFrame([data]) diff --git a/tests/test_nwb/test_utils.py b/tests/test_nwb/test_utils.py index 9db0390..7e35eab 100644 --- a/tests/test_nwb/test_utils.py +++ b/tests/test_nwb/test_utils.py @@ -6,6 +6,7 @@ import numpy as np import pandas as pd +from pydantic import BaseModel from dynamic_foraging_processing.nwb.utils import ( clean_dataframe_for_nwb, @@ -93,6 +94,21 @@ def test_clean_dataframe_accepts_dict_as_single_row(): assert cleaned["payload"].iloc[0] == json.dumps({"t": "2026-06-30T00:00:00", "k": "right"}) +def test_clean_dataframe_accepts_pydantic_model(): + """A pydantic model input is dumped to a one-row DataFrame.""" + + class _Row(BaseModel): + n: int + side: _Side + + cleaned = clean_dataframe_for_nwb(_Row(n=1, side=_Side.LEFT)) + + assert isinstance(cleaned, pd.DataFrame) + assert len(cleaned) == 1 + assert cleaned["n"].iloc[0] == 1 + assert cleaned["side"].iloc[0] == "left" + + def test_clean_dataframe_leaves_plain_columns(): """Scalar non-dict, non-enum columns are left as-is.""" df = pd.DataFrame({"n": [1, 2, 3], "s": ["a", "b", "c"]}) From 81c759ae241bb46e21461e7e3a0b21a199af4304 Mon Sep 17 00:00:00 2001 From: arjunsridhar12345 Date: Tue, 30 Jun 2026 17:29:52 -0700 Subject: [PATCH 36/36] test: fix interrogate --- tests/test_nwb/test_utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_nwb/test_utils.py b/tests/test_nwb/test_utils.py index 7e35eab..3b35109 100644 --- a/tests/test_nwb/test_utils.py +++ b/tests/test_nwb/test_utils.py @@ -98,6 +98,8 @@ def test_clean_dataframe_accepts_pydantic_model(): """A pydantic model input is dumped to a one-row DataFrame.""" class _Row(BaseModel): + """Sample model row used to check model-to-DataFrame cleaning.""" + n: int side: _Side