From b17c5e255a31739dd66507f5f8c882c9775ec6ab Mon Sep 17 00:00:00 2001 From: Gautzilla Date: Thu, 18 Dec 2025 11:31:28 +0100 Subject: [PATCH 1/6] add Analysis zoom level parameter --- src/osekit/public_api/analysis.py | 74 ++++++++++++++++++++++++++++--- tests/test_public_api.py | 58 ++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 6 deletions(-) diff --git a/src/osekit/public_api/analysis.py b/src/osekit/public_api/analysis.py index 25344b0b..7a96f8d9 100644 --- a/src/osekit/public_api/analysis.py +++ b/src/osekit/public_api/analysis.py @@ -3,12 +3,14 @@ from enum import Flag, auto from typing import TYPE_CHECKING, Literal -from osekit.core_api.frequency_scale import Scale +from scipy.signal import ShortTimeFFT + from osekit.utils.audio_utils import Normalization if TYPE_CHECKING: from pandas import Timedelta, Timestamp - from scipy.signal import ShortTimeFFT + + from osekit.core_api.frequency_scale import Scale class AnalysisType(Flag): @@ -78,6 +80,8 @@ def __init__( colormap: str | None = None, scale: Scale | None = None, nb_ltas_time_bins: int | None = None, + zoom_levels: list[int] | None = None, + zoomed_fft: list[ShortTimeFFT] | None = None, ) -> None: """Initialize an Analysis object. @@ -141,6 +145,19 @@ def __init__( If None, the spectrogram will be computed regularly. If specified, the spectrogram will be computed as LTAS, with the value representing the maximum number of averaged time bins. + zoom_levels: list[int] | None + If specified, additional analyses datasets will be created at the requested + zoom levels. + e.g. with a data_duration of 10s and zoom_levels = [2,4], 3 SpectroDatasets + will be created, with data_duration = 5s and 2.5s. + This will only affect spectral exports, and if AnalysisType.AUDIO is + included in the analysis, zoomed SpectroDatasets will be linked to the + x1 zoom SpectroData. + zoomed_fft: list[ShortTimeFFT | None] + FFT to use for computing the zoomed spectra. + By default, SpectroDatasets with a zoomed factor z will use the + same FFT as the z=1 SpectroDataset, but with a hop that is + divided by z. """ self.analysis_type = analysis_type @@ -153,16 +170,22 @@ def __init__( self.name = name self.normalization = normalization self.subtype = subtype - self.fft = fft self.v_lim = v_lim self.colormap = colormap self.scale = scale self.nb_ltas_time_bins = nb_ltas_time_bins if self.is_spectro and fft is None: - raise ValueError( - "FFT parameter should be given if spectra outputs are selected.", - ) + msg = "FFT parameter should be given if spectra outputs are selected." + raise ValueError(msg) + + self.fft = fft + self.zoom_levels = list({1, *zoom_levels}) if zoom_levels else None + self.zoomed_fft = ( + zoomed_fft + if zoomed_fft + else self._get_zoomed_ffts(x1_fft=fft, zoom_levels=self.zoom_levels) + ) @property def is_spectro(self) -> bool: @@ -175,3 +198,42 @@ def is_spectro(self) -> bool: AnalysisType.WELCH, ) ) + + @staticmethod + def _get_zoomed_ffts( + x1_fft: ShortTimeFFT, + zoom_levels: list[int] | None, + ) -> list[ShortTimeFFT]: + """Compute the default FFTs to use for computing the zoomed spectra. + + By default, SpectroDatasets with a zoomed factor z will use the + same FFT as the z=1 SpectroDataset, but with a hop that is + divided by z. + + Parameters + ---------- + x1_fft: ShortTimeFFT + FFT used for computing the unzoomed spectra. + zoom_levels: list[int] | None + Additional zoom levels used for computing the spectra. + + Returns + ------- + list[ShortTimeFFT] + FFTs used for computing the zoomed spectra. + + """ + if not zoom_levels: + return [] + zoomed_ffts = [] + for zoom_level in zoom_levels: + if zoom_level == 1: + continue + zoomed_ffts.append( + ShortTimeFFT( + win=x1_fft.win, + hop=x1_fft.hop // zoom_level, + fs=x1_fft.fs, + ), + ) + return zoomed_ffts diff --git a/tests/test_public_api.py b/tests/test_public_api.py index 1add226e..70c72e1a 100644 --- a/tests/test_public_api.py +++ b/tests/test_public_api.py @@ -1411,3 +1411,61 @@ def test_spectro_analysis_with_existing_ads( assert ad.begin == sd.begin assert ad.end == sd.end assert sd.audio_data == ad + + +@pytest.mark.parametrize( + ("fft", "zoomed_levels", "expected"), + [ + pytest.param( + ShortTimeFFT(hamming(1024), hop=1024, fs=24_000), + None, + [], + id="no_zoom", + ), + pytest.param( + ShortTimeFFT(hamming(1024), hop=1024, fs=24_000), + [1], + [], + id="x1_zoom_only_equals_no_zoom", + ), + pytest.param( + ShortTimeFFT(hamming(1024), hop=1024, fs=24_000), + [2], + [ + ShortTimeFFT(hamming(1024), hop=512, fs=24_000), + ], + id="x2_zoom_only", + ), + pytest.param( + ShortTimeFFT(hamming(1024), hop=1024, fs=24_000), + [2, 4, 8], + [ + ShortTimeFFT(hamming(1024), hop=512, fs=24_000), + ShortTimeFFT(hamming(1024), hop=256, fs=24_000), + ShortTimeFFT(hamming(1024), hop=128, fs=24_000), + ], + id="multiple_zoom_levels", + ), + pytest.param( + ShortTimeFFT(hamming(1024), hop=1024, fs=24_000), + [3], + [ + ShortTimeFFT(hamming(1024), hop=341, fs=24_000), + ], + id="hop_is_rounded_down", + ), + ], +) +def test_default_zoomed_sft( + fft: ShortTimeFFT, + zoomed_levels: list[int] | None, + expected: list[ShortTimeFFT], +) -> None: + for sft, expected_sft in zip( + Analysis._get_zoomed_ffts(fft, zoomed_levels), + expected, + strict=True, + ): + assert np.array_equal(sft.win, expected_sft.win) + assert sft.hop == expected_sft.hop + assert sft.fs == expected_sft.fs From d0c7dfe7f40d85bd0976da98b301b482d70b1d41 Mon Sep 17 00:00:00 2001 From: Gautzilla Date: Fri, 19 Dec 2025 09:59:43 +0100 Subject: [PATCH 2/6] rename zoomed_ffts --- src/osekit/public_api/analysis.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/osekit/public_api/analysis.py b/src/osekit/public_api/analysis.py index 7a96f8d9..e343255d 100644 --- a/src/osekit/public_api/analysis.py +++ b/src/osekit/public_api/analysis.py @@ -81,7 +81,7 @@ def __init__( scale: Scale | None = None, nb_ltas_time_bins: int | None = None, zoom_levels: list[int] | None = None, - zoomed_fft: list[ShortTimeFFT] | None = None, + zoomed_ffts: list[ShortTimeFFT] | None = None, ) -> None: """Initialize an Analysis object. @@ -153,7 +153,7 @@ def __init__( This will only affect spectral exports, and if AnalysisType.AUDIO is included in the analysis, zoomed SpectroDatasets will be linked to the x1 zoom SpectroData. - zoomed_fft: list[ShortTimeFFT | None] + zoomed_ffts: list[ShortTimeFFT | None] FFT to use for computing the zoomed spectra. By default, SpectroDatasets with a zoomed factor z will use the same FFT as the z=1 SpectroDataset, but with a hop that is @@ -182,8 +182,8 @@ def __init__( self.fft = fft self.zoom_levels = list({1, *zoom_levels}) if zoom_levels else None self.zoomed_fft = ( - zoomed_fft - if zoomed_fft + zoomed_ffts + if zoomed_ffts else self._get_zoomed_ffts(x1_fft=fft, zoom_levels=self.zoom_levels) ) From 1830bf3d2148952854bc793ed4bb147fc5250d67 Mon Sep 17 00:00:00 2001 From: Gautzilla Date: Tue, 6 Jan 2026 12:40:20 +0100 Subject: [PATCH 3/6] add zoomed_sds from analysis --- ...mple_multiple_spectrograms_id_public.ipynb | 2 +- ...example_multiple_spectrograms_public.ipynb | 2 +- src/osekit/core_api/spectro_dataset.py | 117 ++++++++++++++++++ src/osekit/public_api/analysis.py | 49 +------- src/osekit/public_api/dataset.py | 35 ++++-- tests/test_public_api.py | 60 +-------- tests/test_spectro.py | 44 +++++++ 7 files changed, 192 insertions(+), 117 deletions(-) diff --git a/docs/source/example_multiple_spectrograms_id_public.ipynb b/docs/source/example_multiple_spectrograms_id_public.ipynb index 8e3e6d0f..68ea7b24 100644 --- a/docs/source/example_multiple_spectrograms_id_public.ipynb +++ b/docs/source/example_multiple_spectrograms_id_public.ipynb @@ -175,7 +175,7 @@ "source": [ "from pandas import Timedelta\n", "\n", - "spectro_dataset = dataset.get_analysis_spectrodataset(analysis)\n", + "spectro_dataset = dataset.get_analysis_spectrodatasets(analysis)\n", "\n", "for sd in spectro_dataset.data:\n", " sd.name = next(iter(sd.audio_data.files)).path.stem\n", diff --git a/docs/source/example_multiple_spectrograms_public.ipynb b/docs/source/example_multiple_spectrograms_public.ipynb index 47a3477d..ec240ab0 100644 --- a/docs/source/example_multiple_spectrograms_public.ipynb +++ b/docs/source/example_multiple_spectrograms_public.ipynb @@ -243,7 +243,7 @@ "source": [ "import matplotlib.pyplot as plt\n", "\n", - "analysis_spectro_dataset = dataset.get_analysis_spectrodataset(\n", + "analysis_spectro_dataset = dataset.get_analysis_spectrodatasets(\n", " analysis=analysis,\n", " audio_dataset=audio_dataset, # So that the filtered SpectroDataset is returned\n", ")\n", diff --git a/src/osekit/core_api/spectro_dataset.py b/src/osekit/core_api/spectro_dataset.py index 5bd9dbf7..0e979db9 100644 --- a/src/osekit/core_api/spectro_dataset.py +++ b/src/osekit/core_api/spectro_dataset.py @@ -384,6 +384,123 @@ def update(first: int, last: int) -> None: update(first=first, last=last) + def get_zoomed_spectro_datasets( + self, + zoom_levels: list[int], + zoom_ffts: list[ShortTimeFFT] | None, + ) -> dict[int, SpectroDataset]: + """Return all zoomed SpectroDatasets from the current SpectroDataset. + + For a given zoom level x, each SpectroData from the current SpectroDataset + will be split in x parts (that is, a 10s-long SpectroData with x=2 will lead to + two 5s-long SpectroDatas). + + Parameters + ---------- + zoom_levels: list[int] + All required zoom levels. + zoom_ffts: list[ShortTimeFFT]|None + FFTs to use for computing the zoomed SpectroDataset. + If None, will be defaulted as done in + the SpectroDataset._get_zoomed_fft() method. + + Returns + ------- + dict[int, SpectroDataset] + Dictionary where the key are the zoom levels, and the values are the + corresponding SpectroDatasets. + + """ + zoom_ffts = zoom_ffts if zoom_ffts is not None else [None] * len(zoom_levels) + output = {} + for zoom_level, zoom_fft in zip(zoom_levels, zoom_ffts, strict=True): + if zoom_level == 1: + continue + zoom_sds = self.get_zoomed_spectro_dataset( + zoom_level=zoom_level, + zoom_fft=zoom_fft, + ) + output[zoom_level] = zoom_sds + return output + + def get_zoomed_spectro_dataset( + self, + zoom_level: int, + zoom_fft: ShortTimeFFT | None = None, + ) -> SpectroDataset: + """Return a zoomed SpectroDataset from the current SpectroDataset. + + For a given zoom level x, each SpectroData from the current SpectroDataset + will be split in x parts (that is, a 10s-long SpectroData with x=2 will lead to + two 5s-long SpectroDatas). + + Parameters + ---------- + zoom_level: int + Zoom level of the output SpectroDataset. + Each SpectroData from the current SpectroDataset will be split in + zoom_level equal-duration parts. + zoom_fft: ShortTimeFFT | None + FFT to use for computing the zoomed SpectroDataset. + If None, will be defaulted as done in + the SpectroDataset._get_zoomed_fft() method. + + Returns + ------- + SpectroDataset: + Zoomed SpectroDataset from the current SpectroDataset. + + """ + if zoom_level == 1: + return self + + zoom_fft = zoom_fft or self._get_zoomed_fft(zoom_level=zoom_level) + + zoomed_sds = SpectroDataset( + [zoomed_sd for sd in self.data for zoomed_sd in sd.split(zoom_level)], + name=f"{self.name}_x{zoom_level}", + suffix=self.suffix, + scale=self.scale, + v_lim=self.v_lim, + ) + zoomed_sds.fft = zoom_fft + + return zoomed_sds + + def _get_zoomed_fft( + self, + zoom_level: int, + ) -> ShortTimeFFT: + """Compute the default FFT to use for computing the zoomed spectra. + + By default, SpectroDatasets with a zoomed factor z will use the + same FFT as the z=1 SpectroDataset, but with a hop that is + divided by z. + + Parameters + ---------- + zoom_level: int + Zoom level used for computing the spectra. + + Returns + ------- + ShortTimeFFT + FFT used for computing the zoomed spectra. + + """ + if zoom_level < 1: + msg = f"Invalid zoom level {zoom_level}." + raise ValueError(msg) + + if zoom_level == 1: + return self.fft + + return ShortTimeFFT( + win=self.fft.win, + hop=self.fft.hop // zoom_level, + fs=self.fft.fs, + ) + def to_dict(self) -> dict: """Serialize a SpectroDataset to a dictionary. diff --git a/src/osekit/public_api/analysis.py b/src/osekit/public_api/analysis.py index e343255d..45947087 100644 --- a/src/osekit/public_api/analysis.py +++ b/src/osekit/public_api/analysis.py @@ -81,7 +81,7 @@ def __init__( scale: Scale | None = None, nb_ltas_time_bins: int | None = None, zoom_levels: list[int] | None = None, - zoomed_ffts: list[ShortTimeFFT] | None = None, + zoom_ffts: list[ShortTimeFFT] | None = None, ) -> None: """Initialize an Analysis object. @@ -153,7 +153,7 @@ def __init__( This will only affect spectral exports, and if AnalysisType.AUDIO is included in the analysis, zoomed SpectroDatasets will be linked to the x1 zoom SpectroData. - zoomed_ffts: list[ShortTimeFFT | None] + zoom_ffts: list[ShortTimeFFT | None] FFT to use for computing the zoomed spectra. By default, SpectroDatasets with a zoomed factor z will use the same FFT as the z=1 SpectroDataset, but with a hop that is @@ -181,11 +181,7 @@ def __init__( self.fft = fft self.zoom_levels = list({1, *zoom_levels}) if zoom_levels else None - self.zoomed_fft = ( - zoomed_ffts - if zoomed_ffts - else self._get_zoomed_ffts(x1_fft=fft, zoom_levels=self.zoom_levels) - ) + self.zoom_ffts = zoom_ffts @property def is_spectro(self) -> bool: @@ -198,42 +194,3 @@ def is_spectro(self) -> bool: AnalysisType.WELCH, ) ) - - @staticmethod - def _get_zoomed_ffts( - x1_fft: ShortTimeFFT, - zoom_levels: list[int] | None, - ) -> list[ShortTimeFFT]: - """Compute the default FFTs to use for computing the zoomed spectra. - - By default, SpectroDatasets with a zoomed factor z will use the - same FFT as the z=1 SpectroDataset, but with a hop that is - divided by z. - - Parameters - ---------- - x1_fft: ShortTimeFFT - FFT used for computing the unzoomed spectra. - zoom_levels: list[int] | None - Additional zoom levels used for computing the spectra. - - Returns - ------- - list[ShortTimeFFT] - FFTs used for computing the zoomed spectra. - - """ - if not zoom_levels: - return [] - zoomed_ffts = [] - for zoom_level in zoom_levels: - if zoom_level == 1: - continue - zoomed_ffts.append( - ShortTimeFFT( - win=x1_fft.win, - hop=x1_fft.hop // zoom_level, - fs=x1_fft.fs, - ), - ) - return zoomed_ffts diff --git a/src/osekit/public_api/dataset.py b/src/osekit/public_api/dataset.py index e284350d..030f7acb 100644 --- a/src/osekit/public_api/dataset.py +++ b/src/osekit/public_api/dataset.py @@ -258,12 +258,18 @@ def get_analysis_audiodataset(self, analysis: Analysis) -> AudioDataset: return ads - def get_analysis_spectrodataset( + def get_analysis_spectrodatasets( self, analysis: Analysis, audio_dataset: AudioDataset | None = None, - ) -> SpectroDataset | LTASDataset: - """Return a SpectroDataset (or LTASDataset) created from analysis parameters. + ) -> tuple[ + SpectroDataset | LTASDataset, + dict[int, list[SpectroDataset | LTASDataset]], + ]: + """Return SpectroDatasets (or LTASDatasets) created from analysis parameters. + + The output contains the unzoomed dataset (matching the analysis data_duration) plus + the potential zoomed datasets. Parameters ---------- @@ -276,11 +282,14 @@ def get_analysis_spectrodataset( Returns ------- - SpectroDataset | LTASDataset: - The SpectroDataset that match the analysis parameters. - This SpectroDataset can be used, for example, to have a peek at the + tuple[SpectroDataset | LTASDataset, dict[int,list[SpectroDataset | LTASDataset]]]: + SpectroDatasets that match the analysis parameters. + The first element of the tuple is the unzoomed analysis dataset. + The second element of the tuple is a dict, with the key + being the zoom level and the value the corresponding analysis dataset. + These SpectroDataset can be used, for example, to have a peek at the analysis output before running it. - If Analysis.is_ltas is True, a LTASDataset is returned. + If Analysis.nb_ltas_time_bins is not None, a LTASDataset is returned. """ if analysis.fft is None: @@ -308,7 +317,13 @@ def get_analysis_spectrodataset( nb_time_bins=analysis.nb_ltas_time_bins, ) - return sds + if analysis.zoom_levels is None: + return sds, {} + + return sds, sds.get_zoomed_spectro_datasets( + zoom_levels=analysis.zoom_levels, + zoom_ffts=analysis.zoom_ffts, + ) def run_analysis( self, @@ -364,8 +379,8 @@ def run_analysis( sds = None if analysis.is_spectro: - sds = ( - self.get_analysis_spectrodataset( + sds, zoomed_sds = ( + self.get_analysis_spectrodatasets( analysis=analysis, audio_dataset=ads, ) diff --git a/tests/test_public_api.py b/tests/test_public_api.py index 70c72e1a..85e53073 100644 --- a/tests/test_public_api.py +++ b/tests/test_public_api.py @@ -1036,7 +1036,7 @@ def test_get_analysis_spectrodataset( ) dataset.build() - analysis_sds = dataset.get_analysis_spectrodataset(analysis=analysis) + analysis_sds, _ = dataset.get_analysis_spectrodatasets(analysis=analysis) assert all( ad.begin == e.begin and ad.end == e.end @@ -1411,61 +1411,3 @@ def test_spectro_analysis_with_existing_ads( assert ad.begin == sd.begin assert ad.end == sd.end assert sd.audio_data == ad - - -@pytest.mark.parametrize( - ("fft", "zoomed_levels", "expected"), - [ - pytest.param( - ShortTimeFFT(hamming(1024), hop=1024, fs=24_000), - None, - [], - id="no_zoom", - ), - pytest.param( - ShortTimeFFT(hamming(1024), hop=1024, fs=24_000), - [1], - [], - id="x1_zoom_only_equals_no_zoom", - ), - pytest.param( - ShortTimeFFT(hamming(1024), hop=1024, fs=24_000), - [2], - [ - ShortTimeFFT(hamming(1024), hop=512, fs=24_000), - ], - id="x2_zoom_only", - ), - pytest.param( - ShortTimeFFT(hamming(1024), hop=1024, fs=24_000), - [2, 4, 8], - [ - ShortTimeFFT(hamming(1024), hop=512, fs=24_000), - ShortTimeFFT(hamming(1024), hop=256, fs=24_000), - ShortTimeFFT(hamming(1024), hop=128, fs=24_000), - ], - id="multiple_zoom_levels", - ), - pytest.param( - ShortTimeFFT(hamming(1024), hop=1024, fs=24_000), - [3], - [ - ShortTimeFFT(hamming(1024), hop=341, fs=24_000), - ], - id="hop_is_rounded_down", - ), - ], -) -def test_default_zoomed_sft( - fft: ShortTimeFFT, - zoomed_levels: list[int] | None, - expected: list[ShortTimeFFT], -) -> None: - for sft, expected_sft in zip( - Analysis._get_zoomed_ffts(fft, zoomed_levels), - expected, - strict=True, - ): - assert np.array_equal(sft.win, expected_sft.win) - assert sft.hop == expected_sft.hop - assert sft.fs == expected_sft.fs diff --git a/tests/test_spectro.py b/tests/test_spectro.py index 062fb6a6..bd00c432 100644 --- a/tests/test_spectro.py +++ b/tests/test_spectro.py @@ -1224,3 +1224,47 @@ def mocked_ad_init( assert ad.begin == sd.begin assert ad.end == sd.end + + +@pytest.mark.parametrize( + ("fft", "zoom_level", "expected"), + [ + pytest.param( + ShortTimeFFT(hamming(1024), hop=1024, fs=24_000), + 1, + ShortTimeFFT(hamming(1024), hop=1024, fs=24_000), + id="x1_zoom_only_equals_no_zoom", + ), + pytest.param( + ShortTimeFFT(hamming(1024), hop=1024, fs=24_000), + 2, + ShortTimeFFT(hamming(1024), hop=512, fs=24_000), + id="x2_zoom", + ), + pytest.param( + ShortTimeFFT(hamming(1024), hop=1024, fs=24_000), + 8, + ShortTimeFFT(hamming(1024), hop=128, fs=24_000), + id="x8_zoom", + ), + pytest.param( + ShortTimeFFT(hamming(1024), hop=1024, fs=24_000), + 3, + ShortTimeFFT(hamming(1024), hop=341, fs=24_000), + id="hop_is_rounded_down", + ), + ], +) +def test_get_zoom_fft( + patch_audio_data: pytest.MonkeyPatch, + fft: ShortTimeFFT, + zoom_level: int, + expected: ShortTimeFFT, +) -> None: + sds = SpectroDataset( + [SpectroData.from_audio_data(AudioData(mocked_value=[]), fft=fft)], + ) + zoom_fft = sds._get_zoomed_fft(zoom_level) + assert np.array_equal(zoom_fft.win, expected.win) + assert zoom_fft.hop == expected.hop + assert zoom_fft.fs == expected.fs From 499f59939cdcec74ee7e729962555078d13cb1eb Mon Sep 17 00:00:00 2001 From: Gautzilla Date: Tue, 6 Jan 2026 13:57:44 +0100 Subject: [PATCH 4/6] add zoom levels and references to public dataset json --- src/osekit/public_api/dataset.py | 53 +++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/src/osekit/public_api/dataset.py b/src/osekit/public_api/dataset.py index 030f7acb..5b0fc03b 100644 --- a/src/osekit/public_api/dataset.py +++ b/src/osekit/public_api/dataset.py @@ -378,8 +378,9 @@ def run_analysis( self._add_audio_dataset(ads=ads, analysis_name=analysis.name) sds = None + zoom_sdses = {} if analysis.is_spectro: - sds, zoomed_sds = ( + sds, zoom_sdses = ( self.get_analysis_spectrodatasets( analysis=analysis, audio_dataset=ads, @@ -388,16 +389,24 @@ def run_analysis( else spectro_dataset ) self._add_spectro_dataset(sds=sds, analysis_name=analysis.name) + for zoom_level, zoom_sds in zoom_sdses.items(): + self._add_spectro_dataset( + sds=zoom_sds, + analysis_name=analysis.name, + zoom_level=zoom_level, + zoom_reference=sds.name, + ) - self.export_analysis( - analysis_type=analysis.analysis_type, - ads=ads, - sds=sds, - link=True, - subtype=analysis.subtype, - nb_jobs=nb_jobs, - name=analysis.name, - ) + for analysis_sds in [sds, *list(zoom_sdses.values())]: + self.export_analysis( + analysis_type=analysis.analysis_type, + ads=ads, + sds=analysis_sds, + link=True, + subtype=analysis.subtype, + nb_jobs=nb_jobs, + name=analysis.name, + ) self.write_json() @@ -552,12 +561,16 @@ def _add_spectro_dataset( self, sds: SpectroDataset | LTASDataset, analysis_name: str, + zoom_level: int = 1, + zoom_reference: str | None = None, ) -> None: sds.folder = self._get_spectro_dataset_subpath(sds=sds) self.datasets[sds.name] = { "class": type(sds).__name__, "dataset": sds, "analysis": analysis_name, + "zoom_level": zoom_level, + "zoom_reference": zoom_reference, } sds.write_json(sds.folder) @@ -717,11 +730,7 @@ def to_dict(self) -> dict: """ return { "datasets": { - name: { - "class": dataset["class"], - "analysis": dataset["analysis"], - "json": str(dataset["dataset"].folder / f"{name}.json"), - } + name: self.analysis_dataset_to_dict(name=name) for name, dataset in self.datasets.items() }, "instrument": ( @@ -733,6 +742,20 @@ def to_dict(self) -> dict: "timezone": self.timezone, } + def analysis_dataset_to_dict(self, name: str) -> dict: + dataset = self.datasets[name] + output = { + "class": dataset["class"], + "analysis": dataset["analysis"], + "json": str(dataset["dataset"].folder / f"{name}.json"), + } + if type(dataset) in (SpectroDataset, LTASDataset): + output |= { + "zoom_level": dataset["zoom_level"], + "zoom_reference": dataset["zoom_reference"], + } + return output + @classmethod def from_dict(cls, dictionary: dict) -> Dataset: """Deserialize a dataset from a dictionary. From dc05644c16c074f72232de174e2001f8161ee9d8 Mon Sep 17 00:00:00 2001 From: Gautzilla Date: Tue, 6 Jan 2026 16:33:08 +0100 Subject: [PATCH 5/6] fix zoom level and reference json writing --- src/osekit/public_api/dataset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/osekit/public_api/dataset.py b/src/osekit/public_api/dataset.py index 5b0fc03b..fdfe3852 100644 --- a/src/osekit/public_api/dataset.py +++ b/src/osekit/public_api/dataset.py @@ -749,7 +749,7 @@ def analysis_dataset_to_dict(self, name: str) -> dict: "analysis": dataset["analysis"], "json": str(dataset["dataset"].folder / f"{name}.json"), } - if type(dataset) in (SpectroDataset, LTASDataset): + if type(dataset["dataset"]) in (SpectroDataset, LTASDataset): output |= { "zoom_level": dataset["zoom_level"], "zoom_reference": dataset["zoom_reference"], From 9e96213c250ba1bd27b73c583b4eb803eb3f6254 Mon Sep 17 00:00:00 2001 From: Gautzilla Date: Tue, 6 Jan 2026 16:40:07 +0100 Subject: [PATCH 6/6] deserialize zoom infos --- src/osekit/public_api/dataset.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/osekit/public_api/dataset.py b/src/osekit/public_api/dataset.py index fdfe3852..2b8fbe1d 100644 --- a/src/osekit/public_api/dataset.py +++ b/src/osekit/public_api/dataset.py @@ -787,6 +787,9 @@ def from_dict(cls, dictionary: dict) -> Dataset: "analysis": dataset["analysis"], "dataset": dataset_class.from_json(Path(dataset["json"])), } + for zoom_info in ("zoom_level", "zoom_reference"): + if zoom_info in dataset: + datasets[name][zoom_info] = dataset[zoom_info] return cls( folder=Path(), instrument=Instrument.from_dict(dictionary["instrument"]),