diff --git a/decode/convert.py b/decode/convert.py index 98a0c15..659f2bb 100644 --- a/decode/convert.py +++ b/decode/convert.py @@ -112,10 +112,10 @@ def to_brightness( if dems.long_name == "Brightness" or dems.units == "K": LOGGER.warning("DEMS may already be on the brightness temperature scale.") - if all(np.isnan(T_room_ := dems.aste_cabin_temperature)): + if np.isnan(T_room_ := dems.aste_cabin_temperature.mean().data): T_room_ = T_room - if all(np.isnan(T_amb_ := dems.temperature)): + if np.isnan(T_amb_ := dems.temperature.mean().data): T_amb_ = T_amb fwd = dems.d2_resp_fwd.data @@ -158,10 +158,10 @@ def to_dfof( if dems.long_name == "df/f" or dems.units == "dimensionless": LOGGER.warning("DEMS may already be on the df/f scale.") - if all(np.isnan(T_room_ := dems.aste_cabin_temperature)): + if np.isnan(T_room_ := dems.aste_cabin_temperature.mean().data): T_room_ = T_room - if all(np.isnan(T_amb_ := dems.temperature)): + if np.isnan(T_amb_ := dems.temperature.mean().data): T_amb_ = T_amb fwd = dems.d2_resp_fwd.data diff --git a/decode/correct.py b/decode/correct.py index ab09b80..78461a6 100644 --- a/decode/correct.py +++ b/decode/correct.py @@ -44,9 +44,9 @@ def for_atmosphere( DEMS DataArray of the on-source samples in the Ta* scale. """ - Tb_on = select.by(dems, "state", include=include_on) - Tb_off = select.by(dems, "state", include=include_off) - Tb_r = select.by(dems, "state", include=include_r) + Tb_on = dems.sel(time=dems.state.isin(include_on)) + Tb_off = dems.sel(time=dems.state.isin(include_off)) + Tb_r = dems.sel(time=dems.state.isin(include_r)) Tb_off_mean = Tb_off.groupby("scan").map(mean_in_time) Tb_r_mean = Tb_r.groupby("scan").map(mean_in_time) diff --git a/decode/load.py b/decode/load.py index 6e1102a..c026bd7 100644 --- a/decode/load.py +++ b/decode/load.py @@ -2,8 +2,10 @@ # standard library +from collections.abc import Sequence +from os import PathLike from pathlib import Path -from typing import Any, Literal, Union +from typing import Any, Literal, Optional, Union from warnings import catch_warnings, simplefilter @@ -11,6 +13,9 @@ import numpy as np import pandas as pd import xarray as xr +from astropy.units import Quantity +from ndtools import Range +from . import convert # constants @@ -66,25 +71,64 @@ def atm(*, type: Literal["eta", "tau"] = "tau") -> xr.DataArray: raise ValueError("Type must be either eta or tau.") -def dems(dems: Union[Path, str], /, **options: Any) -> xr.DataArray: +def dems( + dems: Union[PathLike[str], str], + /, + *, + # options for data selection + include_mkid_types: Optional[Sequence[str]] = ("filter",), + exclude_mkid_types: Optional[Sequence[str]] = None, + include_mkid_ids: Optional[Sequence[int]] = None, + exclude_mkid_ids: Optional[Sequence[int]] = None, + min_frequency: Optional[str] = None, + max_frequency: Optional[str] = None, + # options for coordinate conversion + frequency_units: Optional[str] = "GHz", + skycoord_units: Optional[str] = "arcsec", + skycoord_frame: Optional[str] = None, + # options for data conversion + data_scaling: Optional[Literal["brightness", "df/f"]] = None, + T_amb: float = 273.0, # K + T_room: float = 293.0, # K + # other options for loading + **options: Any, +) -> xr.DataArray: """Load a DEMS file as a DataArray. Args: dems: Path of the DEMS file. - - Keyword Args: - options: Arguments to be passed to ``xarray.open_dataarray``. + include_mkid_types: MKID types to be included. + Defaults to filter-only. + exclude_mkid_types: MKID types to be excluded. + Defaults to no MKID types. + include_mkid_ids: MKID IDs to be included. + Defaults to all MKID IDs. + exclude_mkid_ids: MKID IDs to be excluded. + Defaults to no MKID IDs. + min_frequency: Minimum frequency to be included. + Defaults to no minimum frequency bound. + max_frequency: Maximum frequency to be included. + Defaults to no maximum frequency bound. + frequency_units: Units of the frequency-related coordinates. + Defaults to GHz. + skycoord_units: Units of the skycoord-related coordinates. + Defaults to arcsec. + skycoord_frame: Frame of the skycoord. + Defaults to the skycoord of the input DEMS file. + data_scaling: Data scaling (either brightness or df/f). + Defaults to the data scaling of the input DEMS file. + T_amb: Default ambient temperature value for the data scaling + to be used when the ``temperature`` coordinate is all-NaN. + T_room: Default room temperature value for the data scaling + to be used when the ``aste_cabin_temperature`` coordinate is all-NaN. + **options: Other options for loading (e.g. ``chunks=None``, etc). Return: - Loaded DEMS DataArray. - - Raises: - ValueError: Raised if the file type is not supported. + DataArray of the loaded DEMS file. """ - suffixes = Path(dems).suffixes - - if NETCDF_SUFFIX in suffixes: + # load DEMS as DataArray + if NETCDF_SUFFIX in (suffixes := Path(dems).suffixes): options = { "engine": NETCDF_ENGINE, **options, @@ -101,4 +145,59 @@ def dems(dems: Union[Path, str], /, **options: Any) -> xr.DataArray: "Use netCDF (.nc) or Zarr (.zarr, .zarr.zip)." ) - return xr.open_dataarray(dems, **options) + da = xr.open_dataarray(dems, **options) + + # load DataArray coordinates on memory + for name in da.coords: + da.coords[name].load() + + # select DataArray by MKID types + if include_mkid_types is not None: + da = da.sel(chan=da.d2_mkid_type.isin(include_mkid_types)) + + if exclude_mkid_types is not None: + da = da.sel(chan=~da.d2_mkid_type.isin(exclude_mkid_types)) + + # select DataArray by MKID master IDs + if include_mkid_ids is not None: + da = da.sel(chan=da.d2_mkid_id.isin(include_mkid_ids)) + + if exclude_mkid_ids is not None: + da = da.sel(chan=~da.d2_mkid_id.isin(exclude_mkid_ids)) + + # select DataArray by frequency range + if min_frequency is not None: + min_frequency = Quantity(min_frequency).to(da.frequency.units).value + + if max_frequency is not None: + max_frequency = Quantity(max_frequency).to(da.frequency.units).value + + if min_frequency is not None or max_frequency is not None: + da = da.sel(chan=da.frequency == Range(min_frequency, max_frequency)) + + # convert frequency units + if frequency_units is not None: + da = convert.coord_units( + da, + ["bandwidth", "frequency", "d2_mkid_frequency"], + frequency_units, + ) + + # convert skycoord units and frame + if skycoord_units is not None: + da = convert.coord_units( + da, + ["lat", "lat_origin", "lon", "lon_origin"], + skycoord_units, + ) + + if skycoord_frame is not None: + da = convert.frame(da, skycoord_frame) + + # convert data scaling + if data_scaling == "brightness": + return convert.to_brightness(da, T_amb=T_amb, T_room=T_room) + elif data_scaling == "df/f": + return convert.to_dfof(da, T_amb=T_amb, T_room=T_room) + else: + return da diff --git a/decode/qlook.py b/decode/qlook.py index cac0aac..4de2aa4 100644 --- a/decode/qlook.py +++ b/decode/qlook.py @@ -17,7 +17,7 @@ from contextlib import contextmanager from logging import DEBUG, basicConfig, getLogger from pathlib import Path -from typing import Any, Literal, Optional, Sequence, Union, cast +from typing import Any, Literal, Optional, Sequence, Union from warnings import catch_warnings, simplefilter from datetime import datetime @@ -37,7 +37,7 @@ ABBA_PHASES = {0, 1, 2, 3} DATA_FORMATS = "csv", "nc", "zarr", "zarr.zip" TEXT_FORMATS = ("toml",) -DEFAULT_DATA_TYPE = "auto" +DEFAULT_DATA_SCALING = None DEFAULT_DEBUG = False DEFAULT_FIGSIZE = 12, 4 DEFAULT_FORMAT = "png" @@ -85,7 +85,7 @@ def auto(dems: Path, /, **options: Any) -> Path: """ with xr.set_options(keep_attrs=True): da = load.dems(dems, chunks=None) - obs: str = da.attrs["observation"] + obs: str = da.aste_obs_file if "daisy" in obs: return daisy(dems, **options) @@ -126,7 +126,7 @@ def daisy( exclude_mkid_ids: Optional[Sequence[int]] = DEFAULT_EXCL_MKID_IDS, min_frequency: Optional[str] = DEFAULT_MIN_FREQUENCY, max_frequency: Optional[str] = DEFAULT_MAX_FREQUENCY, - data_type: Literal["auto", "brightness", "df/f"] = DEFAULT_DATA_TYPE, + data_scaling: Optional[Literal["brightness", "df/f"]] = DEFAULT_DATA_SCALING, # options for analysis rolling_time: int = DEFAULT_ROLLING_TIME, source_radius: str = "60 arcsec", @@ -155,8 +155,8 @@ def daisy( Defaults to no minimum frequency bound. max_frequency: Maximum frequency to be included in analysis. Defaults to no maximum frequency bound. - data_type: Data type of the input DEMS file. - Defaults to the ``long_name`` attribute in it. + data_scaling: Data scaling to be used in analysis. + Defaults to the data scaling of the input DEMS file. rolling_time: Moving window size. source_radius: Radius of the on-source area. Other areas are considered off-source in sky subtraction. @@ -185,16 +185,18 @@ def daisy( LOGGER.debug(f"{key}: {val!r}") with xr.set_options(keep_attrs=True): - da = load_dems( + da = load.dems( dems, include_mkid_ids=include_mkid_ids, exclude_mkid_ids=exclude_mkid_ids, min_frequency=min_frequency, max_frequency=max_frequency, - data_type=data_type, + data_scaling=data_scaling, skycoord_units=skycoord_units, + skycoord_frame="relative", + chunks=None, ) - da = select.by(da, "state", exclude="GRAD") + da = da.sel(time=da.state != "GRAD") ### Rolling da_rolled = da.rolling(time=int(rolling_time), center=True).mean() @@ -210,8 +212,8 @@ def daisy( assign.scan(da, by="state", inplace=True) # subtract temporal baseline - da_on = select.by(da, "state", include="SCAN@ON") - da_off = select.by(da, "state", exclude="SCAN@ON") + da_on = da.sel(time=da.state == "SCAN@ON") + da_off = da.sel(time=da.state != "SCAN@ON") da_base = ( da_off.groupby("scan") .map(mean_in_time) @@ -277,7 +279,7 @@ def daisy( ax = axes[0] # type: ignore plot.data(series, ax=ax) - ax.set_title(f"{Path(dems).name}\n({da.observation})") + ax.set_title(f"{Path(dems).name}\n({da.aste_obs_file})") ax = axes[1] # type: ignore map_lim = max(abs(cube.lon).max(), abs(cube.lat).max()) @@ -335,7 +337,7 @@ def pswsc( exclude_mkid_ids: Optional[Sequence[int]] = DEFAULT_EXCL_MKID_IDS, min_frequency: Optional[str] = DEFAULT_MIN_FREQUENCY, max_frequency: Optional[str] = DEFAULT_MAX_FREQUENCY, - data_type: Literal["auto", "brightness", "df/f"] = DEFAULT_DATA_TYPE, + data_scaling: Optional[Literal["brightness", "df/f"]] = DEFAULT_DATA_SCALING, frequency_units: str = DEFAULT_FREQUENCY_UNITS, # options for saving format: str = DEFAULT_FORMAT, @@ -358,8 +360,8 @@ def pswsc( Defaults to no minimum frequency bound. max_frequency: Maximum frequency to be included in analysis. Defaults to no maximum frequency bound. - data_type: Data type of the input DEMS file. - Defaults to the ``long_name`` attribute in it. + data_scaling: Data scaling to be used in analysis. + Defaults to the data scaling of the input DEMS file. frequency_units: Units of the frequency axis. format: Output data format of the quick-look result. outdir: Output directory for the quick-look result. @@ -377,20 +379,22 @@ def pswsc( LOGGER.debug(f"{key}: {val!r}") with xr.set_options(keep_attrs=True): - da = load_dems( + da = load.dems( dems, include_mkid_ids=include_mkid_ids, exclude_mkid_ids=exclude_mkid_ids, min_frequency=min_frequency, max_frequency=max_frequency, - data_type=data_type, + data_scaling=data_scaling, frequency_units=frequency_units, + skycoord_frame="relative", + chunks=None, ) da_despiked = despike(da) # calculate ABBA cycles and phases - da_onoff = select.by(da_despiked, "state", ["ON", "OFF"]) + da_onoff = da_despiked.sel(time=da_despiked.state.isin(["ON", "OFF"])) scan_onoff = utils.phaseof(da_onoff.state) chop_per_scan = da_onoff.beam.groupby(scan_onoff).apply(utils.phaseof) is_second_half = chop_per_scan.groupby(scan_onoff).apply( @@ -424,7 +428,7 @@ def pswsc( ax.set_ylim(get_robust_lim(spec)) for ax in axes: # type: ignore - ax.set_title(f"{Path(dems).name}\n({da.observation})") + ax.set_title(f"{Path(dems).name}\n({da.aste_obs_file})") ax.grid(True) fig.tight_layout() @@ -440,7 +444,7 @@ def raster( exclude_mkid_ids: Optional[Sequence[int]] = DEFAULT_EXCL_MKID_IDS, min_frequency: Optional[str] = DEFAULT_MIN_FREQUENCY, max_frequency: Optional[str] = DEFAULT_MAX_FREQUENCY, - data_type: Literal["auto", "brightness", "df/f"] = DEFAULT_DATA_TYPE, + data_scaling: Optional[Literal["brightness", "df/f"]] = DEFAULT_DATA_SCALING, # options for analysis chan_weight: Literal["uniform", "std", "std/tx"] = "std/tx", pwv: Literal["0.5", "1.0", "2.0", "3.0", "4.0", "5.0"] = "5.0", @@ -467,8 +471,8 @@ def raster( Defaults to no minimum frequency bound. max_frequency: Maximum frequency to be included in analysis. Defaults to no maximum frequency bound. - data_type: Data type of the input DEMS file. - Defaults to the ``long_name`` attribute in it. + data_scaling: Data scaling to be used in analysis. + Defaults to the data scaling of the input DEMS file. chan_weight: Weighting method along the channel axis. uniform: Uniform weight (i.e. no channel dependence). std: Inverse square of temporal standard deviation of sky. @@ -494,19 +498,21 @@ def raster( LOGGER.debug(f"{key}: {val!r}") with xr.set_options(keep_attrs=True): - da = load_dems( + da = load.dems( dems, include_mkid_ids=include_mkid_ids, exclude_mkid_ids=exclude_mkid_ids, min_frequency=min_frequency, max_frequency=max_frequency, - data_type=data_type, + data_scaling=data_scaling, skycoord_units=skycoord_units, + skycoord_frame="relative", + chunks=None, ) # subtract temporal baseline - da_on = select.by(da, "state", include="SCAN") - da_off = select.by(da, "state", exclude="SCAN") + da_on = da.sel(time=da.state == "SCAN") + da_off = da.sel(time=da.state != "SCAN") da_base = ( da_off.groupby("scan") .map(mean_in_time) @@ -572,7 +578,7 @@ def raster( ax = axes[0] # type: ignore plot.data(series, ax=ax) - ax.set_title(f"{Path(dems).name}\n({da.observation})") + ax.set_title(f"{Path(dems).name}\n({da.aste_obs_file})") ax = axes[1] # type: ignore map_lim = max(abs(cube.lon).max(), abs(cube.lat).max()) @@ -630,7 +636,7 @@ def skydip( exclude_mkid_ids: Optional[Sequence[int]] = DEFAULT_EXCL_MKID_IDS, min_frequency: Optional[str] = DEFAULT_MIN_FREQUENCY, max_frequency: Optional[str] = DEFAULT_MAX_FREQUENCY, - data_type: Literal["auto", "brightness", "df/f"] = DEFAULT_DATA_TYPE, + data_scaling: Optional[Literal["brightness", "df/f"]] = DEFAULT_DATA_SCALING, # options for analysis chan_weight: Literal["uniform", "std", "std/tx"] = "std/tx", pwv: Literal["0.5", "1.0", "2.0", "3.0", "4.0", "5.0"] = "5.0", @@ -655,8 +661,8 @@ def skydip( Defaults to no minimum frequency bound. max_frequency: Maximum frequency to be included in analysis. Defaults to no maximum frequency bound. - data_type: Data type of the input DEMS file. - Defaults to the ``long_name`` attribute in it. + data_scaling: Data scaling to be used in analysis. + Defaults to the data scaling of the input DEMS file. chan_weight: Weighting method along the channel axis. uniform: Uniform weight (i.e. no channel dependence). std: Inverse square of temporal standard deviation of sky. @@ -680,18 +686,32 @@ def skydip( LOGGER.debug(f"{key}: {val!r}") with xr.set_options(keep_attrs=True): - da = load_dems( + da = load.dems( dems, include_mkid_ids=include_mkid_ids, exclude_mkid_ids=exclude_mkid_ids, min_frequency=min_frequency, max_frequency=max_frequency, - data_type=data_type, + data_scaling=data_scaling, + skycoord_frame="relative", + chunks=None, ) + # add airmass as a coordinate + # fmt: off + airmass = ( + xr.DataArray(1 / np.sin(convert.units(da.lat, "rad"))) + .assign_attrs( + long_name="Airmass", + units="dimensionless", + ) + ) + da = da.assign_coords(airmass=airmass) + # fmt: on + # make continuum series - da_on = select.by(da, "state", include="SCAN") - da_off = select.by(da, "state", exclude="SCAN") + da_on = da.sel(time=da.state == "SCAN") + da_off = da.sel(time=da.state != "SCAN") weight = get_chan_weight(da_off, method=chan_weight, pwv=pwv) series = da_on.weighted(weight.fillna(0)).mean("chan") @@ -711,7 +731,7 @@ def skydip( plot.data(series, x="secz", ax=ax) for ax in axes: # type: ignore - ax.set_title(f"{Path(dems).name}\n({da.observation})") + ax.set_title(f"{Path(dems).name}\n({da.aste_obs_file})") ax.grid(True) fig.tight_layout() @@ -727,7 +747,7 @@ def still( exclude_mkid_ids: Optional[Sequence[int]] = DEFAULT_EXCL_MKID_IDS, min_frequency: Optional[str] = DEFAULT_MIN_FREQUENCY, max_frequency: Optional[str] = DEFAULT_MAX_FREQUENCY, - data_type: Literal["auto", "brightness", "df/f"] = DEFAULT_DATA_TYPE, + data_scaling: Optional[Literal["brightness", "df/f"]] = DEFAULT_DATA_SCALING, # options for analysis chan_weight: Literal["uniform", "std", "std/tx"] = "std/tx", pwv: Literal["0.5", "1.0", "2.0", "3.0", "4.0", "5.0"] = "5.0", @@ -752,8 +772,8 @@ def still( Defaults to no minimum frequency bound. max_frequency: Maximum frequency to be included in analysis. Defaults to no maximum frequency bound. - data_type: Data type of the input DEMS file. - Defaults to the ``long_name`` attribute in it. + data_scaling: Data scaling to be used in analysis. + Defaults to the data scaling of the input DEMS file. chan_weight: Weighting method along the channel axis. uniform: Uniform weight (i.e. no channel dependence). std: Inverse square of temporal standard deviation of sky. @@ -777,17 +797,19 @@ def still( LOGGER.debug(f"{key}: {val!r}") with xr.set_options(keep_attrs=True): - da = load_dems( + da = load.dems( dems, include_mkid_ids=include_mkid_ids, exclude_mkid_ids=exclude_mkid_ids, min_frequency=min_frequency, max_frequency=max_frequency, - data_type=data_type, + data_scaling=data_scaling, + skycoord_frame="relative", + chunks=None, ) # make continuum series - da_off = select.by(da, "state", exclude=["ON", "SCAN"]) + da_off = da.sel(time=~da.state.isin(["ON", "OFF"])) weight = get_chan_weight(da_off, method=chan_weight, pwv=pwv) series = da.weighted(weight.fillna(0)).mean("chan") @@ -807,7 +829,7 @@ def still( plot.data(series, add_colorbar=False, ax=ax) for ax in axes: # type: ignore - ax.set_title(f"{Path(dems).name}\n({da.observation})") + ax.set_title(f"{Path(dems).name}\n({da.aste_obs_file})") ax.grid(True) fig.tight_layout() @@ -823,7 +845,7 @@ def xscan( exclude_mkid_ids: Optional[Sequence[int]] = DEFAULT_EXCL_MKID_IDS, min_frequency: Optional[str] = DEFAULT_MIN_FREQUENCY, max_frequency: Optional[str] = DEFAULT_MAX_FREQUENCY, - data_type: Literal["auto", "brightness", "df/f"] = DEFAULT_DATA_TYPE, + data_scaling: Optional[Literal["brightness", "df/f"]] = DEFAULT_DATA_SCALING, # options for analysis chan_weight: Literal["uniform", "std", "std/tx"] = "std/tx", pwv: Literal["0.5", "1.0", "2.0", "3.0", "4.0", "5.0"] = "5.0", @@ -848,8 +870,8 @@ def xscan( Defaults to no minimum frequency bound. max_frequency: Maximum frequency to be included in analysis. Defaults to no maximum frequency bound. - data_type: Data type of the input DEMS file. - Defaults to the ``long_name`` attribute in it. + data_scaling: Data scaling to be used in analysis. + Defaults to the data scaling of the input DEMS file. chan_weight: Weighting method along the channel axis. uniform: Uniform weight (i.e. no channel dependence). std: Inverse square of temporal standard deviation of sky. @@ -880,7 +902,7 @@ def xscan( exclude_mkid_ids=exclude_mkid_ids, min_frequency=min_frequency, max_frequency=max_frequency, - data_type=data_type, + data_scaling=data_scaling, # options for analysis chan_weight=chan_weight, pwv=pwv, @@ -902,7 +924,7 @@ def yscan( exclude_mkid_ids: Optional[Sequence[int]] = DEFAULT_EXCL_MKID_IDS, min_frequency: Optional[str] = DEFAULT_MIN_FREQUENCY, max_frequency: Optional[str] = DEFAULT_MAX_FREQUENCY, - data_type: Literal["auto", "brightness", "df/f"] = DEFAULT_DATA_TYPE, + data_scaling: Optional[Literal["brightness", "df/f"]] = DEFAULT_DATA_SCALING, # options for analysis chan_weight: Literal["uniform", "std", "std/tx"] = "std/tx", pwv: Literal["0.5", "1.0", "2.0", "3.0", "4.0", "5.0"] = "5.0", @@ -927,8 +949,8 @@ def yscan( Defaults to no minimum frequency bound. max_frequency: Maximum frequency to be included in analysis. Defaults to no maximum frequency bound. - data_type: Data type of the input DEMS file. - Defaults to the ``long_name`` attribute in it. + data_scaling: Data scaling to be used in analysis. + Defaults to the data scaling of the input DEMS file. chan_weight: Weighting method along the channel axis. uniform: Uniform weight (i.e. no channel dependence). std: Inverse square of temporal standard deviation of sky. @@ -959,7 +981,7 @@ def yscan( exclude_mkid_ids=exclude_mkid_ids, min_frequency=min_frequency, max_frequency=max_frequency, - data_type=data_type, + data_scaling=data_scaling, # options for analysis chan_weight=chan_weight, pwv=pwv, @@ -981,7 +1003,7 @@ def zscan( exclude_mkid_ids: Optional[Sequence[int]] = DEFAULT_EXCL_MKID_IDS, min_frequency: Optional[str] = DEFAULT_MIN_FREQUENCY, max_frequency: Optional[str] = DEFAULT_MAX_FREQUENCY, - data_type: Literal["auto", "brightness", "df/f"] = DEFAULT_DATA_TYPE, + data_scaling: Optional[Literal["brightness", "df/f"]] = DEFAULT_DATA_SCALING, # options for analysis chan_weight: Literal["uniform", "std", "std/tx"] = "std/tx", pwv: Literal["0.5", "1.0", "2.0", "3.0", "4.0", "5.0"] = "5.0", @@ -1006,8 +1028,8 @@ def zscan( Defaults to no minimum frequency bound. max_frequency: Maximum frequency to be included in analysis. Defaults to no maximum frequency bound. - data_type: Data type of the input DEMS file. - Defaults to the ``long_name`` attribute in it. + data_scaling: Data scaling to be used in analysis. + Defaults to the data scaling of the input DEMS file. chan_weight: Weighting method along the channel axis. uniform: Uniform weight (i.e. no channel dependence). std: Inverse square of temporal standard deviation of sky. @@ -1038,7 +1060,7 @@ def zscan( exclude_mkid_ids=exclude_mkid_ids, min_frequency=min_frequency, max_frequency=max_frequency, - data_type=data_type, + data_scaling=data_scaling, # options for analysis chan_weight=chan_weight, pwv=pwv, @@ -1062,7 +1084,7 @@ def _scan( exclude_mkid_ids: Optional[Sequence[int]] = DEFAULT_EXCL_MKID_IDS, min_frequency: Optional[str] = DEFAULT_MIN_FREQUENCY, max_frequency: Optional[str] = DEFAULT_MAX_FREQUENCY, - data_type: Literal["auto", "brightness", "df/f"] = DEFAULT_DATA_TYPE, + data_scaling: Optional[Literal["brightness", "df/f"]] = DEFAULT_DATA_SCALING, # options for analysis chan_weight: Literal["uniform", "std", "std/tx"] = "std/tx", pwv: Literal["0.5", "1.0", "2.0", "3.0", "4.0", "5.0"] = "5.0", @@ -1088,8 +1110,8 @@ def _scan( Defaults to no minimum frequency bound. max_frequency: Maximum frequency to be included in analysis. Defaults to no maximum frequency bound. - data_type: Data type of the input DEMS file. - Defaults to the ``long_name`` attribute in it. + data_scaling: Data scaling to be used in analysis. + Defaults to the data scaling of the input DEMS file. chan_weight: Weighting method along the channel axis. uniform: Uniform weight (i.e. no channel dependence). std: Inverse square of temporal standard deviation of sky. @@ -1113,18 +1135,20 @@ def _scan( LOGGER.debug(f"{key}: {val!r}") with xr.set_options(keep_attrs=True): - da = load_dems( + da = load.dems( dems, include_mkid_ids=include_mkid_ids, exclude_mkid_ids=exclude_mkid_ids, min_frequency=min_frequency, max_frequency=max_frequency, - data_type=data_type, + data_scaling=data_scaling, + skycoord_frame="relative", + chunks=None, ) # make continuum series - da_on = select.by(da, "state", include="ON") - da_off = select.by(da, "state", exclude="ON") + da_on = da.sel(time=da.state == "ON") + da_off = da.sel(time=da.state != "ON") weight = get_chan_weight(da_off, method=chan_weight, pwv=pwv) series = da_on.weighted(weight.fillna(0)).mean("chan") @@ -1144,7 +1168,7 @@ def _scan( plot.data(series, x=f"aste_subref_{axis}", ax=ax) for ax in axes: # type: ignore - ax.set_title(f"{Path(dems).name}\n({da.observation})") + ax.set_title(f"{Path(dems).name}\n({da.aste_obs_file})") ax.grid(True) fig.tight_layout() @@ -1209,11 +1233,11 @@ def subtract_per_abba_phase(dems: xr.DataArray, /) -> xr.DataArray: raise ValueError("State must be unique.") if states[0] == "ON": - src = select.by(dems, "beam", include="A").mean("time") - sky = select.by(dems, "beam", include="B").mean("time") + src = dems.sel(time=dems.beam == "A").mean("time") + sky = dems.sel(time=dems.beam == "B").mean("time") elif states[0] == "OFF": - src = select.by(dems, "beam", include="B").mean("time") - sky = select.by(dems, "beam", include="A").mean("time") + src = dems.sel(time=dems.beam == "B").mean("time") + sky = dems.sel(time=dems.beam == "A").mean("time") else: raise ValueError("State must be either ON or OFF.") @@ -1272,92 +1296,6 @@ def get_robust_lim(da: xr.DataArray) -> tuple[float, float]: ) -def load_dems( - dems: Path, - /, - *, - include_mkid_ids: Optional[Sequence[int]] = DEFAULT_INCL_MKID_IDS, - exclude_mkid_ids: Optional[Sequence[int]] = DEFAULT_EXCL_MKID_IDS, - min_frequency: Optional[str] = DEFAULT_MIN_FREQUENCY, - max_frequency: Optional[str] = DEFAULT_MAX_FREQUENCY, - data_type: Literal["auto", "brightness", "df/f"] = DEFAULT_DATA_TYPE, - frequency_units: str = DEFAULT_FREQUENCY_UNITS, - skycoord_units: str = DEFAULT_SKYCOORD_UNITS, -) -> xr.DataArray: - """Load a DEMS with given conversions and selections. - - Args: - dems: Input DEMS file (netCDF or Zarr). - include_mkid_ids: MKID IDs to be included in analysis. - Defaults to all MKID IDs. - exclude_mkid_ids: MKID IDs to be excluded in analysis. - Defaults to no MKID IDs. - min_frequency: Minimum frequency to be included in analysis. - Defaults to no minimum frequency bound. - max_frequency: Maximum frequency to be included in analysis. - Defaults to no maximum frequency bound. - data_type: Data type of the input DEMS file. - Defaults to the ``long_name`` attribute in it. - frequency_units: Units of the frequency. - skycoord_units: Units of the sky coordinate axes. - - Returns: - DEMS as a DataArray with given conversion and selections. - - """ - da = load.dems(dems, chunks=None) - - if min_frequency is not None: - min_frequency = Quantity(min_frequency).to(frequency_units).value - - if max_frequency is not None: - max_frequency = Quantity(max_frequency).to(frequency_units).value - - if da.frame == "altaz": - z = np.pi / 2 - convert.units(da.lat, "rad") - secz = cast(xr.DataArray, 1 / np.cos(z)) - da = da.assign_coords( - secz=secz.assign_attrs(long_name="sec(Z)", units="dimensionless") - ) - - da = convert.coord_units( - da, - ["d2_mkid_frequency", "frequency"], - frequency_units, - ) - da = convert.coord_units( - da, - ["lat", "lat_origin", "lon", "lon_origin"], - skycoord_units, - ) - da = assign.scan(da, by="state") - da = convert.frame(da, "relative") - da = select.by(da, "d2_mkid_type", "filter") - da = select.by( - da, - "chan", - include=include_mkid_ids, - exclude=exclude_mkid_ids, - ) - da = select.by( - da, - "frequency", - min=min_frequency, - max=max_frequency, - ) - - if data_type == "auto" and "units" in da.attrs: - return da - - if data_type == "brightness": - return da.assign_attrs(long_name="Brightness", units="K") - - if data_type == "df/f": - return da.assign_attrs(long_name="df/f", units="dimensionless") - - raise ValueError("Data type could not be inferred.") - - def save_qlook( qlook: Union[Figure, xr.DataArray, str], file: Path, @@ -1391,7 +1329,7 @@ def save_qlook( return path elif isinstance(qlook, xr.DataArray): if path.name.endswith(".csv"): - name = qlook.attrs["data_type"] + name = qlook.attrs["data_scaling"] ds = qlook.to_dataset(name=name) ds.to_pandas().to_csv(path, **options) return path diff --git a/decode/select.py b/decode/select.py index 7ef2057..fe33fef 100644 --- a/decode/select.py +++ b/decode/select.py @@ -4,6 +4,7 @@ # standard library from collections.abc import Sequence from datetime import datetime, timedelta +from logging import getLogger from typing import Optional, TypeVar, Union @@ -11,6 +12,10 @@ import xarray as xr +# constants +LOGGER = getLogger(__name__) + + # type hints T = TypeVar("T", bool, int, float, str, datetime, timedelta) Multiple = Union[Sequence[T], T] @@ -47,7 +52,21 @@ def by( Returns: Selected DEMS. + Important: + This function will be deprecated in a future release. + For ``include`` and ``exclude`` options, use ``xarray.DataArray.isin`` + like ``dems.sel(time=dems.state.isin(['ON', 'OFF']))``. + For ``min`` and ``max`` options, use ``ndtools.Range`` like + ``dems.sel(chan=dems.frequency == Range(250e9, 300e9))``. + """ + LOGGER.warning( + "This function will be deprecated in a future release. " + "For include and exclude options, use DataArray.isin like " + "dems.sel(time=dems.state.isin(['ON', 'OFF'])). " + "For min and max options, use ndtools.Range like " + "dems.sel(chan=dems.frequency == Range(250e9, 300e9))." + ) coord = dems[coord_name] if not isinstance(coord, xr.DataArray): diff --git a/pyproject.toml b/pyproject.toml index a249aa4..56b3409 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,9 +13,11 @@ keywords = [ requires-python = ">=3.9,<3.13" dependencies = [ "astropy>=6,<8", + "dask>=2024,<2026", "dems>=2025.6,<2026.0", "fire>=0.5,<1.0", "matplotlib>=3,<4", + "ndtools>=1,<2", "numpy>=1.23,<3.0", "pandas>=2,<3", "pandas-stubs>=2,<3", diff --git a/tests/test_assign.py b/tests/test_assign.py index c033ec8..355f633 100644 --- a/tests/test_assign.py +++ b/tests/test_assign.py @@ -8,7 +8,10 @@ # constants DEMS_DIR = Path(__file__).parents[1] / "data" / "dems" -DEMS = load.dems(DEMS_DIR / "dems_20171111110002.nc.gz").compute() +DEMS = load.dems( + DEMS_DIR / "dems_20171111110002.nc.gz", + include_mkid_types=None, +) def test_assign_scan() -> None: diff --git a/tests/test_convert.py b/tests/test_convert.py index db335a2..4b2c378 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -8,7 +8,10 @@ # constants DEMS_DIR = Path(__file__).parents[1] / "data" / "dems" -DEMS = load.dems(DEMS_DIR / "dems_20171111110002.nc.gz").compute() +DEMS = load.dems( + DEMS_DIR / "dems_20171111110002.nc.gz", + include_mkid_types=None, +) def test_convert_frame() -> None: diff --git a/tests/test_load.py b/tests/test_load.py index 3b6f8a5..3c83320 100644 --- a/tests/test_load.py +++ b/tests/test_load.py @@ -20,4 +20,4 @@ def test_load_atm() -> None: @mark.parametrize("dems", DEMS_ALL) def test_load_dems(dems: Path) -> None: - load.dems(dems) + load.dems(dems, include_mkid_types=None) diff --git a/tests/test_plot.py b/tests/test_plot.py index b5e2c47..c376500 100644 --- a/tests/test_plot.py +++ b/tests/test_plot.py @@ -9,7 +9,10 @@ # constants DEMS_DIR = Path(__file__).parents[1] / "data" / "dems" -DEMS = load.dems(DEMS_DIR / "dems_20171111110002.nc.gz").compute() +DEMS = load.dems( + DEMS_DIR / "dems_20171111110002.nc.gz", + include_mkid_types=None, +) def test_plot_data_1d_time() -> None: diff --git a/tests/test_select.py b/tests/test_select.py index c086dfd..f6d6763 100644 --- a/tests/test_select.py +++ b/tests/test_select.py @@ -9,7 +9,10 @@ # constants DEMS_DIR = Path(__file__).parents[1] / "data" / "dems" -DEMS = load.dems(DEMS_DIR / "dems_20171111110002.nc.gz").compute() +DEMS = load.dems( + DEMS_DIR / "dems_20171111110002.nc.gz", + include_mkid_types=None, +) def test_select_by_min() -> None: diff --git a/uv.lock b/uv.lock index bba91df..4a1cc8e 100644 --- a/uv.lock +++ b/uv.lock @@ -347,6 +347,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, ] +[[package]] +name = "cloudpickle" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/39/069100b84d7418bc358d81669d5748efb14b9cceacd2f9c75f550424132f/cloudpickle-3.1.1.tar.gz", hash = "sha256:b216fa8ae4019d5482a8ac3c95d8f6346115d8835911fd4aefd1a445e4242c64", size = 22113, upload-time = "2025-01-14T17:02:05.085Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/e8/64c37fadfc2816a7701fa8a6ed8d87327c7d54eacfbfb6edab14a2f2be75/cloudpickle-3.1.1-py3-none-any.whl", hash = "sha256:c8c5a44295039331ee9dad40ba100a9c7297b6f988e50e87ccdf3765a668350e", size = 20992, upload-time = "2025-01-14T17:02:02.417Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -478,6 +487,52 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, ] +[[package]] +name = "dask" +version = "2024.8.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "cloudpickle", marker = "python_full_version < '3.10'" }, + { name = "fsspec", marker = "python_full_version < '3.10'" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "partd", marker = "python_full_version < '3.10'" }, + { name = "pyyaml", marker = "python_full_version < '3.10'" }, + { name = "toolz", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/2e/568a422d907745a3a897a732c83d05a3923d8cfa2511a6abea2a0e19994e/dask-2024.8.0.tar.gz", hash = "sha256:f1fec39373d2f101bc045529ad4e9b30e34e6eb33b7aa0fa7073aec7b1bf9eee", size = 9895684, upload-time = "2024-08-06T20:23:54.464Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/47/136a5dd68a33089f96f8aa1178ccd545d325ec9ab2bb42a3038711a935c0/dask-2024.8.0-py3-none-any.whl", hash = "sha256:250ea3df30d4a25958290eec4f252850091c6cfaed82d098179c3b25bba18309", size = 1233681, upload-time = "2024-08-06T20:23:42.258Z" }, +] + +[[package]] +name = "dask" +version = "2025.5.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version == '3.11.*'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "cloudpickle", marker = "python_full_version >= '3.10'" }, + { name = "fsspec", marker = "python_full_version >= '3.10'" }, + { name = "importlib-metadata", marker = "python_full_version >= '3.10' and python_full_version < '3.12'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "partd", marker = "python_full_version >= '3.10'" }, + { name = "pyyaml", marker = "python_full_version >= '3.10'" }, + { name = "toolz", marker = "python_full_version >= '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/29/05feb8e2531c46d763547c66b7f5deb39b53d99b3be1b4ddddbd1cec6567/dask-2025.5.1.tar.gz", hash = "sha256:979d9536549de0e463f4cab8a8c66c3a2ef55791cd740d07d9bf58fab1d1076a", size = 10969324, upload-time = "2025-05-20T19:54:30.688Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/30/53b0844a7a4c6b041b111b24ca15cc9b8661a86fe1f6aaeb2d0d7f0fb1f2/dask-2025.5.1-py3-none-any.whl", hash = "sha256:3b85fdaa5f6f989dde49da6008415b1ae996985ebdfb1e40de2c997d9010371d", size = 1474226, upload-time = "2025-05-20T19:54:20.309Z" }, +] + [[package]] name = "decode" version = "2025.4.2" @@ -486,10 +541,13 @@ dependencies = [ { name = "astropy", version = "6.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "astropy", version = "6.1.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "astropy", version = "7.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "dask", version = "2024.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "dask", version = "2025.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "dems" }, { name = "fire" }, { name = "matplotlib", version = "3.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "matplotlib", version = "3.10.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "ndtools" }, { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "numpy", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, @@ -527,9 +585,11 @@ dev = [ [package.metadata] requires-dist = [ { name = "astropy", specifier = ">=6,<8" }, + { name = "dask", specifier = ">=2024,<2026" }, { name = "dems", specifier = ">=2025.6,<2026.0" }, { name = "fire", specifier = ">=0.5,<1.0" }, { name = "matplotlib", specifier = ">=3,<4" }, + { name = "ndtools", specifier = ">=1,<2" }, { name = "numpy", specifier = ">=1.23,<3.0" }, { name = "pandas", specifier = ">=2,<3" }, { name = "pandas-stubs", specifier = ">=2,<3" }, @@ -673,6 +733,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0b/2f/c536b5b9bb3c071e91d536a4d11f969e911dbb6b227939f4c5b0bca090df/fonttools-4.58.4-py3-none-any.whl", hash = "sha256:a10ce13a13f26cbb9f37512a4346bb437ad7e002ff6fa966a7ce7ff5ac3528bd", size = 1114660, upload-time = "2025-06-13T17:25:13.321Z" }, ] +[[package]] +name = "fsspec" +version = "2025.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/f7/27f15d41f0ed38e8fcc488584b57e902b331da7f7c6dcda53721b15838fc/fsspec-2025.5.1.tar.gz", hash = "sha256:2e55e47a540b91843b755e83ded97c6e897fa0942b11490113f09e9c443c2475", size = 303033, upload-time = "2025-05-24T12:03:23.792Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/61/78c7b3851add1481b048b5fdc29067397a1784e2910592bc81bb3f608635/fsspec-2025.5.1-py3-none-any.whl", hash = "sha256:24d3a2e663d5fc735ab256263c4075f374a174c3410c0b25e5bd1970bceaa462", size = 199052, upload-time = "2025-05-24T12:03:21.66Z" }, +] + [[package]] name = "idna" version = "3.10" @@ -696,7 +765,7 @@ name = "importlib-metadata" version = "8.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "zipp", marker = "python_full_version < '3.10'" }, + { name = "zipp", marker = "python_full_version < '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } wheels = [ @@ -996,6 +1065,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/1d/50ad811d1c5dae091e4cf046beba925bcae0a610e79ae4c538f996f63ed5/kiwisolver-1.4.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:65ea09a5a3faadd59c2ce96dc7bf0f364986a315949dc6374f04396b0d60e09b", size = 71762, upload-time = "2024-12-24T18:30:48.903Z" }, ] +[[package]] +name = "locket" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/83/97b29fe05cb6ae28d2dbd30b81e2e402a3eed5f460c26e9eaa5895ceacf5/locket-1.0.0.tar.gz", hash = "sha256:5c0d4c052a8bbbf750e056a8e65ccd309086f4f0f18a2eac306a8dfa4112a632", size = 4350, upload-time = "2022-04-20T22:04:44.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl", hash = "sha256:b6c819a722f7b6bd955b80781788e4a66a55628b858d347536b7e81325a3a5e3", size = 4398, upload-time = "2022-04-20T22:04:42.23Z" }, +] + [[package]] name = "markdown-it-py" version = "3.0.0" @@ -1238,6 +1316,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5f/df/76d0321c3797b54b60fef9ec3bd6f4cfd124b9e422182156a1dd418722cf/myst_parser-4.0.1-py3-none-any.whl", hash = "sha256:9134e88959ec3b5780aedf8a99680ea242869d012e8821db3126d427edc9c95d", size = 84579, upload-time = "2025-02-12T10:53:02.078Z" }, ] +[[package]] +name = "ndtools" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "numpy", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pandas" }, + { name = "pandas-stubs", version = "2.2.2.240807", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pandas-stubs", version = "2.2.3.250527", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/c4/db95382830ae5b0e538ba0d37f4de8713a9736624c0d4dac7ceca624974c/ndtools-1.0.1.tar.gz", hash = "sha256:44d6b2da9e3f508772145e65035b91e3a510fc44054835caee0a2c569087c322", size = 63830, upload-time = "2025-04-29T06:47:52.795Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/74/7c15ce8fa6bc9eea35c19299a150502f8af55efa90816f5f4914ced7972e/ndtools-1.0.1-py3-none-any.whl", hash = "sha256:464aaed3ffd79770d200a89b041749454155da99afef3d3f359f6eab2431c965", size = 11114, upload-time = "2025-04-29T06:47:51.479Z" }, +] + [[package]] name = "nodeenv" version = "1.9.1" @@ -1555,6 +1651,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650, upload-time = "2024-04-05T09:43:53.299Z" }, ] +[[package]] +name = "partd" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "locket" }, + { name = "toolz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/3a/3f06f34820a31257ddcabdfafc2672c5816be79c7e353b02c1f318daa7d4/partd-1.4.2.tar.gz", hash = "sha256:d022c33afbdc8405c226621b015e8067888173d85f7f5ecebb3cafed9a20f02c", size = 21029, upload-time = "2024-05-06T19:51:41.945Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl", hash = "sha256:978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f", size = 18905, upload-time = "2024-05-06T19:51:39.271Z" }, +] + [[package]] name = "pathspec" version = "0.12.1" @@ -2271,6 +2380,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, ] +[[package]] +name = "toolz" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/0b/d80dfa675bf592f636d1ea0b835eab4ec8df6e9415d8cfd766df54456123/toolz-1.0.0.tar.gz", hash = "sha256:2c86e3d9a04798ac556793bced838816296a2f085017664e4995cb40a1047a02", size = 66790, upload-time = "2024-10-04T16:17:04.001Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/98/eb27cc78ad3af8e302c9d8ff4977f5026676e130d28dd7578132a457170c/toolz-1.0.0-py3-none-any.whl", hash = "sha256:292c8f1c4e7516bf9086f8850935c799a874039c8bcf959d47b600e4c44a6236", size = 56383, upload-time = "2024-10-04T16:17:01.533Z" }, +] + [[package]] name = "traitlets" version = "5.14.3"