From ce3a39e4df2962dcf14e3e9cc32e9235f7312bb0 Mon Sep 17 00:00:00 2001 From: Adrian Benson Date: Thu, 10 Sep 2026 14:04:43 +1200 Subject: [PATCH 01/36] refactor: update include_unknown_fields default to True and clean up coderefactor: update include_unknown_fields default to True and clean up code --- src/gsolve/core/data.py | 29 ++++++++++++++++------------- src/gsolve/observations.py | 2 +- src/gsolve/reductions/anomalies.py | 6 ++---- src/gsolve/sites.py | 2 +- 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/gsolve/core/data.py b/src/gsolve/core/data.py index 6a8ef39..37fa57e 100644 --- a/src/gsolve/core/data.py +++ b/src/gsolve/core/data.py @@ -18,14 +18,13 @@ """Base class and function definitions for Gsolve data structures.""" -import abc -import copy +from dask.array import ma + import dataclasses import warnings from collections.abc import Callable from copy import deepcopy -from types import MappingProxyType -from typing import Any, ClassVar, Protocol, Self +from typing import Any, Self, ClassVar import numpy as np import pandas as pd @@ -401,8 +400,12 @@ class GSolveTable(_HasKnownFields, abc.ABC): The primary data storage object. """ - data: pd.DataFrame - params: GSolveParameters | None + _known_fields: ClassVar[dict[str, DataFieldSpecification]] + _default_excel_sheet_name: ClassVar[str | tuple[str, ...]] = "" + + def __init__(self) -> None: + self.data: _pd.DataFrame + pass def __repr__(self) -> str: rval = [] @@ -658,10 +661,9 @@ def from_excel( are [year, month, day, hour, minute, second, microsecond, nanosecond], with at least year, month, and day being required. mapper : dict-like or function, default None - Dict-like or function transformations to apply to column names before. - Allows non-standard column/field names to be corrected prior to - object creation. The simplest use case is to provide a dict of - input_name, output_name pairs e.g. ``{'lat': 'latitude', ...}`` + Dict-like or function transformations to apply to column names before + creating object. The simplest approach is to provide a dict of the form + ``{'input_name': 'output_name', ...}`` kwargs Additional keyword arguments to be passed to ``pandas.read_excel``. @@ -672,7 +674,7 @@ def from_excel( See Also -------- pandas.read_excel : For available ``kwargs`` . - pandas.DataFrame.rename : For full details of ``mapper`` argument. + pandas.DataFrame.rename : For full details of `mapper`` argument. """ if sheet_name is None: try: @@ -684,9 +686,9 @@ def from_excel( ) raise ValueError(msg) from None else: - sheet_name_ = sheet_name + _sheet_name = sheet_name - df = read_excel_worksheet(excel_file, sheet_name=sheet_name_, **kwargs) + df = read_excel_worksheet(excel_file, sheet_name=_sheet_name, **kwargs) return cls.from_dataframe( df, use_index=False, @@ -702,6 +704,7 @@ def write_to_csv( expand_datetime: str | None = None, drop_datetime: bool = False, bool_to_int: bool = False, + include_unknown_fields: bool = True, **kwargs, ) -> None: """Write data to a csv file. diff --git a/src/gsolve/observations.py b/src/gsolve/observations.py index 67e700b..b124014 100644 --- a/src/gsolve/observations.py +++ b/src/gsolve/observations.py @@ -1066,7 +1066,7 @@ def _get_writable_df( bool_to_int=bool_to_int, ) - def write_to_csv( + def write_to_csv( # ruff: ignore[undocumented-public-method] self, fname: FilePath, *, diff --git a/src/gsolve/reductions/anomalies.py b/src/gsolve/reductions/anomalies.py index 404cc35..e7ef52c 100644 --- a/src/gsolve/reductions/anomalies.py +++ b/src/gsolve/reductions/anomalies.py @@ -19,10 +19,8 @@ """Functions and classes to compute standard gravity anomalies.""" -from types import MappingProxyType - -import numpy as np -import pandas as pd +import numpy as _np +import pandas as _pd from numpy.typing import ArrayLike from gsolve.core.data import DataFieldSpecification, GSolveTable diff --git a/src/gsolve/sites.py b/src/gsolve/sites.py index b7ec692..5201eab 100644 --- a/src/gsolve/sites.py +++ b/src/gsolve/sites.py @@ -428,7 +428,7 @@ def _get_writable_df( normalize_column_names: bool = True, bool_to_int: bool = True, include_unknown_fields: bool = True, - ) -> pd.DataFrame: + ) -> _pd.DataFrame: """ Return GravitySite data as a DataFrame suitable for writing to an excel or csv file. From 15233e302a93a7ee307ead66823f80e63f6d93f6 Mon Sep 17 00:00:00 2001 From: Adrian Benson Date: Thu, 10 Sep 2026 22:54:53 +1200 Subject: [PATCH 02/36] docs: enhance mapper argument documentation for clarity and consistency --- src/gsolve/core/data.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/gsolve/core/data.py b/src/gsolve/core/data.py index 37fa57e..d410c2c 100644 --- a/src/gsolve/core/data.py +++ b/src/gsolve/core/data.py @@ -637,7 +637,7 @@ def from_csv( def from_excel( cls, excel_file: FilePath, - sheet_name: str | int | list[str | int] | tuple[str] | None = None, + sheet_name: str | int | list[str | int] | None = None, ignore_unknown_fields: bool = False, parse_split_datetime: bool = True, mapper: Renamer | None = None, @@ -661,9 +661,10 @@ def from_excel( are [year, month, day, hour, minute, second, microsecond, nanosecond], with at least year, month, and day being required. mapper : dict-like or function, default None - Dict-like or function transformations to apply to column names before - creating object. The simplest approach is to provide a dict of the form - ``{'input_name': 'output_name', ...}`` + Dict-like or function transformations to apply to column names before. + Allows non-standard column/field names to be corrected prior to + object creation. The simplest use case is to provide a dict of + input_name, output_name pairs e.g. ``{'lat': 'latitude', ...}`` kwargs Additional keyword arguments to be passed to ``pandas.read_excel``. @@ -674,7 +675,7 @@ def from_excel( See Also -------- pandas.read_excel : For available ``kwargs`` . - pandas.DataFrame.rename : For full details of `mapper`` argument. + pandas.DataFrame.rename : For full details of ``mapper`` argument. """ if sheet_name is None: try: From 8b0f1cc6899ab14a2165e3cf8ded2a7f85c283ba Mon Sep 17 00:00:00 2001 From: Adrian Benson Date: Sat, 12 Sep 2026 00:26:37 +1200 Subject: [PATCH 03/36] refactor: remove calculate_calibration_factor argument from solve_lstsq calls --- .../TeMaari_ocean_loading_pyhardisp.py | 2 +- examples/scripts/okataina_survey.py | 2 +- examples/scripts/read_cg6.py | 4 +- pyproject.toml | 74 +++++++++---------- 4 files changed, 41 insertions(+), 41 deletions(-) diff --git a/examples/scripts/TeMaari_ocean_loading_pyhardisp.py b/examples/scripts/TeMaari_ocean_loading_pyhardisp.py index 7434ade..5e47124 100644 --- a/examples/scripts/TeMaari_ocean_loading_pyhardisp.py +++ b/examples/scripts/TeMaari_ocean_loading_pyhardisp.py @@ -98,7 +98,7 @@ """ Run the network adjustment. Here we use solve method "2", see documentation. We process each loop individually -and apply a 95 percentile cutoff filter to the residuals.""" +and apply a 99 percentile cutoff filter to the residuals.""" results = survey.solve_lstsq(method=2, use_loops=True, percentile_clipping=95) # results.site_solution contains the adjusted gravity per station diff --git a/examples/scripts/okataina_survey.py b/examples/scripts/okataina_survey.py index bb18e48..33834cb 100644 --- a/examples/scripts/okataina_survey.py +++ b/examples/scripts/okataina_survey.py @@ -152,7 +152,7 @@ """ Run the network adjustment. Here we use solve method "2", see documentation. We process each loop individually -and apply a 95 percentile cutoff filter to the residuals.""" +and apply a 99 percentile cutoff filter to the residuals.""" results = survey.solve_lstsq(method=2, use_loops=True, percentile_clipping=95) # results.site_solution contains the adjusted gravity per station diff --git a/examples/scripts/read_cg6.py b/examples/scripts/read_cg6.py index 5022acd..2c30c4f 100644 --- a/examples/scripts/read_cg6.py +++ b/examples/scripts/read_cg6.py @@ -71,7 +71,9 @@ survey = GravitySurvey(obs, sites) # solve for gravity -results = survey.solve_lstsq(method=1, use_loops=True, percentile_clipping=99) +results = survey.solve_lstsq( + method=1, use_loops=True, percentile_clipping=99 +) # results.site_solution contains the adjusted gravity per station print(results.site_solution) diff --git a/pyproject.toml b/pyproject.toml index 2e2682f..baf4203 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -119,48 +119,46 @@ line-length = 88 # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. # Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or # McCabe complexity (`C901`) by default. -# select = ["ANN", "D", "DOC", "PD", "NPY"] +select = ["ANN", "D", "DOC", "PD", "NPY"] preview = true extend-select = [ - "ARG", # flake8-unused-arguments - "B", # flake8-bugbear - "C4", # flake8-comprehensions - "D", # pydocstyle - "EM", # flake8-errmsg - "EXE", # flake8-executable - "FURB", # refurb - "G", # flake8-logging-format - "I", # isort - "ICN", # flake8-import-conventions - "NPY", # NumPy specific rules - "PD", # pandas-vet - "PGH", # pygrep-hooks - "PIE", # flake8-pie - "PL", # pylint - "PT", # flake8-pytest-style - "PTH", # flake8-use-pathlib - "PYI", # flake8-pyi - "RET", # flake8-return - "RUF", # Ruff-specific - "SIM", # flake8-simplify - "T20", # flake8-print - "UP", # pyupgrade - "YTT", # flake8-2020 - "too-many-positional-arguments", # need to make keyword args using * + "ARG", # flake8-unused-arguments + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "D", # pydocstyle + "EM", # flake8-errmsg + "EXE", # flake8-executable + "FURB", # refurb + "G", # flake8-logging-format + "I", # isort + "ICN", # flake8-import-conventions + "NPY", # NumPy specific rules + "PD", # pandas-vet + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PL", # pylint + "PT", # flake8-pytest-style + "PTH", # flake8-use-pathlib + "PYI", # flake8-pyi + "RET", # flake8-return + "RUF", # Ruff-specific + "SIM", # flake8-simplify + "T20", # flake8-print + "UP", # pyupgrade + "YTT", # flake8-2020 ] ignore = [ - "missing-type-kwargs", # AN003 allow unannotated **kwargs - "undocumented-magic-method", # allow undocumented magic methods - "docstring-missing-exception", # don't NEED to document exceptions - "undocumented-public-module", # allow undocumented modules - "PLR09", # Too many branches, args etc - "magic-value-comparison", # Magic value used in comparison - "unnecessary-assign", # Allow variable assignment only for return - "pytest-fixture-incorrect-parentheses-style", # Conventions for parenthesis on pytest.fixture - "unnecessary-multiline-docstring", # Allow single line doc strings in their own line - "no-explicit-stacklevel", # Allow warnings.warn without stacklevel - "non-pep695-type-alias", # Allow old style type alias - will fix later - "non-augmented-assignment", # Don't suggest/enforce augmented assignment (+=, /= etc) - breaks pandas + "ANN003", + "D105", + "DOC502", + "DOC501", + "D100", + "ISC001", # Conflicts with formatter + "PLR09", # Too many <...> + "PLR2004", # Magic value used in comparison + "RET504", # Allow variable assignment only for return + "PT001", # Conventions for parenthesis on pytest.fixture + "D200", # Allow single line docstrings in their own line ] [tool.ruff.lint.per-file-ignores] From 9c3a7304f69af6a00239a5cbadd3977b8e5c41d9 Mon Sep 17 00:00:00 2001 From: Adrian Benson Date: Sun, 13 Sep 2026 00:23:05 +1200 Subject: [PATCH 04/36] rebase on main --- examples/scripts/read_cg6.py | 4 +- prek.toml | 21 +- src/gsolve/core/_typing.py | 16 +- src/gsolve/core/data.py | 293 ++++++++++++++++++- src/gsolve/core/excel_io.py | 31 +- src/gsolve/core/utils.py | 65 ++-- src/gsolve/gsolve_algorithms.py | 6 +- src/gsolve/gsolve_outputs.py | 12 +- src/gsolve/meter_conversion.py | 7 +- src/gsolve/observations.py | 34 +-- src/gsolve/reductions/corrections.py | 2 +- src/gsolve/reductions/terrain_corrections.py | 21 +- src/gsolve/reports.py | 19 +- src/gsolve/scintrex.py | 26 +- src/gsolve/sites.py | 2 +- src/gsolve/tide/earth_tide.py | 24 +- src/gsolve/tide/ocean_load.py | 28 +- 17 files changed, 416 insertions(+), 195 deletions(-) diff --git a/examples/scripts/read_cg6.py b/examples/scripts/read_cg6.py index 2c30c4f..5022acd 100644 --- a/examples/scripts/read_cg6.py +++ b/examples/scripts/read_cg6.py @@ -71,9 +71,7 @@ survey = GravitySurvey(obs, sites) # solve for gravity -results = survey.solve_lstsq( - method=1, use_loops=True, percentile_clipping=99 -) +results = survey.solve_lstsq(method=1, use_loops=True, percentile_clipping=99) # results.site_solution contains the adjusted gravity per station print(results.site_solution) diff --git a/prek.toml b/prek.toml index ba72978..6ab1784 100644 --- a/prek.toml +++ b/prek.toml @@ -1,11 +1,10 @@ [[repos]] repo = "builtin" hooks = [ - { id = "trailing-whitespace", files = "\\.py$" }, - { id = "end-of-file-fixer", files = "\\.py$" }, + { id = "trailing-whitespace" }, + { id = "end-of-file-fixer" }, { id = "check-added-large-files" }, - { id = "check-case-conflict" }, - { id = "detect-private-key" }, + ] [[repos]] @@ -13,20 +12,10 @@ repo = "https://github.com/astral-sh/ruff-pre-commit" rev = "v0.16.7" # Ruff version. hooks = [ # Run the linter. - { id = "ruff-check", args = [ - "--fix", - ], types_or = [ - "python", - "pyi", - "pyproject", - ] }, + { id = "ruff-check", args = ["--fix"], types_or = ["python", "pyi"] }, # Run the formatter. - { id = "ruff-format", types_or = [ - "python", - "pyi", - "markdown", - ] }, + { id = "ruff-format", types_or = ["python", "pyi", "markdown"] }, ] diff --git a/src/gsolve/core/_typing.py b/src/gsolve/core/_typing.py index 52fdda3..11a6c04 100644 --- a/src/gsolve/core/_typing.py +++ b/src/gsolve/core/_typing.py @@ -74,8 +74,20 @@ type Renamer = Mapping[Any, Hashable] | Callable[[Any], Hashable] -type DateTimeConvertibleTypes = ( - str | int | float | datetime.timedelta | list | tuple | ArrayLike | Index | Series +DateTimeConvertibleTypes: TypeAlias = ( + str + | int + | float + | datetime.timedelta + | list + | tuple + | range + | ArrayLike + | Index + | Series +) +DatetimeScalar: TypeAlias = ( + int | float | str | datetime.date | np.datetime64 | pd.Timestamp ) type DatetimeScalar = int | float | str | datetime.date | np.datetime64 | pd.Timestamp diff --git a/src/gsolve/core/data.py b/src/gsolve/core/data.py index d410c2c..9785d3c 100644 --- a/src/gsolve/core/data.py +++ b/src/gsolve/core/data.py @@ -18,13 +18,11 @@ """Base class and function definitions for Gsolve data structures.""" -from dask.array import ma - import dataclasses import warnings from collections.abc import Callable from copy import deepcopy -from typing import Any, Self, ClassVar +from typing import Any, ClassVar, Self import numpy as np import pandas as pd @@ -405,7 +403,6 @@ class GSolveTable(_HasKnownFields, abc.ABC): def __init__(self) -> None: self.data: _pd.DataFrame - pass def __repr__(self) -> str: rval = [] @@ -429,22 +426,22 @@ def __len__(self) -> int: return len(self.data) if self else 0 def __copy__(self) -> Self: - """Ensure all copies are deep copies.""" + """Ensure all copies are deep copies.""" # ruff: ignore[docstring-missing-returns] return deepcopy(self) def copy(self) -> Self: - """Return a deep copy of object.""" - return deepcopy(self) + """Return a deep copy of object.""" # ruff: ignore[docstring-missing-returns] + return self.__copy__() @classmethod def known_fields(cls) -> list[str]: - """Return a list of known fields in the object.""" - fields = [str(k) for k in getattr(cls, "_known_fields", {})] + """Return a list of known fields in the object.""" # ruff: ignore[docstring-missing-returns] + fields = [str(k) for k in getattr(cls, "_known_fields", {}).keys()] return fields @classmethod def required_fields(cls) -> list[str]: - """Return a list of required fields in the object.""" + """Return a list of required fields in the object.""" # ruff: ignore[docstring-missing-returns] if cls.known_fields(): return [k for k, v in cls._known_fields.items() if v.required] return [] @@ -452,8 +449,8 @@ def required_fields(cls) -> list[str]: def set_column( self, label: str, - data: Any | None = None, - default: Any | None = None, + data: Any | None = None, # ruff: ignore[any-type] + default: Any | None = None, # ruff: ignore[any-type] dtype: str | type | None = None, ) -> None: """ @@ -505,7 +502,7 @@ def set_column( self.data[label] = pd.Series(data=data_, index=self.data.index, dtype=dtype) def _data_ok(self, warn: bool = True) -> bool: - """Test whether data are complete according to specifications in ``obj._known_fields``.""" + """Test whether data are complete according to specifications in ``obj._known_fields``.""" # ruff: ignore[docstring-missing-returns] rval = True for f in self.required_fields(): if f not in self.data.columns: @@ -679,7 +676,7 @@ def from_excel( """ if sheet_name is None: try: - sheet_name_ = cls._default_excel_sheet_name + _sheet_name = cls._default_excel_sheet_name except AttributeError: msg = ( f"sheet_name is None, but {type(cls).__name__} class " @@ -767,3 +764,271 @@ def write_to_csv( # "'sheet_name' not defined and object has no valid " # "_default_excel_sheet_name attribute" # ) + + +@dataclasses.dataclass +class GSolveParameters: + """Base class to store parameters related to GSolveTable derived classes.""" + + def __param_str__(self) -> str: + # Return a string representation of the parameters + return repr(self).partition("(")[2].rpartition(")")[0] + + def __copy__(self) -> Self: + # Ensure all copies are deep copies. + return deepcopy(self) + + def copy(self) -> Self: + """Return a deep copy of object.""" # ruff: ignore[docstring-missing-returns] + return self.__copy__() + + def to_dict(self) -> dict: + """Return parameters as a dict.""" # ruff: ignore[docstring-missing-returns] + return dataclasses.asdict(self) + + def to_series( + self, + series_name: str | None = None, + index_name: str | None = None, + index_prefix: str | None = None, + ) -> _pd.Series: + """Return parameters as a Series with parameter names as the index. + + Parameters + ---------- + series_name : str | None, default is None + The series data field name. + index_name : str | None, default is None + The series index name. + index_prefix : str | None, optional + Create a multiinidex where level 0 is 'index_prefix` and level 1 are + the parameter names. + + Returns + ------- + Series + + """ + ds = _pd.Series(data=self.to_dict(), name=series_name).rename_axis(index_name) + if index_prefix: + ds.index = ds.index = _pd.MultiIndex.from_arrays( + arrays=([index_prefix] * ds.shape[0], ds.index), + ) + if index_name is not None: + ds = ds.rename_axis(["", index_name]) + + return ds + + @classmethod + def from_series( + cls, + ds: _pd.Series, + skip_missing: bool = False, + skip_unknown_parameters: bool = False, + ) -> Self: + """Generate a GsolveParameters object from a pandas.Series. + + Parameters + ---------- + ds : _pd.Series + The input Series is parsed in a dict-like manner with indicies as parameter + names and series data as values. + skip_missing: bool, default False: + How to handle cases where ``ds`` does not provide values for all parameters. + If False, raise a TypeError exception. If True and the missing parameters + have default values, create the object with default values. Parameters + without a default value must always be defined in the input series. + skip_unknown_parameters : bool, default False + If False, raise a TypeError if ``ds`` contains indices that do not match + known parameters. If True, silently ignore unknown parameters + + Returns + ------- + GSolveParameters + + """ + _ds = ds.copy() + if _ds.index.nlevels > 1: + raise ValueError("MultiIndex series not supported.") + args: dict[str, Any] = { + str(k): v for k, v in ds.items() if k in cls.__dataclass_fields__ + } + missing_args = [k for k in cls.__dataclass_fields__ if k not in args] + + if not skip_missing and missing_args: + raise TypeError( + f"skip_missing=False: missing required parameters: {missing_args}" + ) + extra_args = [k for k in _ds.index if k not in cls.__dataclass_fields__] + if extra_args and not skip_unknown_parameters: + raise TypeError( + f"series contains unknown parameters: {_ds.index[extra_args].to_list()}" + ) + + return cls(**args) + + @classmethod + def default_values(cls) -> dict: + """Return dict of default parameter values.""" # ruff: ignore[docstring-missing-returns] + return { + k: cls.__dataclass_fields__[k].default for k in cls.__dataclass_fields__ + } + + def non_default_values(self) -> dict: + """Return dict of non-default parameter values.""" # ruff: ignore[docstring-missing-returns] + defaults = self.default_values() + return {k: v for k, v in self.to_dict().items() if defaults.get(k, None) != v} + + def to_excel( + self, + fname: FilePath, + sheet_name: str | None = None, + if_workbook_exists: IfWorkbookExists = "error", + if_sheet_exists: IfSheetExists = "error", + parameter_name_label: str = "parameter", + parameter_value_label: str = "value", + **kwargs, + ) -> None: + """Write parameters to an Excel worksheet. + + Parameters + ---------- + fname : str or PathLike + The path to the output Excel file. + sheet_name : str + The name of the excel worksheet to write terrain corrections. + if_workbook_exists : {'error', 'append', 'replace'}, default 'error' + Action to take if the workbook already exists. Options are: + 'error', 'append', or 'replace'. + if_sheet_exists : {'error', 'replace', 'new'}, default 'error' + Action to take if the sheet already exists. Options are: + 'error', 'replace', or 'new'. + parameters_label : str, default is 'parameter' + Set the header label for parameter names column in output worksheet. + values_label : str, default is 'value' + Set the header label for parameter names column in output worksheet. + kwargs : dict + Additional keyword arguments passed to ``pandas.DataFrame.to_excel``. + + See Also + -------- + write_excel_worksheet : Function to write a DataFrame to an Excel worksheet + with options for handling existing workbooks and sheets. + pandas.Dataframe.to_excel + + """ + params_ds = self.to_series( + index_name=parameter_name_label, series_name=parameter_value_label + ) + if sheet_name is None: + sheet_name = getattr(self, "_default_excel_sheet_name", None) + if sheet_name is None: + raise ValueError( + "sheet_name is None and object has no " + "_default_excel_sheet_name attribute." + ) + + write_excel_worksheet( + prepare_writable_df(params_ds.to_frame(), normalize_column_names=True), + fname, + sheet_name=sheet_name, + if_workbook_exists=if_workbook_exists, + if_sheet_exists=if_sheet_exists, + **kwargs, + ) + + # Todo: remove this method + def summary( + self, include_name: bool = True, as_list: bool = True + ) -> list[str] | str: + """ + Return parameters as strings in the form 'param: value'. + + Parameters + ---------- + include_name : bool, default True + Include the class name in the output. + as_list : bool, default True + Return the output as a list of strings. If False, return as a single string + with each parameter on a new line. + + Returns + ------- + list[str] | str + The parameters as a string or list of strings. + """ + txt = [] + if include_name: + txt.append(f"{type(self).__name__}") + txt.extend([f"{k}: {v}" for k, v in self.to_dict().items()]) + if as_list: + return txt + return "\n".join(txt) + + +def _concat_gsolvetable_dataframes_with_fill( + df1: _pd.DataFrame, + df2: _pd.DataFrame, + fill_str: str | None = "", + fill_bool: bool | None = None, + known_fields: dict[str, Any] | None = None, + **kwargs, +) -> _pd.DataFrame: + + if kwargs.get("axis", 0) != 0: + raise ValueError( + f"incompatible kwarg axis={kwargs['axis']}, " + "function operates in vstack (axis=0) mode only." + ) + kwargs["axis"] = 0 + + use_known_fields = False + if known_fields is not None: + use_known_fields = True + if not all([hasattr(f, "default") for f in known_fields.values()]): + raise TypeError( + f"if specified, known_fields must be a dict of DataFieldSpecification objects" + ) + + do_str_fill = fill_str is not None + if do_str_fill: + fill_str = str(fill_str) + + do_bool_fill = fill_bool is not None + if do_bool_fill: + fill_bool = bool(fill_bool) + + combined_df = _pd.concat([df1, df2], **kwargs) + if not use_known_fields and not do_str_fill and not do_bool_fill: + return combined_df + + in_df1_only = [c for c in df1.columns if c not in df2.columns] + idx = df2.index + for c in in_df1_only: + if use_known_fields and c in known_fields: + if known_fields[c].default is not None: + combined_df.loc[idx, c] = combined_df.loc[idx, c].fillna( + known_fields[c].default + ) + continue + if do_str_fill and is_string_dtype(df1[c]): + combined_df.loc[idx, c] = combined_df.loc[idx, c].fillna(fill_str) + elif do_bool_fill and is_bool_dtype(df1[c]): + combined_df.loc[idx, c] = combined_df.loc[idx, c].fillna(fill_bool) + + in_df2_only = [c for c in df2.columns if c not in df1.columns] + idx = df1.index + for c in in_df2_only: + if use_known_fields and c in known_fields: + if known_fields[c].default is not None: + combined_df.loc[idx, c] = combined_df.loc[idx, c].fillna( + known_fields[c].default + ) + continue + if do_str_fill and is_string_dtype(df2[c]): + combined_df.loc[idx, c] = combined_df.loc[idx, c].fillna(fill_str) + continue + elif do_bool_fill and is_bool_dtype(df2[c]): + combined_df.loc[idx, c] = combined_df.loc[idx, c].fillna(fill_bool) + + return combined_df diff --git a/src/gsolve/core/excel_io.py b/src/gsolve/core/excel_io.py index 4c146be..b9b17a2 100644 --- a/src/gsolve/core/excel_io.py +++ b/src/gsolve/core/excel_io.py @@ -81,8 +81,9 @@ def get_true_sheet_name( return sheet_names[sheet_name] except IndexError as err: if raise_error: - msg = f"excel file {excel_file} has no sheet at index: {sheet_name}" - raise ValueError(msg) from err + raise ValueError( + f"excel file {excel_file} has no sheet at index: {sheet_name}" + ) return None else: sheet_names_lc = [ @@ -91,24 +92,21 @@ def get_true_sheet_name( if sheet_name.lower() in sheet_names_lc: return sheet_names[sheet_names_lc.index(sheet_name.lower())] if raise_error: - msg = f"excel file {excel_file} has no sheet named '{sheet_name}'" - raise ValueError(msg) + raise ValueError( + f"excel file {excel_file} has no sheet named '{sheet_name}'" + ) return None def _parse_sheet_name_arg( sheet_name: str | int | Sequence[str | int], ) -> list[str | int]: - """Parse and validate sheet_name argument. - - Returns - ------- - list : str - Sheet names as a list - """ - sheet_name_list: list[str | int] = ( - list(sheet_name) if is_list_like(sheet_name) else [sheet_name] - ) + """Parse and validate sheet_name argument.""" # ruff: ignore[docstring-missing-returns] + sheet_name_list: list[str | int] + if is_list_like(sheet_name): + sheet_name_list = [s for s in sheet_name] # pyrefly:ignore[not-iterable] + else: + sheet_name_list = [sheet_name] # pyrefly:ignore[bad-assignment] if not all(isinstance(s, (str, int)) for s in sheet_name_list): msg = ( @@ -242,8 +240,9 @@ def write_excel_worksheet( excel_file = Path(excel_file) if excel_file.exists(): if if_workbook_exists == "error": - msg = f"file {excel_file} already exists, and arg {if_workbook_exists=}" - raise ValueError(msg) + raise ValueError( + f"file {excel_file} already exists, and arg {if_workbook_exists=}" + ) if if_workbook_exists == "append": writer_kwargs["mode"] = "a" diff --git a/src/gsolve/core/utils.py b/src/gsolve/core/utils.py index f7898c5..efbf42d 100644 --- a/src/gsolve/core/utils.py +++ b/src/gsolve/core/utils.py @@ -22,8 +22,10 @@ import itertools import sys +import warnings from collections.abc import Sequence -from typing import Any, Literal, TypeAliasType, get_args, get_origin, overload +from os import PathLike +from typing import Any, Literal, Type, TypeAliasType, get_args, get_origin, overload import numpy as np import pandas as pd @@ -67,14 +69,11 @@ "prepare_writable_df", "round_coords", "timestamp_to_columns", - "to_1d_ndarray", - "to_1d_ndarray_or_float", "to_naive_utc_datetime", - "to_points3d", ] -def is_filepath_like(obj: Any) -> bool: +def is_filepath_like(obj: Any) -> bool: # ruff: ignore[any-type] """Test if object type is compatible with ``gsolve.core._typing.FilePath``. Returns @@ -84,7 +83,7 @@ def is_filepath_like(obj: Any) -> bool: return isinstance(obj, FilePath.__value__) -def is_in_literal(value: Any, literal_type: TypeAliasType) -> bool: +def is_in_literal(value: Any, literal_type: TypeAliasType) -> bool: # ruff: ignore[any-type] """Test if value is in a Literal type. Parameters @@ -109,11 +108,10 @@ def is_in_literal(value: Any, literal_type: TypeAliasType) -> bool: if get_origin(literal_type.__value__) is Literal: return value in get_args(literal_type.__value__) - msg = f"{literal_type} is not a Literal type" - raise TypeError(msg) + raise TypeError(f"{literal_type} is not a Literal type") -def is_datetime_array(v: Any) -> bool: +def is_datetime_array(v: Any) -> bool: # ruff: ignore[any-type] """Test if the input is a datetime-like array. Returns @@ -123,7 +121,7 @@ def is_datetime_array(v: Any) -> bool: return isinstance(v, DatetimeArray.__value__) -def is_points3d_like(v: Any) -> bool: +def is_points3d_like(v: Any) -> bool: # ruff: ignore[any-type] """Test if value is compatible with Points3D type. Note that is not possible to test the data type of contained arrays. @@ -246,7 +244,7 @@ def _nat_check[T](v: T) -> T: if isinstance(t, (NaTType, NAType)): return _nat_check(pd.NaT) - if isinstance(t, (pd.Timestamp, pd.DatetimeIndex)): + if isinstance(t, pd.Timestamp) or isinstance(t, pd.DatetimeIndex): return _nat_check(t if t.tz is None else t.tz_convert("UTC").tz_localize(None)) if isinstance(t, pd.Series): ds = t if t.dtype == "datetime64[ns]" else pd.to_datetime(t, **kwargs) @@ -268,12 +266,11 @@ def _nat_check[T](v: T) -> T: return_scalar = True try: idx = pd.to_datetime(t, **kwargs) - except Exception as err: - msg = ( + except Exception: + raise ValueError( f"unable to convert input '{t}' of type {type(t).__name__} " "to Timestamp or DateTimeIndex" ) - raise ValueError(msg) from err rval = _nat_check( idx if idx.tz is None else idx.tz_convert("UTC").tz_localize(None) @@ -335,29 +332,11 @@ def to_1d_ndarray( return a -def to_1d_ndarray_or_float( - a: ArrayLike, dtype: DTypeLike = np.float64 -) -> NDArray[np.float64] | np.float64: - """Convert input to a 1D numpy array or a float. - - If the input is a length-1 array, it will be converted to a float. - - Parameters - ---------- - a : array-like - The input to be converted to a 1D numpy array or a float. - dtype : data-type, optional - If specified, the resulting array will be cast to this data type. - If None, the default data type is np.float64. - - Returns - ------- - numpy.ndarray or float - A 1D numpy array or a float, depending on the size of the input. - - """ - a = to_1d_ndarray(a, dtype=dtype) - return a[0] if a.size == 1 else a +def to_1d_ndarray_or_float(a: ArrayLike) -> NDArray[np.float64] | np.float64: + _a = to_1d_ndarray(a).astype(np.float64) + if _a.size == 1: + return _a[0] + return _a # Remove in future release @@ -369,10 +348,10 @@ def check_duplicate_index(idx: pd.Index | pd.DataFrame | pd.Series) -> None: msg = f"idx must be a pandas Index, DataFrame, or Series, not {type(idx).__name__}" raise TypeError(msg) - if idx.duplicated().any(): - idx_dupes = idx[idx.duplicated().tolist()] - msg = f"duplicate index values: {idx_dupes.unique().to_list()}" - raise ValueError(msg) + if _idx.duplicated().any(): + idx_name = _idx.name or "index" + idx_dupes = _idx[_idx.duplicated().tolist()] + raise ValueError(f"duplicate index values: {idx_dupes.unique().to_list()}") @overload @@ -408,8 +387,7 @@ def normalize_field_names(df: pd.DataFrame | pd.Series) -> pd.DataFrame | pd.Ser df = df.rename_axis(index=normalize_str) df.name = normalize_str(str(df.name)) return df - msg = f"df must be a pandas DataFrame or Series, not {type(df).__name__}" - raise TypeError(msg) + raise TypeError(f"df must be a pandas DataFrame or Series, not {type(df).__name__}") @overload @@ -785,7 +763,6 @@ def expand_datetime_column( msg = ( f"overwrite=False and splitting would overwrite columns: {existing}" ) - raise ValueError(msg) df = df.drop(columns=existing) if not insert_after: diff --git a/src/gsolve/gsolve_algorithms.py b/src/gsolve/gsolve_algorithms.py index 60ee014..7fc7867 100644 --- a/src/gsolve/gsolve_algorithms.py +++ b/src/gsolve/gsolve_algorithms.py @@ -77,8 +77,7 @@ def call_gsolve_lstsq( # index in obs where ties are located m_ties = ref_sites.index.intersection(obs["site_id"].to_list()) if m_ties.empty: - msg = "no tie sites" - raise ValueError(msg) + raise ValueError("no tie sites") ref_sites = ref_sites.loc[m_ties] # set up g_solver_lstsq input arguments - do this segmented so that can report @@ -164,8 +163,7 @@ def call_gsolve_calibration( # index in obs where ties are located m_ties = ref_sites.index.intersection(obs["site_id"].to_list()) if m_ties.empty: - msg = "no tie sites" - raise ValueError(msg) + raise ValueError("no tie sites") ref_sites = ref_sites.loc[m_ties] # set up g_solver_lstsq input arguments diff --git a/src/gsolve/gsolve_outputs.py b/src/gsolve/gsolve_outputs.py index 02ef1cd..4b9ad22 100644 --- a/src/gsolve/gsolve_outputs.py +++ b/src/gsolve/gsolve_outputs.py @@ -134,12 +134,7 @@ class GSolveResults: """ - obs_solution: pd.DataFrame - site_solution: pd.DataFrame - loop_solution: pd.DataFrame - observations_input: pd.DataFrame - reference_sites_input: pd.DataFrame - params: GSolveSolutionParameters + """ # ruff: ignore[incorrect-section-order] def __init__( self, @@ -281,7 +276,8 @@ def plot_residual_cdf( loops = df_loops ax_title = f"{ax_title} each loop" elif is_list_like(loop): - loops: list[str] = [str(l) for l in loop] + loops: list[str] = [str(l) for l in loop] # type: ignore[bad-assignment-type] + ax_title = f"{ax_title} loops {', '.join(loops)}" elif loop == "all": loops = ["all"] df["loop"] = "all" @@ -388,7 +384,7 @@ def plot_residual_drift( x_col: str = "timedelta" y_col: str = "residual" - drift = float(self.loop_solution.loc[loop, "drift"]) + drift = float(self.loop_solution.at[loop, "drift"]) # type: ignore[bad-argument-type] # ruff: ignore[pandas-use-of-dot-at] m_loop = self.obs_solution["loop"].eq(loop) m_active = self.obs_solution["active"].eq(True) diff --git a/src/gsolve/meter_conversion.py b/src/gsolve/meter_conversion.py index a77a184..34a58a5 100644 --- a/src/gsolve/meter_conversion.py +++ b/src/gsolve/meter_conversion.py @@ -19,9 +19,10 @@ """Module for converting Lacoste-Romberg G and D meter readings to mGal.""" import pathlib +import warnings from collections.abc import Sequence from io import StringIO -from typing import Protocol, Self, TextIO, runtime_checkable +from typing import Any, Protocol, TextIO, runtime_checkable import numpy as np import numpy.typing as npt @@ -261,7 +262,7 @@ def set_datetime_range( self._endtime = et @property - def starttime(self) -> pd.Timestamp | None: + def starttime(self) -> _pd.Timestamp | None: """The date from which correction parameters are valid.""" st = getattr(self, "_starttime", None) if st is not None and not isinstance(st, pd.Timestamp): @@ -270,7 +271,7 @@ def starttime(self) -> pd.Timestamp | None: return st @property - def endtime(self) -> pd.Timestamp | None: + def endtime(self) -> _pd.Timestamp | None: """The date up to which correction parameters are valid.""" r = getattr(self, "_endtime", None) if r is not None and not isinstance(r, pd.Timestamp): diff --git a/src/gsolve/observations.py b/src/gsolve/observations.py index b124014..10ba49a 100644 --- a/src/gsolve/observations.py +++ b/src/gsolve/observations.py @@ -97,7 +97,7 @@ class GravityObservationsParameters(GSolveParameters): earthtide_correction_method: str = "" ocean_load_correction_method: str = "" - def __setattr__(self, name: str, value: Any) -> None: + def __setattr__(self, name: str, value: Any) -> None: # ruff: ignore[any-type] if name == "timedelta_unit": value = pd.Timedelta(value) elif name == "fixed_time_datum": @@ -479,8 +479,7 @@ def set_obs_id( msg = f"{len(dupes)} duplicated obs_id's : {dupes.unique().to_list()}" if duplicated_obs_id == "error": - msg_0 = f"{msg}" - raise ValueError(msg_0) + raise ValueError(f"{msg}") if duplicated_obs_id == "keep": _warnings.warn(f"keeping {msg}") elif duplicated_obs_id == "rename": @@ -819,6 +818,10 @@ def set_calibration_factor( msg = "Multiple gravity meters found in data, must specify ``meter_id``" raise ValueError(msg) + if self.data["meter_id"].nunique() > 1 and meter_id is None: # ruff: ignore[pandas-nunique-constant-series-check] + raise ValueError( + "Multiple gravity meters found in data, must specify ``meter_id``" + ) if meter_id is None: self.set_column(c_label, float(calibration_factor)) else: @@ -988,8 +991,7 @@ def _parse_inputs(o: str | Iterable[str] | None) -> list[str]: return [o] if isinstance(o, Iterable): return [str(oi) for oi in o] - msg = f"invalid input of type '{type(o).__name__}'" - raise TypeError(msg) + raise TypeError(f"invalid input of type '{type(o).__name__}'") # parse all args first to check for errors before modifying data obs_id = _parse_inputs(obs_id) @@ -1030,13 +1032,8 @@ def _get_writable_df( bool_to_int: bool = True, include_unknown_fields: bool | Sequence[str] = True, active_only: bool = False, - ) -> pd.DataFrame: - """Return a DataFrame suitable for writing to an excel or csv file. - - Returns - ------- - Dataframe - """ + ) -> _pd.DataFrame: + """Return a DataFrame suitable for writing to an excel or csv file.""" # ruff: ignore[docstring-missing-returns] cols = [c for c in self.known_fields() if c in self.data.columns] if include_unknown_fields: if include_unknown_fields is True: @@ -1146,7 +1143,7 @@ def plot_observed_data( y_column: str = "meter_reading_mgal", savefilename: FilePath | None = None, figsize: tuple[float, float] = (12, 8), - ax: plt.Axes | None = None, + ax=None, # ruff: ignore[missing-type-function-argument] show: bool = True, **kwargs, ) -> tuple[plt.Figure, plt.Axes]: @@ -1354,9 +1351,9 @@ def plot_site_visits(self, loop: str) -> None: ticks=list(site_id_to_int.values()), labels=list(site_id_to_int.keys()) ) - def loop_summary(self) -> pd.DataFrame: - """Return a summary of the observations by loop.""" - from gsolve.core._summary_functions import ( # ruff: ignore[import-outside-top-level] + def loop_summary(self) -> _pd.DataFrame: + """Return a summary of the observations by loop.""" # ruff: ignore[docstring-missing-returns] + from gsolve.core._summary_functions import ( duration_hr, endtime_utc, n_sites, @@ -1479,9 +1476,8 @@ def check_data(self, warn: bool = True) -> bool: if "loop" in self.data.columns and "meter_id" in self.data.columns: for l in self.loop_ids: - # m = self.data.loc[self.data["loop"].eq(l)].to_numpy() - m = self.data.loc[self.data["loop"].eq(l), "meter_id"].to_numpy() - if not (m.shape[0] == 0 or (m[0] == m).all()): + m = self.data["loop"].eq(l) + if self.data.loc[m, "meter_id"].nunique() > 1: # ruff: ignore[pandas-nunique-constant-series-check] warner(f"Multiple gravity meters found in loop '{l}'") warner.final_msg() diff --git a/src/gsolve/reductions/corrections.py b/src/gsolve/reductions/corrections.py index a34702d..10fd6a8 100644 --- a/src/gsolve/reductions/corrections.py +++ b/src/gsolve/reductions/corrections.py @@ -869,7 +869,7 @@ def compute( return GravityCorrections(params=self.params, site_id=idx, **df_dict) def _configured_bouguer_corrections(self) -> Sequence[str]: - """Return bouguer correction method names required for the current parameters.""" + """Return bouguer correction method names required for the current parameters.""" # ruff: ignore[docstring-missing-returns] corrections = ["normal_gravity_at_ellipsoid", "free_air_correction"] if self.params.use_atmospheric_correction: corrections.append("atmospheric_correction") diff --git a/src/gsolve/reductions/terrain_corrections.py b/src/gsolve/reductions/terrain_corrections.py index e748bb5..3ad3109 100644 --- a/src/gsolve/reductions/terrain_corrections.py +++ b/src/gsolve/reductions/terrain_corrections.py @@ -18,9 +18,7 @@ """Functions and classes for computing gravity terrain corrections.""" import dataclasses -import operator import pathlib -import sys import warnings from collections.abc import Iterable, Sequence from types import MappingProxyType @@ -31,6 +29,7 @@ import numpy.typing as npt import pandas as pd import xarray as xr +from pandas.core.series import Series from tqdm import tqdm as _tqdm from gsolve.core._typing import ( @@ -65,8 +64,8 @@ ] -def _is_dataarray(obj: Any) -> bool: - """Check if an object is an xarray DataArray.""" +def _is_dataarray(obj: Any) -> bool: # ruff: ignore[any-type] + """Check if an object is an xarray DataArray.""" # ruff: ignore[docstring-missing-returns] return isinstance(obj, xr.DataArray) @@ -262,7 +261,7 @@ def calculate_terrain_correction( pt_topo_density = topo_density tcorr_topo[i] = tcorr_harmonica_topography( - point=(px, py, pz), + (px, py, pz), topography=pt_topo_elev, topography_density=pt_topo_density, ) @@ -280,7 +279,7 @@ def calculate_terrain_correction( pt_bathy_density = bathy_density tcorr_bathy[i] = tcorr_harmonica_bathymetry( - point=(px, py, pz), + (px, py, pz), bathymetry=pt_bathy_depth, bathymetry_density=pt_bathy_density, sea_level_elevation=sea_level_elevation, @@ -1134,9 +1133,8 @@ def __init__( "params is specified but terrain_corrections is None: " "must specify both or neither" ) - raise ValueError(msg) if params is None and terrain_corrections is not None: - msg = ( + raise ValueError( "terrain_corrections is specified but params is None: " "must specify both or neither" ) @@ -1652,8 +1650,7 @@ def to_csv(self, fname: FilePath | None = None, **kwargs) -> str | None: csv = "\n".join(csv) if fname is None: return csv - pathlib.Path(fname).write_text(csv) # ruff: ignore[unspecified-encoding] - return None + pathlib.Path(fname).write_text(csv) @classmethod def from_csv( @@ -1675,7 +1672,7 @@ def from_csv( TerrainCorrectionOutput """ - with pathlib.Path(fname).open() as f: # ruff: ignore[unspecified-encoding] + with pathlib.Path(fname).open() as f: lines = f.readlines() params = [l.lstrip("#").strip().split(",") for l in lines if l.startswith("#")] if len(params) == 0: @@ -1753,7 +1750,7 @@ def get_corrections( raise ValueError(err_msg) if if_missing == "drop": - warnings.warn(f"{err_msg}, dropping from output") + warnings.warn(f"{err_msg}, dropping from ouput") return tcorrs.loc[site_id_found, cols] warnings.warn(f"{err_msg}, filling with {fill_value}") diff --git a/src/gsolve/reports.py b/src/gsolve/reports.py index 9cb470b..1d357a8 100644 --- a/src/gsolve/reports.py +++ b/src/gsolve/reports.py @@ -16,8 +16,7 @@ # Copyright (c) 2025 Earth Sciences New Zealand. - -import copy +from copy import deepcopy from pathlib import Path from typing import Any, Self @@ -126,12 +125,12 @@ def __init__( self._set_terrain_correction_data(terrain_corrections=terrain_corrections) def copy(self) -> Self: - """Return a deep copy.""" - return copy.copy(self) + """Return a deep copy.""" # ruff: ignore[docstring-missing-returns] + return self.__copy__() def __copy__(self) -> Self: - """Return a deep copy.""" - return copy.deepcopy(self) + """Return a deep copy.""" # ruff: ignore[docstring-missing-returns] + return deepcopy(self) def _set_params( self, @@ -314,7 +313,7 @@ def to_excel( filename: FilePath, if_workbook_exists: IfWorkbookExists = "error", if_sheet_exists: IfSheetExists = "error", - **kwargs: Any, + **kwargs: Any, # ruff: ignore[any-type] ) -> None: """ Save the report data to an Excel file. @@ -414,9 +413,9 @@ def to_excel( all_params = [] # This is a kludge - should create method on parameter objects to - # to normalize parameter outputs for writing to excel. - def _format_value(x: Any) -> str | float | int | bool: - if isinstance(x, pd.Timedelta): + # to normalise parameter outputs for writing to excel. + def _format_value(x: Any) -> str | float | int | bool: # ruff: ignore[any-type] + if isinstance(x, _pd.Timedelta): return x.total_seconds() if isinstance(x, Path): return str(x) diff --git a/src/gsolve/scintrex.py b/src/gsolve/scintrex.py index a13fa78..1bb3615 100644 --- a/src/gsolve/scintrex.py +++ b/src/gsolve/scintrex.py @@ -17,7 +17,7 @@ # Copyright (c) 2025 Earth Sciences New Zealand. import abc -import copy +import dataclasses import pathlib import warnings from collections.abc import Callable, Mapping, Sequence @@ -47,9 +47,7 @@ __all__ = ["CG6Data", "ScintrexData"] -type _ScintrexMetadataDataTypes = str | float | int | bool | pd.Timestamp - -type _ScintrexOnErrorOptions = Literal["raise", "warn", "ignore"] +_ScintrexMetadataDataTypes: TypeAlias = str | float | int | bool | pd.Timestamp class ScintrexData(abc.ABC): @@ -125,7 +123,7 @@ def copy(self) -> Self: ------- ScintrexData """ - return copy.copy(self) + return self.__copy__() # ruff: ignore[unnecessary-dunder-call] class CG6Data(ScintrexData): @@ -338,7 +336,7 @@ def _set_data(self, data: pd.DataFrame, on_error: str = "raise") -> None: self.data = df def _strip_corrections(self) -> pd.Series: - """Return corrgrav values with all corrections removed.""" + """Return corrgrav values with all corrections removed.""" # ruff: ignore[docstring-missing-returns] return ( self.data["corrgrav"] - (self.data["driftcorr"] * self.data["correction_drift"]) @@ -473,8 +471,7 @@ def set_loop( Name of the output column. """ if loop_format and "LOOP" not in loop_format: - msg = "format_str must contain 'LOOP'." - raise ValueError(msg) + raise ValueError("format_str must contain 'LOOP'.") # ensure only one method is used args = (field, array, datetimes, time_gap) @@ -620,8 +617,9 @@ def to_gsolve_observations( include_non_standard_fields = [str(f) for f in include_non_standard_fields] missing = [f for f in include_non_standard_fields if f not in df.columns] if missing: - msg = f"Requested non-standard fields not found in data: {missing}" - raise KeyError(msg) + raise KeyError( + f"Requested non-standard fields not found in data: {missing}" + ) to_drop = set(df.columns) - set( GravityObservations.known_fields() + include_non_standard_fields @@ -752,7 +750,7 @@ def set_drift_correction( def _slurp_scintrex_text_file(filepath: FilePath) -> list[str]: - """Read a Scintrex text file, fix encoding and return lines as a list.""" + """Read a Scintrex text file, fix encoding and return lines as a list.""" # ruff: ignore[docstring-missing-returns] with pathlib.Path(filepath).open("r", encoding="utf-8-sig") as fh: return [l.strip() for l in fh] @@ -762,7 +760,7 @@ def _split_header_key_val_unit( normalize_key: bool = True, extract_units: bool = True, ) -> tuple[str, str, str]: - """Split headers into key, value and units.""" + """Split headers into key, value and units.""" # ruff: ignore[docstring-missing-returns] header = header.strip("/ ") if not header: return ("", "", "") @@ -787,7 +785,7 @@ def _scintrex_header_type_conversion( header_val: _ScintrexMetadataDataTypes, data_type: type | Callable | None = None, ) -> _ScintrexMetadataDataTypes | None: - if header_val is None or data_type is None: + if not header_val or data_type is None: return "" if data_type is pd.Timestamp: @@ -814,7 +812,7 @@ def _scintrex_header_type_conversion( def _extract_unit_from_keyword(header: str) -> tuple[str, str]: - """Get header and unit form a header string.""" + """Get header and unit form a header string.""" # ruff: ignore[docstring-missing-returns] if header.endswith(")"): sep = "(" elif header.endswith("]"): diff --git a/src/gsolve/sites.py b/src/gsolve/sites.py index 5201eab..8592911 100644 --- a/src/gsolve/sites.py +++ b/src/gsolve/sites.py @@ -568,7 +568,7 @@ def sample_elevation( xcol: str = "easting", ycol: str = "northing", method: str = "nearest", - ) -> pd.Series | None: + ) -> _pd.Series | None: """Get elevations at site locations from an DEM/xarray grid. Parameters diff --git a/src/gsolve/tide/earth_tide.py b/src/gsolve/tide/earth_tide.py index a15f4ce..417379d 100644 --- a/src/gsolve/tide/earth_tide.py +++ b/src/gsolve/tide/earth_tide.py @@ -1171,20 +1171,17 @@ def time_series( msg = "No results returned from pygtide prediction." raise ValueError(msg) # ruff: ignore[type-check-without-type-error] - normalized_cols = ["datetime", "signal", "tide", "pole_tide", "lod_tide"] - tides_df = tides_df.rename( - columns=dict(zip(tides_df.columns, normalized_cols, strict=True)) - ) - tides_df["datetime"] = to_naive_utc_datetime(tides_df["datetime"]) - tides_df = tides_df.set_index("datetime") - - # values in nm/s2, no conversion required + normalised_cols = ["datetime", "signal", "tide", "pole_tide", "lod_tide"] + df = df.rename( + columns={a: b for a, b in zip(df.columns, normalised_cols)} + ).set_index("datetime") + df = df.set_index(to_naive_utc_datetime(df.index)) + if unit == "nm/s^2": + return df if unit == "ugal": - tides_df *= 1e-3 - elif unit == "mgal": - tides_df *= 1e-4 - - return tides_df + return df * 1e-3 + if unit == "mgal": + return df * 1e-4 # TODO: site_id is not truly required, so remove and infer sites from lat/lon/elev def tidal_correction( @@ -1247,7 +1244,6 @@ def tidal_correction( "site_id is a required parameter for " "EternaPredictTidalCorrection tidal_correction method." ) - raise ValueError(msg) if isinstance(site_id, str): site_id = [site_id] * lat.size site_id = to_1d_ndarray(site_id, expected_size=lat.size, dtype=str) diff --git a/src/gsolve/tide/ocean_load.py b/src/gsolve/tide/ocean_load.py index 6b0d072..a05820f 100644 --- a/src/gsolve/tide/ocean_load.py +++ b/src/gsolve/tide/ocean_load.py @@ -18,6 +18,7 @@ """Methods and classes for reading and applying ocean load corrections to gravity data.""" +import pathlib import warnings from pathlib import Path from typing import Any, Literal, Protocol, runtime_checkable @@ -151,8 +152,8 @@ def __init__( ) self.metadata: dict[str, Any] = metadata - def identifier(self) -> str: - """Corrector identifier string.""" + def identifier(self, **kwargs) -> str: + """Corrector identifier string.""" # ruff: ignore[docstring-missing-returns] return f"{type(self).__name__}()" def ocean_load_correction( @@ -295,8 +296,8 @@ def __repr__(self) -> str: md = ",".join([f"{v}={k}" for v, k in self.metadata.items()]) return f"{cname}({md})" - def identifier(self) -> str: - """Corrector identifier string.""" + def identifier(self, **kwargs) -> str: + """Corrector identifier string.""" # ruff: ignore[docstring-missing-returns] return f"{self.__class__.__name__}()" @property @@ -377,17 +378,16 @@ class since it provides corrections for a single station. def _datetimes_to_np_datetime64( dt: DatetimeScalar | DatetimeArray, dtype: str = "datetime64" ) -> np.ndarray: - """Convert datetimes to numpy datetime64 array.""" - dt = to_naive_utc_datetime(dt, allow_nat=False) - if isinstance(dt, pd.Timestamp): - return np.array([dt], dtype=dtype) - if isinstance(dt, (pd.DatetimeIndex, pd.Series)): - return np.atleast_1d(dt).astype(dtype) - msg = ( + """Convert datetimes to numpy datetime64 array.""" # ruff: ignore[docstring-missing-returns] + _dt = to_naive_utc_datetime(dt, allow_nat=False) + if isinstance(_dt, pd.Timestamp): + return np.array([_dt], dtype=dtype) + if isinstance(_dt, (pd.DatetimeIndex, pd.Series)): + return np.atleast_1d(_dt).astype(dtype) + raise TypeError( "datetimes must be a pandas Timestamp, DatetimeIndex, or Series, not " f"{type(dt).__name__}." ) - raise TypeError(msg) def _validate_timeseries_data(df: pd.DataFrame) -> None: @@ -458,7 +458,7 @@ def qtp_to_corrector( if corr_type == "auto": # determine file type by reading first line - with Path(file_path).open("r", encoding="iso-8859-1") as f: + with pathlib.Path(file_path).open("r", encoding="iso-8859-1") as f: first_line = f.readline() if first_line.strip().startswith("Year DOY Time"): corr_type = "timeseries" @@ -722,7 +722,7 @@ def _get_model_parameters(self, f: FilePath) -> None: "ocean_tide_model": "", "center_mass_correction": False, } - with Path(f).open() as fh: # ruff: ignore[unspecified-encoding] + with pathlib.Path(f).open() as fh: model_txt = [l.strip() for l in fh if l.startswith("$$")] for l in model_txt: if l.startswith("$$ Greens function:"): From ead28bef6a4415138bca45c04e3c3f097ac8b0b1 Mon Sep 17 00:00:00 2001 From: Adrian Benson Date: Sat, 12 Sep 2026 01:30:56 +1200 Subject: [PATCH 05/36] Start fixing pre commit hook issues. --- examples/scripts/okataina_survey.py | 2 +- pyproject.toml | 27 +- src/gsolve/core/data.py | 62 +++-- src/gsolve/core/excel_io.py | 31 ++- src/gsolve/core/utils.py | 122 ++++++--- src/gsolve/core/xr_accessor.py | 10 +- src/gsolve/core/xr_methods.py | 18 +- src/gsolve/gsolve_algorithms.py | 14 +- src/gsolve/gsolve_outputs.py | 8 +- src/gsolve/meter_conversion.py | 46 ++-- src/gsolve/observations.py | 110 ++++---- src/gsolve/reductions/anomalies.py | 24 +- src/gsolve/reductions/corrections.py | 19 +- src/gsolve/reductions/terrain_corrections.py | 270 ++++++++++--------- src/gsolve/reports.py | 18 +- src/gsolve/scintrex.py | 71 +++-- src/gsolve/sites.py | 40 ++- src/gsolve/tide/earth_tide.py | 136 ++++++---- src/gsolve/tide/ocean_load.py | 61 ++--- 19 files changed, 631 insertions(+), 458 deletions(-) diff --git a/examples/scripts/okataina_survey.py b/examples/scripts/okataina_survey.py index 33834cb..9abe2c0 100644 --- a/examples/scripts/okataina_survey.py +++ b/examples/scripts/okataina_survey.py @@ -122,7 +122,7 @@ qtp_output_file = ocean_load_path / "okataina_qtp_input_Modified.csv" if not qtp_output_file.exists(): - msg = "You didn't run QuickTide Pro yet did you?" + msg = f"You didn't run QuickTide Pro yet did you?" raise FileNotFoundError(msg) qtp_ocean_load_corrector = qtp_to_corrector( diff --git a/pyproject.toml b/pyproject.toml index baf4203..51edc62 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -159,25 +159,12 @@ ignore = [ "RET504", # Allow variable assignment only for return "PT001", # Conventions for parenthesis on pytest.fixture "D200", # Allow single line docstrings in their own line + "B028", # Allow no stacklevel in warnings.warn ] [tool.ruff.lint.per-file-ignores] -"examples/scripts/**" = [ - "D", # Docs not required for scripts - "DOC", # ditto - "print", # Allow print() in scripts -] -"tests/**" = [ - "missing-type-function-argument", # type annotations not required for func args - "missing-return-type-undocumented-public-function", # or public func return - "missing-return-type-private-function", # or private func return - "D", # Docstrings not required - "DOC", # Docstrings not required - "no-self-use", # allow unused self in test classes - "import-private-name", # permit import of private functions etc - we are need to test em - "pytest-raises-too-broad", # allow tests to catch exceptions without a 'match' arg - "pytest-warns-too-broad", # allow tests to catch warnings without a 'match' arg -] +"tests/**" = ["ANN001", "ANN201", "ANN202", "D", "DOC", "PLR6301"] +"examples/scripts/**" = ["D100"] [tool.ruff.lint.pydocstyle] convention = "numpy" @@ -230,3 +217,11 @@ notice = ''' # SPDX-License-Identifier: GPLv3 ''' exclude = ["__init__.py"] + + +[tool.ty.src] +include = ["gsolve/", "examples/scripts/"] + + +[tool.numpydoc_validation] +checks = ["all", "EX01", "SA01"] diff --git a/src/gsolve/core/data.py b/src/gsolve/core/data.py index 9785d3c..57ca023 100644 --- a/src/gsolve/core/data.py +++ b/src/gsolve/core/data.py @@ -24,7 +24,6 @@ from copy import deepcopy from typing import Any, ClassVar, Self -import numpy as np import pandas as pd from pandas.api.types import is_bool_dtype, is_string_dtype @@ -402,14 +401,14 @@ class GSolveTable(_HasKnownFields, abc.ABC): _default_excel_sheet_name: ClassVar[str | tuple[str, ...]] = "" def __init__(self) -> None: - self.data: _pd.DataFrame + self.data: pd.DataFrame def __repr__(self) -> str: rval = [] if hasattr(self, "data"): rval.append(f"data:shape={self.data.shape}") if hasattr(self, "params") and isinstance(self.params, GSolveParameters): - rval.append(self.params._param_str()) + rval.append(self.params.__param_str__()) rval = ", ".join(rval) @@ -431,12 +430,12 @@ def __copy__(self) -> Self: def copy(self) -> Self: """Return a deep copy of object.""" # ruff: ignore[docstring-missing-returns] - return self.__copy__() + return deepcopy(self) @classmethod def known_fields(cls) -> list[str]: """Return a list of known fields in the object.""" # ruff: ignore[docstring-missing-returns] - fields = [str(k) for k in getattr(cls, "_known_fields", {}).keys()] + fields = [str(k) for k in getattr(cls, "_known_fields", {})] return fields @classmethod @@ -485,10 +484,10 @@ def set_column( break if dtype == "datetime": - data_ = to_naive_utc_datetime(pd.to_datetime(data)) + data_ = to_naive_utc_datetime(data) # ty:ignore[no-matching-overload] # pyrefly:ignore dtype = None elif dtype == "timedelta": - data_ = pd.to_timedelta(data) + data_ = pd.to_timedelta(data) # ty:ignore[no-matching-overload] # pyrefly:ignore dtype = None elif data is None: if default is not None: @@ -674,9 +673,10 @@ def from_excel( pandas.read_excel : For available ``kwargs`` . pandas.DataFrame.rename : For full details of ``mapper`` argument. """ + sheet_name_: str | int | list[str | int] if sheet_name is None: try: - _sheet_name = cls._default_excel_sheet_name + sheet_name_ = cls._default_excel_sheet_name except AttributeError: msg = ( f"sheet_name is None, but {type(cls).__name__} class " @@ -684,9 +684,9 @@ def from_excel( ) raise ValueError(msg) from None else: - _sheet_name = sheet_name + sheet_name_ = sheet_name - df = read_excel_worksheet(excel_file, sheet_name=_sheet_name, **kwargs) + df = read_excel_worksheet(excel_file, sheet_name=sheet_name_, **kwargs) return cls.from_dataframe( df, use_index=False, @@ -780,7 +780,7 @@ def __copy__(self) -> Self: def copy(self) -> Self: """Return a deep copy of object.""" # ruff: ignore[docstring-missing-returns] - return self.__copy__() + return deepcopy(self) def to_dict(self) -> dict: """Return parameters as a dict.""" # ruff: ignore[docstring-missing-returns] @@ -791,7 +791,7 @@ def to_series( series_name: str | None = None, index_name: str | None = None, index_prefix: str | None = None, - ) -> _pd.Series: + ) -> pd.Series: """Return parameters as a Series with parameter names as the index. Parameters @@ -809,9 +809,9 @@ def to_series( Series """ - ds = _pd.Series(data=self.to_dict(), name=series_name).rename_axis(index_name) + ds = pd.Series(data=self.to_dict(), name=series_name).rename_axis(index_name) if index_prefix: - ds.index = ds.index = _pd.MultiIndex.from_arrays( + ds.index = ds.index = pd.MultiIndex.from_arrays( arrays=([index_prefix] * ds.shape[0], ds.index), ) if index_name is not None: @@ -822,7 +822,7 @@ def to_series( @classmethod def from_series( cls, - ds: _pd.Series, + ds: pd.Series, skip_missing: bool = False, skip_unknown_parameters: bool = False, ) -> Self: @@ -830,7 +830,7 @@ def from_series( Parameters ---------- - ds : _pd.Series + ds : pd.Series The input Series is parsed in a dict-like manner with indicies as parameter names and series data as values. skip_missing: bool, default False: @@ -849,20 +849,23 @@ def from_series( """ _ds = ds.copy() if _ds.index.nlevels > 1: - raise ValueError("MultiIndex series not supported.") + msg = "MultiIndex series not supported." + raise ValueError(msg) args: dict[str, Any] = { str(k): v for k, v in ds.items() if k in cls.__dataclass_fields__ } missing_args = [k for k in cls.__dataclass_fields__ if k not in args] if not skip_missing and missing_args: + msg = f"skip_missing=False: missing required parameters: {missing_args}" raise TypeError( - f"skip_missing=False: missing required parameters: {missing_args}" + msg ) extra_args = [k for k in _ds.index if k not in cls.__dataclass_fields__] if extra_args and not skip_unknown_parameters: + msg = f"series contains unknown parameters: {_ds.index[extra_args].to_list()}" raise TypeError( - f"series contains unknown parameters: {_ds.index[extra_args].to_list()}" + msg ) return cls(**args) @@ -923,10 +926,13 @@ def to_excel( if sheet_name is None: sheet_name = getattr(self, "_default_excel_sheet_name", None) if sheet_name is None: - raise ValueError( + msg = ( "sheet_name is None and object has no " "_default_excel_sheet_name attribute." ) + raise ValueError( + msg + ) write_excel_worksheet( prepare_writable_df(params_ds.to_frame(), normalize_column_names=True), @@ -967,27 +973,31 @@ def summary( def _concat_gsolvetable_dataframes_with_fill( - df1: _pd.DataFrame, - df2: _pd.DataFrame, + df1: pd.DataFrame, + df2: pd.DataFrame, fill_str: str | None = "", fill_bool: bool | None = None, known_fields: dict[str, Any] | None = None, **kwargs, -) -> _pd.DataFrame: +) -> pd.DataFrame: if kwargs.get("axis", 0) != 0: - raise ValueError( + msg = ( f"incompatible kwarg axis={kwargs['axis']}, " "function operates in vstack (axis=0) mode only." ) + raise ValueError( + msg + ) kwargs["axis"] = 0 use_known_fields = False if known_fields is not None: use_known_fields = True if not all([hasattr(f, "default") for f in known_fields.values()]): + msg = f"if specified, known_fields must be a dict of DataFieldSpecification objects" raise TypeError( - f"if specified, known_fields must be a dict of DataFieldSpecification objects" + msg ) do_str_fill = fill_str is not None @@ -998,7 +1008,7 @@ def _concat_gsolvetable_dataframes_with_fill( if do_bool_fill: fill_bool = bool(fill_bool) - combined_df = _pd.concat([df1, df2], **kwargs) + combined_df = pd.concat([df1, df2], **kwargs) if not use_known_fields and not do_str_fill and not do_bool_fill: return combined_df diff --git a/src/gsolve/core/excel_io.py b/src/gsolve/core/excel_io.py index b9b17a2..10e924e 100644 --- a/src/gsolve/core/excel_io.py +++ b/src/gsolve/core/excel_io.py @@ -81,8 +81,9 @@ def get_true_sheet_name( return sheet_names[sheet_name] except IndexError as err: if raise_error: + msg = f"excel file {excel_file} has no sheet at index: {sheet_name}" raise ValueError( - f"excel file {excel_file} has no sheet at index: {sheet_name}" + msg ) return None else: @@ -92,8 +93,9 @@ def get_true_sheet_name( if sheet_name.lower() in sheet_names_lc: return sheet_names[sheet_names_lc.index(sheet_name.lower())] if raise_error: + msg = f"excel file {excel_file} has no sheet named '{sheet_name}'" raise ValueError( - f"excel file {excel_file} has no sheet named '{sheet_name}'" + msg ) return None @@ -109,8 +111,9 @@ def _parse_sheet_name_arg( sheet_name_list = [sheet_name] # pyrefly:ignore[bad-assignment] if not all(isinstance(s, (str, int)) for s in sheet_name_list): - msg = ( - "sheet_name args must be either a str (sheet name) or an int (sheet index)" + msg = "sheet_name args must be either a str (sheet name) or an int (sheet index)" + raise TypeError( + msg ) raise TypeError(msg) @@ -216,19 +219,23 @@ def write_excel_worksheet( pandas.ExcelWriter """ - if not is_in_literal(if_workbook_exists, IfWorkbookExists): + if if_workbook_exists not in get_args(IfWorkbookExists): msg = ( f"invalid value for {if_workbook_exists=}, must be one of " f"{get_args(IfWorkbookExists)}" ) - raise ValueError(msg) + raise ValueError( + msg + ) - if not is_in_literal(if_sheet_exists, IfSheetExists): + if if_sheet_exists not in get_args(IfSheetExists): msg = ( f"invalid value for {if_sheet_exists=}, must be one of " f"{get_args(IfSheetExists)}" ) - raise ValueError(msg) + raise ValueError( + msg + ) writer_kwargs: dict[str, Any] = { # "engine": "openpyxl", # "xlsxwriter", "openpyxl", "xlwt" @@ -240,8 +247,9 @@ def write_excel_worksheet( excel_file = Path(excel_file) if excel_file.exists(): if if_workbook_exists == "error": + msg = f"file {excel_file} already exists, and arg {if_workbook_exists=}" raise ValueError( - f"file {excel_file} already exists, and arg {if_workbook_exists=}" + msg ) if if_workbook_exists == "append": writer_kwargs["mode"] = "a" @@ -253,7 +261,8 @@ def write_excel_worksheet( with pd.ExcelWriter(excel_file, **writer_kwargs) as writer: df.to_excel(writer, sheet_name=sheet_name, **kwargs) except PermissionError: - msg = ( - f"Cannot write to {excel_file}, it is probably open in another application" + msg = f"Cannot write to {excel_file}, it is probably open in another application" + raise PermissionError( + msg ) raise PermissionError(msg) from None diff --git a/src/gsolve/core/utils.py b/src/gsolve/core/utils.py index efbf42d..0a395f6 100644 --- a/src/gsolve/core/utils.py +++ b/src/gsolve/core/utils.py @@ -108,7 +108,8 @@ def is_in_literal(value: Any, literal_type: TypeAliasType) -> bool: # ruff: ign if get_origin(literal_type.__value__) is Literal: return value in get_args(literal_type.__value__) - raise TypeError(f"{literal_type} is not a Literal type") + msg = f"{literal_type} is not a Literal type" + raise TypeError(msg) def is_datetime_array(v: Any) -> bool: # ruff: ignore[any-type] @@ -160,7 +161,9 @@ def to_points3d( """ if not is_points3d_like(v): msg = "object is not Points3D-like so cannot be converted to a true Points3D" - raise TypeError(msg) + raise TypeError( + msg + ) x, y, z = v x = to_1d_ndarray(v[0]).astype(np.float64) y = to_1d_ndarray(v[1], expected_size=x.size).astype(np.float64) @@ -231,8 +234,9 @@ def _nat_check[T](v: T) -> T: if not allow_nat: if isinstance(v, (pd.DatetimeIndex, pd.Series)): if any(v.isna()): - msg_0 = ( - "input contains values that resolve to NaT and allow_nat=False" + msg_0 = "input contains values that resolve to NaT and allow_nat=False" + raise ValueError( + msg_0 ) raise ValueError(msg_0) elif v is pd.NaT: @@ -267,10 +271,13 @@ def _nat_check[T](v: T) -> T: try: idx = pd.to_datetime(t, **kwargs) except Exception: - raise ValueError( + msg = ( f"unable to convert input '{t}' of type {type(t).__name__} " "to Timestamp or DateTimeIndex" ) + raise ValueError( + msg + ) rval = _nat_check( idx if idx.tz is None else idx.tz_convert("UTC").tz_localize(None) @@ -284,9 +291,13 @@ def to_1d_ndarray( a: ArrayLike, expected_size: int | None = None, extend_len_1_array: bool = False, - dtype: DTypeLike | None = None, -) -> np.ndarray[tuple[int], np.dtype[Any]]: - """Convert input to a 1D numpy array. +) -> NDArray: + _a = np.atleast_1d(a) + if _a.ndim > 1: + _a = np.squeeze(_a) + if _a.ndim != 1: + msg = f"input not convertible to 1d array" + raise ValueError(msg) Replicates the functionality of numpy.atleast_1d, but with additional checks for expected size and optional extension of length-1 arrays. @@ -321,10 +332,14 @@ def to_1d_ndarray( a = np.full(expected_size, a[0]) else: msg_0 = "expected_size must be specified if extend_len_1_array is True" - raise ValueError(msg_0) - if expected_size is not None and a.size != expected_size: - msg = f"expected array of size {expected_size}, got size = {a.size}" - raise ValueError(msg) + raise ValueError( + msg_0 + ) + if expected_size is not None and _a.size != expected_size: + msg = f"expected array of size {expected_size}, got size = {_a.size}" + raise ValueError( + msg + ) if dtype is not None: a = a.astype(dtype=dtype) @@ -343,15 +358,20 @@ def to_1d_ndarray_or_float(a: ArrayLike) -> NDArray[np.float64] | np.float64: def check_duplicate_index(idx: pd.Index | pd.DataFrame | pd.Series) -> None: """Raise a ValueError if the index contains duplicate values.""" if isinstance(idx, (pd.DataFrame, pd.Series)): - idx = idx.index - elif not isinstance(idx, pd.Index): + _idx = idx.index + elif isinstance(idx, pd.Index): + _idx = idx + else: msg = f"idx must be a pandas Index, DataFrame, or Series, not {type(idx).__name__}" - raise TypeError(msg) + raise TypeError( + msg + ) if _idx.duplicated().any(): idx_name = _idx.name or "index" idx_dupes = _idx[_idx.duplicated().tolist()] - raise ValueError(f"duplicate index values: {idx_dupes.unique().to_list()}") + msg = f"duplicate index values: {idx_dupes.unique().to_list()}" + raise ValueError(msg) @overload @@ -387,7 +407,8 @@ def normalize_field_names(df: pd.DataFrame | pd.Series) -> pd.DataFrame | pd.Ser df = df.rename_axis(index=normalize_str) df.name = normalize_str(str(df.name)) return df - raise TypeError(f"df must be a pandas DataFrame or Series, not {type(df).__name__}") + msg = f"df must be a pandas DataFrame or Series, not {type(df).__name__}" + raise TypeError(msg) @overload @@ -529,7 +550,9 @@ def columns_to_timestamp( ts_columns = ts_columns or DEFAULT_TIMESTAMP_COLUMNS if not is_list_like(ts_columns): msg = f"ts_columns must be list-like, not {type(ts_columns).__name__}" - raise TypeError(msg) + raise TypeError( + msg + ) n_ts_columns = len(ts_columns) if not 3 <= n_ts_columns <= len(DEFAULT_TIMESTAMP_COLUMNS): @@ -537,7 +560,9 @@ def columns_to_timestamp( f"length of ts_columns is {n_ts_columns}, " "must be between 3 (=ymd) and 8 (=ymdHMSun)" ) - raise ValueError(msg) + raise ValueError( + msg + ) matched_columns = [c for c in ts_columns if c in df.columns] n_matched = len(matched_columns) @@ -547,14 +572,18 @@ def columns_to_timestamp( f"found {n_matched} columns matching ts_columns, " f"must be >= 3 (ymd) and <= {n_ts_columns} the length of ts_columns" ) - raise ValueError(msg) + raise ValueError( + msg + ) if matched_columns != list(ts_columns[:n_matched]): msg = ( f"expected columns {ts_columns[:n_matched]} " f"!= matched columns {matched_columns}" ) - raise ValueError(msg) - rename_map = dict(zip(matched_columns, DEFAULT_TIMESTAMP_COLUMNS, strict=False)) + raise ValueError( + msg + ) + rename_map = dict(zip(matched_columns, DEFAULT_TIMESTAMP_COLUMNS)) return pd.to_datetime( df.loc[:, matched_columns].rename(columns=rename_map), **kwargs ) @@ -608,7 +637,9 @@ def timestamp_to_columns( if resolution is not None: if resolution not in AVAIL_TRUNCATION_COLUMNS: msg = f"resolution '{resolution}' is not in {AVAIL_TRUNCATION_COLUMNS}" - raise ValueError(msg) + raise ValueError( + msg + ) res = _TIMESTAMP_COLUMNS_TO_RESOLUTION[resolution] @@ -619,7 +650,7 @@ def timestamp_to_columns( elif round_method == "ceil": ds = ds.dt.ceil(res) else: - msg_0 = f"unrecognized rounding method '{round_method}'" + msg_0 = "unreconised rounding method '{round_method}'" raise ValueError(msg_0) df = pd.DataFrame( @@ -731,27 +762,30 @@ def expand_datetime_column( cols_to_split = list(column_name) else: msg = f"column_name must be a string or list-like, not {type(column_name).__name__}" - raise TypeError(msg) + raise TypeError( + msg + ) cols_to_split = [str(c) for c in cols_to_split if str(c) in candidate_columns] if not cols_to_split: msg = ( - "the specified column_name(s) are either missing or or are " + f"the specified column_name(s) are either missing or or are " "not datetime-like columns" ) - raise ValueError(msg) - - if prefix is None or not prefix: - prefixes = [f"{n}_" for n in cols_to_split] if len(cols_to_split) > 1 else [""] + raise ValueError( + msg + ) else: prefixes = list(prefix) if is_list_like(prefix) else [prefix] if len(prefixes) != len(cols_to_split): msg = ( - f"inconsistent 'column_name' and 'prefix' arg lengths: " + f"inconsisitent 'column_name' and 'prefix' arg lengths: " f"{len(cols_to_split)} != {len(prefixes)}" ) - raise ValueError(msg) + raise ValueError( + msg + ) for col, pre in zip(cols_to_split, prefixes, strict=True): ts_df = timestamp_to_columns( @@ -760,8 +794,9 @@ def expand_datetime_column( existing = ts_df.columns.intersection(df.columns).to_list() if existing: if not overwrite: - msg = ( - f"overwrite=False and splitting would overwrite columns: {existing}" + msg = f"overwrite=False and splitting would overwrite columns: {existing}" + raise ValueError( + msg ) df = df.drop(columns=existing) @@ -771,7 +806,9 @@ def expand_datetime_column( i_dt = df.columns.get_loc(col) if not isinstance(i_dt, int): msg_0 = "unexpected error: could not locate resolution column in output dataframe" - raise TypeError(msg_0) + raise TypeError( + msg_0 + ) i_dt += 1 df = pd.concat([df.iloc[:, :i_dt], ts_df, df.iloc[:, i_dt:]], axis=1) @@ -929,7 +966,9 @@ def generate_loop_intervals( db = to_naive_utc_datetime(datetime_bounds, allow_nat=False) if not isinstance(db, (pd.Series, pd.DatetimeIndex)) or len(db) < 2: msg = "datetimes must be an array-like object with at least two elements." - raise ValueError(msg) + raise ValueError( + msg + ) db = pd.Series(db) if not db.is_monotonic_increasing: @@ -967,12 +1006,15 @@ def identify_loop_blocks( dt = to_naive_utc_datetime(datetimes, allow_nat=False) if not isinstance(dt, (pd.Series, pd.DatetimeIndex)) or len(dt) < 2: msg = "datetimes must be an array-like object with at least two elements." - raise ValueError(msg) + raise ValueError( + msg + ) dt = pd.Series(dt) # ensure we have a Series for diff() and indexing if not dt.is_monotonic_increasing: msg = "datetimes must be sorted in increasing order." raise ValueError(msg) - + # if isinstance(_datetimes, pd.DatetimeIndex): + # _datetimes = _datetimes.to_series() gap = pd.to_timedelta(gap) gaps = dt.diff().gt(gap) @@ -1024,7 +1066,9 @@ def loops_from_gaps( dt = to_naive_utc_datetime(datetimes, allow_nat=False) if not isinstance(dt, (pd.Series, pd.DatetimeIndex)) or len(dt) < 2: msg = "datetimes must be an array-like object with at least two elements." - raise ValueError(msg) + raise ValueError( + msg + ) loop_intervals = identify_loop_blocks(dt, gap, as_intervals=True) loop_ids = generate_loop_names( len(loop_intervals), start=loop_start, step=loop_step, format_str=loop_format diff --git a/src/gsolve/core/xr_accessor.py b/src/gsolve/core/xr_accessor.py index 410e997..42b4970 100644 --- a/src/gsolve/core/xr_accessor.py +++ b/src/gsolve/core/xr_accessor.py @@ -151,7 +151,9 @@ def clip_to_points( "GravitySites object missing required point columns: " f"{self.xdim}, {self.ydim}" ) - raise TypeError(msg) + raise TypeError( + msg + ) x = points.data[self.xdim].to_numpy() y = points.data[self.ydim].to_numpy() @@ -435,9 +437,11 @@ def generate_distance_mask( np.ndarray A boolean array of same dimensions as the calling DataArray. """ - if mask_type not in {"radial", "rectangular"}: + if mask_type not in ("radial", "rectangular"): msg = f"mask_type must be 'radial' or 'rectangular', not '{mask_type}'" - raise ValueError(msg) + raise ValueError( + msg + ) if max_dist is not None and max_dist <= min_dist: msg = f"invalid {max_dist=}, must be > {min_dist=}" raise ValueError(msg) diff --git a/src/gsolve/core/xr_methods.py b/src/gsolve/core/xr_methods.py index 5a0e930..8e96ad6 100644 --- a/src/gsolve/core/xr_methods.py +++ b/src/gsolve/core/xr_methods.py @@ -139,17 +139,23 @@ def prepare_dem( "DataArray object. Use 'var_name' to specify the variable to " f"convert. Variables in dem: {list(dem.data_vars)}" ) - raise ValueError(msg) - input_var_name = str(next(iter(dem.data_vars))) + raise ValueError( + msg + ) + input_var_name = str(list(dem.data_vars.keys())[0]) dem = dem[input_var_name] if not isinstance(dem, xr.DataArray): msg = f"dem must be an xarray Dataset or DataArray, not {type(dem).__name__}" - raise TypeError(msg) + raise TypeError( + msg + ) dem = dem.squeeze() if dem.ndim != 2: msg = f"Dem must be a 2D array. Object is {dem.ndim}D with shape {dem.shape}" - raise ValueError(msg) + raise ValueError( + msg + ) # Drop singleton coordinate variables that are not dimensions (e.g. 'band', 'spatial_ref') # These can cause xarray/rioxarray broadcasting/indexing issues during operations @@ -166,7 +172,9 @@ def prepare_dem( f"prepare_dem(): dropping unused coordinate '{coord_name}' " f"failed with error: {e}" ) - raise RuntimeError(msg) from e + raise RuntimeError( + msg + ) from e # set dimension names if y_dim and dem.tcorr.ydim != y_dim: diff --git a/src/gsolve/gsolve_algorithms.py b/src/gsolve/gsolve_algorithms.py index 7fc7867..3f23bee 100644 --- a/src/gsolve/gsolve_algorithms.py +++ b/src/gsolve/gsolve_algorithms.py @@ -77,7 +77,8 @@ def call_gsolve_lstsq( # index in obs where ties are located m_ties = ref_sites.index.intersection(obs["site_id"].to_list()) if m_ties.empty: - raise ValueError("no tie sites") + msg = "no tie sites" + raise ValueError(msg) ref_sites = ref_sites.loc[m_ties] # set up g_solver_lstsq input arguments - do this segmented so that can report @@ -163,7 +164,8 @@ def call_gsolve_calibration( # index in obs where ties are located m_ties = ref_sites.index.intersection(obs["site_id"].to_list()) if m_ties.empty: - raise ValueError("no tie sites") + msg = "no tie sites" + raise ValueError(msg) ref_sites = ref_sites.loc[m_ties] # set up g_solver_lstsq input arguments @@ -292,7 +294,9 @@ def g_solver_lstsq( # ruff: ignore[too-many-positional-arguments] f"invalid percentile value {percentile_clipping}, " "must be between 0 and 100 inclusive" ) - raise ValueError(msg) + raise ValueError( + msg + ) n_obs = np.size(obs_g) n_ties = np.size(ties_site_id) @@ -345,7 +349,9 @@ def g_solver_lstsq( # ruff: ignore[too-many-positional-arguments] "obs_g_not_detided must be provided when " "calculate_calibration_factor is True" ) - raise ValueError(msg_0) + raise ValueError( + msg_0 + ) A[i, n_sites + (2 * n_loops)] = float(obs_g_not_detided[i]) # Ties to absolute sites diff --git a/src/gsolve/gsolve_outputs.py b/src/gsolve/gsolve_outputs.py index 4b9ad22..ee8098e 100644 --- a/src/gsolve/gsolve_outputs.py +++ b/src/gsolve/gsolve_outputs.py @@ -173,7 +173,9 @@ def set_solutions(self, results: GSolveSolverReturn) -> None: "calibration factor was not calculated but " "calculate_calibration_factor is True." ) - raise ValueError(msg) + raise ValueError( + msg + ) # store the calculated calibration factor in the params object self.params.calculated_calibration_factor = calibration_factor @@ -266,7 +268,7 @@ def plot_residual_cdf( unit_label = "mGal" precision = ".04f" else: - msg = f"unrecognized unit '{unit}'. Must be 'mGal' or 'uGal'" + msg = f"unrecgnised unit '{unit}'. Must be 'mGal' or 'uGal'" raise ValueError(msg) df_loops: list[str] = [str(l) for l in df["loop"].unique()] @@ -402,7 +404,7 @@ def plot_residual_drift( unit_label = "mGal" precision = ".04f" if unit_label is None: - msg = f"unrecognized unit '{unit}'. Must be 'mGal' or 'uGal'" + msg = f"unrecgnised unit '{unit}'. Must be 'mGal' or 'uGal'" raise ValueError(msg) x = df[x_col].to_numpy() diff --git a/src/gsolve/meter_conversion.py b/src/gsolve/meter_conversion.py index 34a58a5..5fdcb3e 100644 --- a/src/gsolve/meter_conversion.py +++ b/src/gsolve/meter_conversion.py @@ -113,28 +113,28 @@ def __init__( c_reading = np.atleast_1d(np.array(counter_reading, dtype=np.float64)) value_mgal = np.atleast_1d(np.array(value_mgal, dtype=np.float64)) - if c_reading.ndim != 1 or c_reading.size == 0: + if _c_reading.ndim != 1 or _c_reading.size == 0: msg = "counter_reading must be a non-empty 1-dimensional array." raise ValueError(msg) - if np.isnan(c_reading).any(): + if _np.isnan(_c_reading).any(): msg = "counter_reading contains NaN." raise ValueError(msg) - if value_mgal.ndim != 1 or value_mgal.size == 0: + if _value_mgal.ndim != 1 or _value_mgal.size == 0: msg = "value_mgal must be a non-empty 1-dimensional array." raise ValueError(msg) - if np.isnan(value_mgal).any(): + if _np.isnan(_value_mgal).any(): msg = "value_mgal contains NaN." raise ValueError(msg) - if c_reading.size != value_mgal.size: + if _c_reading.size != _value_mgal.size: msg = "counter_reading and value_mgal arrays must be the same shape." raise ValueError(msg) nrows: int = c_reading.size if interval_factor is not None: - interval_factor = np.atleast_1d(interval_factor).astype(float) - if interval_factor.ndim != 1 or interval_factor.size == 0: + _interval_factor = _np.atleast_1d(interval_factor).astype(float) + if _interval_factor.ndim != 1 or _interval_factor.size == 0: msg = "if specified, interval_factor must be a non-empty 1-dimensional array." raise ValueError(msg) @@ -144,11 +144,11 @@ def __init__( interval_factor = np.append(interval_factor, np.nan) else: msg = ( - f"invalid interval_factor: array size {interval_factor.size} is not " + f"invalid interval_factor: array size {_interval_factor.size} is not " f"the same as or 1 less than counter_reading ({nrows})." ) raise ValueError(msg) - if np.isnan(interval_factor[:-1]).any(): + if _np.isnan(_interval_factor[:-1]).any(): msg_0 = "interval_factor is specified, but contains NaN values." raise ValueError(msg_0) recalc_value_mgal = True @@ -237,9 +237,9 @@ def set_datetime_range( st = to_naive_utc_datetime(starttime, allow_nat=False) except ValueError as e: msg = f"Error setting starttime: {e}" - raise ValueError(msg) from None + raise ValueError(msg) else: - msg = f"invalid starttime type {type(starttime)}. Should be datetime-like or None." + msg = f"invalid starttime type {type(starttime)}. Should be datetimelike or None." raise TypeError(msg) if endtime is pd.NaT or endtime is None: @@ -249,9 +249,11 @@ def set_datetime_range( et = to_naive_utc_datetime(endtime, allow_nat=False) except ValueError as e: msg = f"Error setting endtime: {e}" - raise ValueError(msg) from None + raise ValueError(msg) else: - msg = f"invalid endtime type {type(endtime)}. Should be datetime-like or None." + msg = ( + f"invalid endtime type {type(endtime)}. Should be datetimelike or None." + ) raise TypeError(msg) if st is not None and et is not None and st >= et: @@ -265,7 +267,7 @@ def set_datetime_range( def starttime(self) -> _pd.Timestamp | None: """The date from which correction parameters are valid.""" st = getattr(self, "_starttime", None) - if st is not None and not isinstance(st, pd.Timestamp): + if st is not None and not isinstance(st, _pd.Timestamp): msg = f"invalid starttime type {type(st)}. Should be pandas.Timestamp or None." raise TypeError(msg) return st @@ -274,7 +276,7 @@ def starttime(self) -> _pd.Timestamp | None: def endtime(self) -> _pd.Timestamp | None: """The date up to which correction parameters are valid.""" r = getattr(self, "_endtime", None) - if r is not None and not isinstance(r, pd.Timestamp): + if r is not None and not isinstance(r, _pd.Timestamp): msg = f"invalid endtime type {type(r)}. Should be pandas.Timestamp or None." raise TypeError(msg) return r @@ -329,16 +331,16 @@ def convert_readings( if meter_id is not None: m_meter_id = np.atleast_1d(meter_id).astype(str) == self.meter_id - if m_meter_id.size == 0: + if _m_meter_id.size == 0: msg_0 = "invalid meter_id arg: empty array." raise ValueError(msg_0) - if m_meter_id.ndim != 1: + if _m_meter_id.ndim != 1: msg_0 = "invalid meter_id arg: must be a scalar or 1-dimensional array." raise ValueError(msg_0) - if m_meter_id.size == 1 and readings.size > 1: - m_meter_id = np.full(readings.shape, m_meter_id[0]) - elif m_meter_id.size != readings.size: + if _m_meter_id.size == 1 and _readings.size > 1: + _m_meter_id = _np.full(_readings.shape, _m_meter_id[0]) + elif _m_meter_id.size != _readings.size: msg_0 = "invalid meter_id arg: length must match readings array." raise ValueError(msg_0) else: @@ -356,10 +358,10 @@ def convert_readings( ) raise TypeError(msg_0) - if date_time.size != readings.size: + if _date_time.size != _readings.size: msg_0 = "invalid date_time array: date_time values must be the same length as readings." raise ValueError(msg_0) - if any(date_time.isna()): + if any(_date_time.isna()): msg_0 = "date_time contains NaT values." raise ValueError(msg_0) diff --git a/src/gsolve/observations.py b/src/gsolve/observations.py index 10ba49a..d76ad8d 100644 --- a/src/gsolve/observations.py +++ b/src/gsolve/observations.py @@ -237,43 +237,33 @@ class GravityObservations(GSolveTable): """ - _known_fields: MappingProxyType[str, DataFieldSpecification] = MappingProxyType( - { - "site_id": COMMON_FIELDS["site_id"], - "datetime": COMMON_FIELDS["datetime"], - "meter_id": DataFieldSpecification( - "meter_id", str, "", True, legacy_name="meter" - ), - "loop": COMMON_FIELDS["loop"], - "active": COMMON_FIELDS["active"], - "meter_reading": DataFieldSpecification( - "meter_reading", float, np.nan, False, legacy_name="reading" - ), - "meter_reading_mgal": DataFieldSpecification( - "meter_reading_mgal", float, np.nan, False - ), - "loop_tdelta": DataFieldSpecification("loop_tdelta", float, np.nan, False), - "survey_tdelta": DataFieldSpecification( - "survey_tdelta", float, np.nan, False - ), - "calibration_factor": DataFieldSpecification( - "calibration_factor", float, 1.0, False - ), - "earth_tide_corr": DataFieldSpecification( - "earth_tide_corr", float, 0.0, False - ), - "ocean_load_corr": DataFieldSpecification( - "ocean_load_corr", float, 0.0, False - ), - "custom_corr": DataFieldSpecification("custom_corr", float, 0.0, False), - "gravity_corr": DataFieldSpecification( - "gravity_corr", float, np.nan, False - ), - "meter_reading_converter_id": DataFieldSpecification( - "meter_reading_converter_id", str, "NA", False - ), - } - ) + _known_fields: dict[str, DataFieldSpecification] = { + "site_id": COMMON_FIELDS["site_id"], + "datetime": COMMON_FIELDS["datetime"], + "meter_id": DataFieldSpecification( + "meter_id", str, "", True, legacy_name="meter" + ), + "loop": COMMON_FIELDS["loop"], + "active": COMMON_FIELDS["active"], + "meter_reading": DataFieldSpecification( + "meter_reading", float, np.nan, False, legacy_name="reading" + ), + "meter_reading_mgal": DataFieldSpecification( + "meter_reading_mgal", float, np.nan, False + ), + "loop_tdelta": DataFieldSpecification("loop_tdelta", float, np.nan, False), + "survey_tdelta": DataFieldSpecification("survey_tdelta", float, np.nan, False), + "calibration_factor": DataFieldSpecification( + "calibration_factor", float, 1.0, False + ), + "earth_tide_corr": DataFieldSpecification("earth_tide_corr", float, 0.0, False), + "ocean_load_corr": DataFieldSpecification("ocean_load_corr", float, 0.0, False), + "custom_corr": DataFieldSpecification("custom_corr", float, 0.0, False), + "gravity_corr": DataFieldSpecification("gravity_corr", float, np.nan, False), + "meter_reading_converter_id": DataFieldSpecification( + "meter_reading_converter_id", str, "NA", False + ), + } _index_field: str = "obs_id" _default_excel_sheet_name: str | tuple[str, ...] = ("observations", "Survey Data") @@ -377,7 +367,7 @@ def _default_index_generator(self) -> pd.Index: site_id_tstamp_labels = ( self.data["site_id"].astype(str).str.cat(tstamps, sep=".") ) - new_idx = pd.Index(site_id_tstamp_labels, name=self._index_field, dtype=str) + new_idx = pd.Index(siteid_tstamp_labels, name=self._index_field, dtype=str) return self._index_deduplicator(new_idx) @staticmethod @@ -461,7 +451,7 @@ def set_obs_id( elif is_list_like(idx) and isinstance(idx, Iterable): # a sequence will converted to index - new_idx = pd.Index([str(i) for i in idx], name=self._index_field, dtype=str) + new_idx = pd.Index(_idx, name=self._index_field, dtype=str) else: msg = f"invalid idx arg of type '{type(idx).__name__}'" @@ -479,7 +469,8 @@ def set_obs_id( msg = f"{len(dupes)} duplicated obs_id's : {dupes.unique().to_list()}" if duplicated_obs_id == "error": - raise ValueError(f"{msg}") + msg_0 = f"{msg}" + raise ValueError(msg_0) if duplicated_obs_id == "keep": _warnings.warn(f"keeping {msg}") elif duplicated_obs_id == "rename": @@ -616,10 +607,10 @@ def set_fixed_time_datum( if t is None or pd.isna(t): self._fixed_time_datum = None else: - t = to_naive_utc_datetime(t) - if isinstance(t, pd.Timestamp): - self._fixed_time_datum = t - elif t is pd.NaT: + _t = to_naive_utc_datetime(t) + if isinstance(_t, pd.Timestamp): + self._fixed_time_datum = _t + elif _t is pd.NaT: self._fixed_time_datum = None else: msg = f"invalid fixed_time_datum of type '{type(t).__name__}'" @@ -776,7 +767,7 @@ def apply_ocean_load_correction( If ``corrector`` does not implement the ``OceanLoadCorrectionProvider`` protocol. """ if not isinstance(corrector, OceanLoadCorrectionProvider): - msg = "ocean_load_corrector must implement OceanLoadCorrectionProvider protocol" + msg = f"ocean_load_corrector must implement OceanLoadCorrectionProvider protocol" raise TypeError(msg) corrections = corrector.ocean_load_correction( @@ -819,9 +810,9 @@ def set_calibration_factor( raise ValueError(msg) if self.data["meter_id"].nunique() > 1 and meter_id is None: # ruff: ignore[pandas-nunique-constant-series-check] - raise ValueError( - "Multiple gravity meters found in data, must specify ``meter_id``" - ) + msg = "Multiple gravity meters found in data, must specify ``meter_id``" + raise ValueError(msg) + if meter_id is None: self.set_column(c_label, float(calibration_factor)) else: @@ -991,7 +982,8 @@ def _parse_inputs(o: str | Iterable[str] | None) -> list[str]: return [o] if isinstance(o, Iterable): return [str(oi) for oi in o] - raise TypeError(f"invalid input of type '{type(o).__name__}'") + msg = f"invalid input of type '{type(o).__name__}'" + raise TypeError(msg) # parse all args first to check for errors before modifying data obs_id = _parse_inputs(obs_id) @@ -1032,7 +1024,7 @@ def _get_writable_df( bool_to_int: bool = True, include_unknown_fields: bool | Sequence[str] = True, active_only: bool = False, - ) -> _pd.DataFrame: + ) -> pd.DataFrame: """Return a DataFrame suitable for writing to an excel or csv file.""" # ruff: ignore[docstring-missing-returns] cols = [c for c in self.known_fields() if c in self.data.columns] if include_unknown_fields: @@ -1063,7 +1055,7 @@ def _get_writable_df( bool_to_int=bool_to_int, ) - def write_to_csv( # ruff: ignore[undocumented-public-method] + def write_to_csv( self, fname: FilePath, *, @@ -1221,6 +1213,10 @@ def plot_observed_data( return fig, ax def _make_network(self, sites: GravitySites) -> pd.DataFrame: + df = self.data.assign( + group=self.data["site_id"].ne(self.data["site_id"].shift()).cumsum() + ) + station_order = ( self.data.assign( group=self.data["site_id"].ne(self.data["site_id"].shift()).cumsum() @@ -1232,9 +1228,9 @@ def _make_network(self, sites: GravitySites) -> pd.DataFrame: ) # merge with 'site' object to get location information - network_df = station_order.merge(sites.data, on="site_id", how="inner").loc[ - :, ["site_id", "loop", "latitude", "longitude"] - ] + network_df = pd.merge( + left=station_order, right=sites.data, on="site_id", how="inner" + ).loc[:, ["site_id", "loop", "latitude", "longitude"]] station_occupations = network_df.site_id.value_counts() return network_df, station_occupations @@ -1351,9 +1347,9 @@ def plot_site_visits(self, loop: str) -> None: ticks=list(site_id_to_int.values()), labels=list(site_id_to_int.keys()) ) - def loop_summary(self) -> _pd.DataFrame: + def loop_summary(self) -> pd.DataFrame: """Return a summary of the observations by loop.""" # ruff: ignore[docstring-missing-returns] - from gsolve.core._summary_functions import ( + from gsolve.core._summary_functions import ( # ruff: ignore[import-outside-top-level] duration_hr, endtime_utc, n_sites, @@ -1835,7 +1831,7 @@ def solve_calibration_factor( meter_ids = self.observations.data["meter_id"].unique() if len(meter_ids) > 1: msg = ( - "Calibration factor can only be calculated for a single instrument. " + "Calibration factor can only be calulated for a single instrument. " f"Observations include data from {len(meter_ids)} meter_id's = {meter_ids}" ) raise ValueError(msg) diff --git a/src/gsolve/reductions/anomalies.py b/src/gsolve/reductions/anomalies.py index e7ef52c..0251d91 100644 --- a/src/gsolve/reductions/anomalies.py +++ b/src/gsolve/reductions/anomalies.py @@ -249,7 +249,7 @@ def compute_free_air_anomaly( if any(_args_contain_nulls(absolute_gravity, normal_gravity, free_air_correction)): msg = "inputs contain nan's" raise ValueError(msg) - return np.atleast_1d( + return _np.atleast_1d( to_1d_ndarray_or_float(absolute_gravity) - ( to_1d_ndarray_or_float(normal_gravity) @@ -419,7 +419,9 @@ def __init__( abs_grav_df = absolute_gravity.to_frame(name="absolute_gravity") else: msg = f"invalid type for arg 'absolute_gravity': {type(absolute_gravity)}" - raise TypeError(msg) + raise TypeError( + msg + ) if abs_grav_df is None: msg_0 = "absolute_gravity has no site_solution data" raise ValueError(msg_0) @@ -458,12 +460,15 @@ def __init__( "invalid type for corrections_provider argument: " f"{type(corrections_parameters).__name__}" ) - raise TypeError(msg) + raise TypeError( + msg + ) # ensure we have entry in `sites` for all absolute gravity data sites if not abs_grav_df.index.isin(sites_df.index).all(): - msg_0 = ( - "absolute_gravity has sites with no corresponding site info in sites" + msg_0 = "absolute_gravity has sites with no corresponding site info in sites" + raise ValueError( + msg_0 ) raise ValueError(msg_0) @@ -478,8 +483,9 @@ def __init__( corrs = corr_provider.compute(sites=sites_df) else: if not self.data.index.isin(precomputed_corrections.data.index).all(): - msg_0 = ( - "precomputed corrections do not provide corrections for all sites" + msg_0 = "precomputed corrections do not provide corrections for all sites" + raise ValueError( + msg_0 ) raise ValueError(msg_0) precomputed_corrections.data = precomputed_corrections.data.loc[ @@ -497,7 +503,9 @@ def __init__( "required TerrainCorrectionData: " f"got {type(terrain_corrections).__name__}" ) - raise TypeError(msg) + raise TypeError( + msg + ) tc = terrain_corrections.get_corrections( self.data.index, if_missing="fill", fill_value=np.nan ) diff --git a/src/gsolve/reductions/corrections.py b/src/gsolve/reductions/corrections.py index 10fd6a8..292225d 100644 --- a/src/gsolve/reductions/corrections.py +++ b/src/gsolve/reductions/corrections.py @@ -180,7 +180,9 @@ def normal_gravity_at_ellipsoid( b = 6356774.5161 else: msg = f"Unknown ellipsoid '{ellipsoid}': must be one of {valid_ellipsoids}" - raise ValueError(msg) + raise ValueError( + msg + ) lat = np.deg2rad(latitude) normal_gravity = ( @@ -473,8 +475,9 @@ def bouguer_slab_curvature_corrected( if isinstance(er, boule.Ellipsoid): Ro = float(er.mean_radius) else: - msg = ( - f"Unknown ellipsoid '{ellipsoid_or_radius}': must be 'WGS84' or 'GRS80'" + msg = f"Unknown ellipsoid '{ellipsoid_or_radius}': must be 'WGS84' or 'GRS80'" + raise ValueError( + msg ) raise ValueError(msg) # ruff: ignore[type-check-without-type-error] @@ -710,7 +713,9 @@ def __init__( "params must be None or a GravityCorrectionParameters object, " f"not '{type(params)}'" ) - raise TypeError(msg) + raise TypeError( + msg + ) def __repr__(self) -> str: return f"{type(self).__name__}({self.params._param_str()})" @@ -782,7 +787,9 @@ def compute( "argument 'sites' must be a Dataframe or GravitySites object, not " f"'{type(sites)}'" ) - raise TypeError(msg) + raise TypeError( + msg + ) lon = sites_df[cols["longitude"]].to_numpy() lat = sites_df[cols["latitude"]].to_numpy() @@ -799,7 +806,7 @@ def compute( c for c in corrs if c not in self.available_corrections() ] if has_bad_corrections: - msg = f"Unrecognized corrections: {has_bad_corrections}" + msg = f"Unrecognised corrections: {has_bad_corrections}" raise ValueError(msg) else: corrs = self.params.bouguer_correction_fields() diff --git a/src/gsolve/reductions/terrain_corrections.py b/src/gsolve/reductions/terrain_corrections.py index 3ad3109..2194901 100644 --- a/src/gsolve/reductions/terrain_corrections.py +++ b/src/gsolve/reductions/terrain_corrections.py @@ -133,14 +133,18 @@ def calculate_terrain_correction( "density_dataset must be an xarray.DataArray not " f"'{type(density_dataset).__name__}'" ) - raise TypeError(msg) + raise TypeError( + msg + ) if not dem.tcorr.is_compatible(density_dataset): msg_0 = ( "Specified density_dataset is incompatible with dem. " "Check that the DataArrays have the same shape and coordinates." ) - raise ValueError(msg_0) + raise ValueError( + msg_0 + ) # format the points if len(points) != 3: @@ -151,9 +155,9 @@ def calculate_terrain_correction( pts_x = to_1d_ndarray(points[0]).astype(np.float64) pts_y = to_1d_ndarray(points[1], expected_size=pts_x.size).astype(np.float64) pts_z = to_1d_ndarray(points[2], expected_size=pts_x.size).astype(np.float64) - except Exception as e: # ruff: ignore[blind-except] + except Exception as e: msg = f"Points must contain 1d x,y,z arrays of equal size: {e}" - raise ValueError(msg) from None + raise ValueError(msg) # check the correction distances use_distance_mask = True @@ -162,7 +166,9 @@ def calculate_terrain_correction( raise ValueError(msg) if max_dist <= min_dist: msg = f"Incompatible distance args, {max_dist=} not greater than {min_dist=}" - raise ValueError(msg) + raise ValueError( + msg + ) # get land sea mask land_sea_mask: xr.DataArray = dem.tcorr.get_land_sea_mask(sea_level_elevation) @@ -503,20 +509,16 @@ class TerrainCorrectionParameters(GSolveParameters): def __post_init__(self) -> None: self._sanity_check(if_errors="warn") - def __setattr__(self, name: str, value: Any) -> None: - if name not in (n.name for n in dataclasses.fields(self)): - msg = f"unrecognized field name {name}" + def _normalize_fields(self) -> None: + if self.name is None: + msg = "'name' attribute must be a non-zero length string" raise ValueError(msg) - if name in { - "method", - "name", - "site_height_field", - "site_easting_field", - "site_northing_field", - "distance_mask_type", - }: - return super().__setattr__(name, str(value)) + name = str(self.name) + if not name: + msg = "'name' attribute must be a non-zero length string" + raise ValueError(msg) + object.__setattr__(self, "name", name) if name in {"compute_topography", "compute_bathymetry"}: return super().__setattr__(name, bool(value)) @@ -532,26 +534,16 @@ def __setattr__(self, name: str, value: Any) -> None: if name == "density_dataset_source" and value is None: return super().__setattr__(name, "") - msg = f"invalid type for {name} field: {type(value).__name__}" - raise TypeError(msg) - - return super().__setattr__(name, bool(value)) - - def _sanity_check(self, if_errors: Literal["warn", "error"] = "error") -> None: - """Check that all parameters are valid. - - Parameters - ---------- - if_errors : {"warn", "error"}, default "error" - How to handle any validation errors. If ``'error'``, then raise - an exception. If ``'warn'``, issue a warning and continue checking. - """ - throw_error: bool = if_errors == "error" - # error_count: int = 0 - - def warn_(m: str) -> None: - warnings.warn(m, category=UserWarning) - # error_count += 1 + if not value: + object.__setattr__(self, field_name, "") + continue + msg = ( + f"{field_name} attribute must be a DataArray, str, or " + f"Path-like object, not a {type(value).__name__}" + ) + raise TypeError( + msg + ) if ( np.isnan(self.min_dist) @@ -560,51 +552,33 @@ def warn_(m: str) -> None: or self.max_dist <= self.min_dist ): msg = ( - "invalid/incompatible 'min_dist' and 'max_dist'. Must be real values where" + "invalid 'min_dist' and 'max_dist' parameters. Must be real values where" "0.0 <= min_dist < max_dist: " f"got min_dist={self.min_dist}, max_dist={self.max_dist}" ) - if throw_error: - raise ValueError(msg) - warn_(msg) - + raise ValueError( + msg + ) # check distance msk type is valid if not is_in_literal(self.distance_mask_type, TCorrDistanceMaskType): msg = ( f"invalid 'distance_mask_type': {self.distance_mask_type}. " f"Expected one of: {get_args(TCorrDistanceMaskType.__value__)}" ) - if throw_error: - raise ValueError(msg) - warn_(msg) - - # check dem_source - if not _is_dataarray(self.dem_source) and not is_filepath_like(self.dem_source): - msg = ( - f"invalid dem_source: must be an xarray.DataArray or file path" - f", not {type(self.dem_source).__name__}" + raise ValueError( + msg ) - if throw_error: - raise ValueError(msg) - warn_(msg) if _is_dataarray(self.dem_source): if not self.dem_source.tcorr.is_valid_dem: - msg = "invalid dem_source DataArray: must be a 2D array of floats" - if throw_error: - raise ValueError(msg) - warn_(msg) - - elif is_filepath_like(self.dem_source): - if not self.dem_source: - msg = "invalid dem_source file-path like" - if throw_error: - raise ValueError(msg) - warn_(msg) - else: - msg = ( - f"invalid dem_source: must be an xarray.DataArray or file-path like" - f", not {type(self.dem_source).__name__}" + msg_0 = "invalid dem_source DataArray: must be a 2D array of floats" + raise ValueError( + msg_0 + ) + elif not self.dem_source: + msg_0 = "invalid dem_source: must be an xarray.DataArray or file path" + raise TypeError( + msg_0 ) if throw_error: raise ValueError(msg) @@ -613,23 +587,13 @@ def warn_(m: str) -> None: # check density_dataset_source if _is_dataarray(self.density_dataset_source): if not self.density_dataset_source.tcorr.is_valid_dem(): - msg = ( - "invalid density_dataset_source DataArray: " - "must be a 2D array of floats" + msg_0 = ( + "density_dataset_source is an xr.DataArray object but is not a valid DEM. " + "Check that it has the correct dimensions and coordinates." + ) + raise ValueError( + msg_0 ) - if throw_error: - raise ValueError(msg) - warn_(msg) - - elif is_filepath_like(self.density_dataset_source): - if not self.density_dataset_source: - msg = "" - elif self.density_dataset_source is not None: - msg = ( - "invalid density_dataset_source: expected file-path like, " - f"DataArray or None, not {type(self.density_dataset_source).__name__}" - ) - raise TypeError(msg) def to_series( self, @@ -678,11 +642,15 @@ def to_series( elif is_list_like(index_prefix): if len(index_prefix) != 2: msg = "if index_prefix is list-like, it must have length 2" - raise ValueError(msg) + raise ValueError( + msg + ) idx_val, idx_name = index_prefix else: msg = "index_prefix must be a string or list-like of length 2" - raise ValueError(msg) + raise ValueError( + msg + ) ds[idx_name] = idx_val ds = ds.set_index([idx_name, "parameter"])[series_name] @@ -751,8 +719,10 @@ def from_dataframe(cls, df: pd.DataFrame) -> dict[str, Self]: params.append(ds) else: msg = f"params dataframe must have 2 or 3 columns: found {df.shape[1]}" - raise ValueError(msg) - if not all(isinstance(ds, pd.Series) for ds in params): + raise ValueError( + msg + ) + if not all([isinstance(ds, pd.Series) for ds in params]): msg_0 = "error converting dataframe to series" raise ValueError(msg_0) params = [cls.from_series(p) for p in params] @@ -794,14 +764,18 @@ def __init__( elif isinstance(params, Iterable): if not all(isinstance(p, TerrainCorrectionParameters) for p in params): msg = "if params is a list-like, all items must be TerrainCorrectionParameters objects" - raise TypeError(msg) + raise TypeError( + msg + ) params = list(params) else: msg = ( "params arg must be a TerrainCorrectionParameters object or a list-like" f" of TerrainCorrectionParameters objects, not '{type(params)}'" ) - raise TypeError(msg) + raise TypeError( + msg + ) for p in params: self.add_zone(params=p) @@ -819,7 +793,9 @@ def add_zone(self, params: TerrainCorrectionParameters) -> None: "params must be a TerrainCorrectionParameters object, " f"not {type(params)}" ) - raise TypeError(msg) + raise TypeError( + msg + ) self.params[params.name] = params @@ -892,7 +868,9 @@ def compute( "points must be a sequence of arrays of form (x, y, z) " f"or a GravitySites object, not {type(points)}" ) - raise TypeError(msg) + raise TypeError( + msg + ) # Establish if we need to get points for each zone. # - If the points is a tuple, get 1 set of x,y,z now # - If the points is a GravitySites object, check if the site coordinate fields @@ -964,9 +942,11 @@ def compute( ycol=pars.site_northing_field, zcol=pars.site_height_field, ) - except Exception as e: # ruff: ignore[blind-except] + except Exception as e: msg = f"Error extracting site coordinates from GravitySites object: {e}" - raise ValueError(msg) from None + raise ValueError( + msg + ) # get the dem for this zone # maybe do not copy here @@ -980,7 +960,9 @@ def compute( f"DEM not specified or zone='{zone}': TerrainCorrectionParameter " "object must provide source file or an xarray.DataArray object." ) - raise ValueError(msg) + raise ValueError( + msg + ) # get the density model if defined if _is_dataarray(pars.density_dataset_source): @@ -1133,12 +1115,17 @@ def __init__( "params is specified but terrain_corrections is None: " "must specify both or neither" ) - if params is None and terrain_corrections is not None: raise ValueError( + msg + ) + if params is None and terrain_corrections is not None: + msg = ( "terrain_corrections is specified but params is None: " "must specify both or neither" ) - raise ValueError(msg) + raise ValueError( + msg + ) # initialise data frame with site_id's as index sids = to_1d_ndarray(site_id).astype(str) @@ -1168,47 +1155,59 @@ def __init__( if isinstance(params, TerrainCorrectionParameters): params_list = [params] elif isinstance(params, (list, tuple)): - params_list = list(params) - if not all(isinstance(p, TerrainCorrectionParameters) for p in params_list): + _params_list = list(params) + if not all( + isinstance(p, TerrainCorrectionParameters) for p in _params_list + ): msg_0 = ( "if params is a list or tuple, all items must be " "TerrainCorrectionParameters objects" ) - raise TypeError(msg_0) + raise TypeError( + msg_0 + ) else: msg = ( "params arg must be a TerrainCorrectionParameters object, None or a " "list or tuple of TerrainCorrectionParameters objects, " f"not '{type(params)}'" ) - raise TypeError(msg) + raise TypeError( + msg + ) if terrain_corrections is None: tc_list = [None] * len(params_list) elif isinstance(terrain_corrections, FloatArray): tc_list = [terrain_corrections] elif isinstance(terrain_corrections, (list, tuple)): - tc_list = list(terrain_corrections) - if not all(isinstance(tc, FloatArray) for tc in tc_list): + _tc_list = list(terrain_corrections) + if not all(isinstance(tc, FloatArray) for tc in _tc_list): msg_0 = ( "if terrain_corrections is a list or tuple, all items must be " "array-like (e.g. numpy arrays or pandas Series)" ) - raise TypeError(msg_0) + raise TypeError( + msg_0 + ) else: msg = ( "terrain_corrections arg must be an array-like, None or a list or tuple " f"of array-likes, not '{type(terrain_corrections)}'" ) - raise TypeError(msg) + raise TypeError( + msg + ) - if len(params_list) != len(tc_list): + if len(_params_list) != len(_tc_list): msg = ( "inconsistent params and terrain_corrections arg lengths: " f"{len(params_list)} params and {len(tc_list)} " "terrain_corrections specified" ) - raise ValueError(msg) + raise ValueError( + msg + ) for p, tc in zip(params_list, tc_list, strict=True): self.set_corrections(p, tc) @@ -1264,13 +1263,17 @@ def set_corrections( "params must be a TerrainCorrectionParameters object, " f"not {type(params)}" ) - raise TypeError(msg) + raise TypeError( + msg + ) if bathymetry_corrections is None and topography_corrections is None: msg_0 = ( "Must specify at least one of topography_corrections or " "bathymetry_corrections" ) - raise ValueError(msg_0) + raise ValueError( + msg_0 + ) tcorr_prefix = "tcorr" @@ -1299,7 +1302,9 @@ def set_corrections( ) except ValueError as e: msg = f"{corr_type} must be a 1d array of floats of the len as site_id: {e} " - raise ValueError(msg) from None + raise ValueError( + msg + ) self.set_column(label=col_name, data=c, dtype=float) # now set the total column @@ -1357,17 +1362,14 @@ def set_corrections( # insertion_point = self.data.columns.get_loc["easting"] + 1 # self.data.insert(insertion_point, output_height_field, np.nan) - # if isinstance(elevations, SitesLike): - # if not hasattr(elevations, "data"): - # msg = "elevations arg is a SitesLike object but does not have a " - # raise TypeError(msg) - - # elevations = elevations.loc[self.data] - # else: - # elevations = to_1d_ndarray( - # elevations, expected_size=self.data.shape[0], dtype=float - # ) - # self.data[output_height_field] = elevations + if isinstance(elevations, SitesLike): + if not hasattr(elevations, "data"): + msg = "elevations arg is a SitesLike object but does not have a " + raise TypeError( + msg + ) + # z = elevations.data[input_height_field].astype(float).to_numpy() + # df = elevations.data[] def __repr__(self) -> str: zones = ", ".join( @@ -1424,7 +1426,9 @@ def from_dataframe( "params must be a Dataframe, Series or TerrainCorrectionParameters " f"object, not {type(params)}" ) - raise TypeError(msg) + raise TypeError( + msg + ) consumed_columns = ["tcorr:total"] @@ -1451,7 +1455,9 @@ def from_dataframe( kk = f"{k}:topo" if kk not in df.columns: msg = f"terrain correction data missing for zone '{k}': '{kk}'" - raise KeyError(msg) + raise KeyError( + msg + ) tc_args["topography_corrections"] = df[kk].to_numpy() consumed_columns.append(kk) @@ -1459,7 +1465,9 @@ def from_dataframe( kk = f"{k}:bath" if kk not in df.columns: msg = f"terrain correction data missing for zone '{k}': '{kk}'" - raise KeyError(msg) + raise KeyError( + msg + ) tc_args["bathymetry_corrections"] = df[kk].to_numpy() consumed_columns.append(kk) @@ -1470,12 +1478,14 @@ def from_dataframe( for c in df.columns if c.startswith("tcorr:") and c not in consumed_columns ] - if unconsumed_columns: + if uncomsumed_columns: msg = ( "terrain corrections parameters and values are inconsistent: " f"no parameters for terrain_correction data {unconsumed_columns}" ) - raise ValueError(msg) + raise ValueError( + msg + ) return obj @@ -1724,9 +1734,11 @@ def get_corrections( otherwise a DataFrame is returned. """ - if if_missing not in {"drop", "raise", "fill"}: + if if_missing not in ["drop", "raise", "fill"]: msg = f"invalid if_missing arg '{if_missing}'" - raise ValueError(msg) # fixed typo in message + raise ValueError( + msg + ) # fixed typo in message site_id_idx = pd.Index(np.atleast_1d(site_id).astype(str).tolist()) tcorrs = self.data.reset_index().set_index("site_id") diff --git a/src/gsolve/reports.py b/src/gsolve/reports.py index 1d357a8..8298906 100644 --- a/src/gsolve/reports.py +++ b/src/gsolve/reports.py @@ -101,7 +101,9 @@ def __init__( if sites is None or results is None or observations is None: msg = "'observations', 'sites' and 'results' arguments must be provided." - raise ValueError(msg) + raise ValueError( + msg + ) if isinstance(observations, GravitySurvey): observations = observations.observations @@ -202,7 +204,7 @@ def _set_site_data( i_n_obs_used = df.columns.get_loc("n_obs_used") if not isinstance(i_n_obs_used, int): msg = "unexpected non-integer column index" - raise TypeError(msg) + raise ValueError(msg) df.insert(i_n_obs_used, "n_obs_input", n_obs_input) # add a csv string of loops each site is included in @@ -243,7 +245,7 @@ def _set_obs_data( i_included_in_solution = df.columns.get_loc("included_in_solution") if not isinstance(i_included_in_solution, int): msg = "unexpected non-integer column index" - raise TypeError(msg) + raise ValueError(msg) df.insert( loc=i_included_in_solution + 1, column="has_site_solution", @@ -270,7 +272,7 @@ def _set_loop_data( i_n_obs_input = df.columns.get_loc("n_obs_input") if not isinstance(i_n_obs_input, int): msg = "unexpected non-integer column index" - raise TypeError(msg) + raise ValueError(msg) df.insert(i_n_obs_input + 1, "n_obs_used", 0) df.loc[n_obs_used.index, "n_obs_used"] = n_obs_used @@ -345,7 +347,9 @@ def to_excel( if filename.exists(): if if_workbook_exists == "error": msg = f"file {filename} already exists, and arg {if_workbook_exists=}" - raise ValueError(msg) + raise ValueError( + msg + ) if if_workbook_exists == "append" and if_sheet_exists == "error": existing_worksheets = [ @@ -357,7 +361,9 @@ def to_excel( "Use 'if_workbook_exists' and 'if_sheet_exists' parameters " "to specify behaviour." ) - raise ValueError(msg) + raise ValueError( + msg + ) # observations write_excel_worksheet( diff --git a/src/gsolve/scintrex.py b/src/gsolve/scintrex.py index 1bb3615..9e02945 100644 --- a/src/gsolve/scintrex.py +++ b/src/gsolve/scintrex.py @@ -287,16 +287,27 @@ def _set_data(self, data: pd.DataFrame, on_error: str = "raise") -> None: # if this_dtype is float: try: df[c] = df[c].astype(this_dtype) - except Exception as err_read: - if on_error == "raise": + except Exception: + if on_error in ["warn", "ignore"]: + if on_error == "warn": + warnings.warn( + f"bad data encountered in column '{c}', setting to nan" + ) + try: + df[c] = ( + df[c] + .replace(to_replace=["--", "****", "******"], value=np.nan) + .astype(this_dtype) + ) + except Exception as err: + msg = f"unfixable error converting data in column '{c}' to {this_dtype}" + raise TypeError( + msg + ) from err + else: msg = f"error converting data in column '{c}' to {this_dtype}" - raise TypeError(msg) from err_read - - try: - df[c] = ( - df[c] - .replace(to_replace=["--", "****", "******"], value=np.nan) - .astype(this_dtype) + raise TypeError( + msg ) except Exception as err_cant_replace: msg = f"unfixable error converting data in column '{c}' to {this_dtype}" @@ -378,10 +389,6 @@ def from_file( msg = f"No data read from {cg6_file}" raise ValueError(msg) - if not file_data[0].startswith("/"): - msg = f"No header data found in {cg6_file}" - raise ValueError(msg) - idx_column_names = 0 for i, line in enumerate(file_data): if not line.startswith(r"/"): @@ -471,13 +478,16 @@ def set_loop( Name of the output column. """ if loop_format and "LOOP" not in loop_format: - raise ValueError("format_str must contain 'LOOP'.") + msg = "format_str must contain 'LOOP'." + raise ValueError(msg) # ensure only one method is used args = (field, array, datetimes, time_gap) if all(a is None for a in args): msg = "At least one of 'field', 'array', 'datetimes' or 'time_gap' must be set." - raise ValueError(msg) + raise ValueError( + msg + ) if sum(a is not None for a in args) > 1: msg = "Only one of 'field', 'array', or 'datetimes' can be set." raise ValueError(msg) @@ -485,7 +495,9 @@ def set_loop( if field is not None: if field not in self.data.columns: msg = f"arg {field=}, but not column name '{field}' found in obj.data." - raise KeyError(msg) + raise KeyError( + msg + ) self.data[output_column] = self.data[field].astype(str) return @@ -493,7 +505,9 @@ def set_loop( array = np.atleast_1d(array) if len(array) != len(self.data): msg_0 = "Length of 'array' must match the number of observations." - raise ValueError(msg_0) + raise ValueError( + msg_0 + ) self.data[output_column] = array.astype(str).tolist() return @@ -511,7 +525,9 @@ def set_loop( ) else: msg_0 = "datetimes must be a dictionary, Series or array-like object." - raise TypeError(msg_0) + raise TypeError( + msg_0 + ) if not dates.is_monotonic_increasing: msg_0 = "datetimes must be sorted in increasing order." @@ -521,7 +537,9 @@ def set_loop( f"First datetime in 'datetimes' ({dates[0]}) must be <= " f"earliest observation time ({self.data.datetime.min()})" ) - raise ValueError(msg) + raise ValueError( + msg + ) if dates[-1] < self.data["datetime"].max(): t_max = self.data["datetime"].max() + pd.Timedelta(seconds=1) dates = pd.DatetimeIndex([*dates.to_list(), t_max]) @@ -617,8 +635,9 @@ def to_gsolve_observations( include_non_standard_fields = [str(f) for f in include_non_standard_fields] missing = [f for f in include_non_standard_fields if f not in df.columns] if missing: + msg = f"Requested non-standard fields not found in data: {missing}" raise KeyError( - f"Requested non-standard fields not found in data: {missing}" + msg ) to_drop = set(df.columns) - set( @@ -670,9 +689,11 @@ def to_gsolve_sites( GravitySites """ - if coords_source not in {"user", "gps"}: + if coords_source not in ("user", "gps"): msg = f"coords_source must be 'user' or 'gps', not {coords_source}." - raise ValueError(msg) + raise ValueError( + msg + ) coord_cols = [f"{c}{coords_source}" for c in ("lat", "lon", "elev")] agg_method = "mean" if coords_source == "gps" else "first" @@ -732,9 +753,11 @@ def set_drift_correction( drift_zero_time = to_naive_utc_datetime(drift_zero_time) drift_rate = float(drift_rate) - if not isinstance(drift_zero_time, pd.Timestamp): + if not isinstance(_drift_zero_time, pd.Timestamp): msg = "drift_zero_time could not be converted to a valid Timestamp." - raise TypeError(msg) + raise ValueError( + msg + ) drift_corr = ( (self.data["datetime"] - drift_zero_time) diff --git a/src/gsolve/sites.py b/src/gsolve/sites.py index 8592911..63caa3c 100644 --- a/src/gsolve/sites.py +++ b/src/gsolve/sites.py @@ -300,7 +300,9 @@ def activate_ties(self, site_id: str | npt.ArrayLike | None = None) -> None: site_id = [str(s) for s in site_id] else: msg = "site_id must be None, a string, or an array-like of strings" - raise TypeError(msg) + raise TypeError( + msg + ) self._check_bad_site_ids(site_id) @@ -678,12 +680,16 @@ def merge( f"invalid type for other: " f"expected {type(self).__name__}, got {type(other)}" ) - raise TypeError(msg) + raise TypeError( + msg + ) valid_duplicates_args = {"drop", "error"} if if_duplicate not in valid_duplicates_args: msg = f"duplicates must be one of {valid_duplicates_args}, not '{if_duplicate}'" - raise ValueError(msg) + raise ValueError( + msg + ) other_df = other.data is_duplicate = other_df.index.isin(self.data.index) @@ -764,14 +770,22 @@ def __init__( "creating ReferenceGravity object: " f"site_id field contains duplicated values: {duplicates}" ) - raise ValueError(msg) + raise ValueError( + msg + ) # catch empty site_id - if idx.isna().any() or (idx == "").any(): # ruff: ignore[compare-to-empty-string] + if idx.isna().any() or (idx == "").any(): # type: ignore[unresolved-attribute, ty:unresolved-attribute] + m = idx.isna() | (idx == "") + empty = _pd.Series(m) + empty = empty.loc[m.tolist()].index.to_list() msg = ( - "creating ReferenceGravity object: site_id field contains empty values" + "creating ReferenceGravity object: " + f"site_id field contains empty values at rows: {empty}" + ) + raise ValueError( + msg ) - raise ValueError(msg) self.data = pd.DataFrame(index=idx, data=None) self.set_column("gravity", gravity) @@ -783,7 +797,9 @@ def __init__( "creating ReferenceGravity object: " f"gravity field contains null values for sites: {nodata}" ) - raise ValueError(msg) + raise ValueError( + msg + ) for k, v in kwargs.items(): self.set_column(k, v) @@ -996,12 +1012,16 @@ def merge( f"invalid type for other: " f"expected {type(self).__name__}, got {type(other)}" ) - raise TypeError(msg) + raise TypeError( + msg + ) valid_duplicates_args = {"drop", "error"} if if_duplicate not in valid_duplicates_args: msg = f"duplicates must be one of {valid_duplicates_args}, not '{if_duplicate}'" - raise ValueError(msg) + raise ValueError( + msg + ) other_df = other.data is_duplicate = other_df.index.isin(self.data.index) diff --git a/src/gsolve/tide/earth_tide.py b/src/gsolve/tide/earth_tide.py index 417379d..9e0c9dc 100644 --- a/src/gsolve/tide/earth_tide.py +++ b/src/gsolve/tide/earth_tide.py @@ -66,8 +66,7 @@ def tidal_correction( # ruff: ignore[undocumented-public-method] elev: FloatArray, date_time: DatetimeArray, site_id: SiteIDArray | None = None, - **kwargs: Any, - ) -> NDArray[np.float64]: ... + ) -> NDArray[_np.float64]: ... def identifier(self, **kwargs) -> str: ... # ruff: ignore[undocumented-public-method] @@ -96,7 +95,7 @@ def gravimetric_factor( .. [1] Agnew, D. C. (2007). 3.06 Earth Tides. In Treatise on Geophysics (pp. 163-195). Elsevier. https://doi.org/10.1016/B978-044452748-6.00056-0 """ - h2 = to_1d_ndarray(h2, dtype=float).astype(float) + h2 = to_1d_ndarray(h2).astype(float) k2 = to_1d_ndarray(k2, expected_size=h2.size).astype(float) gfactor = 1 + h2 - 1.5 * k2 return gfactor[0] if gfactor.size == 1 else gfactor @@ -229,7 +228,9 @@ def gravity_accelerations( elev = to_1d_ndarray(elev, expected_size=lon.size).astype(float) except ValueError: msg = "Invalid lat, lon, or elev: must be equal sized 1d arrays of floats" - raise ValueError(msg) from None + raise ValueError( + msg + ) T = _decimal_julian_century(dt) t0 = _decimal_hour_of_day(dt) @@ -464,20 +465,31 @@ def time_series( valid_methods = {"correction", "acceleration"} if method not in valid_methods: msg = f"method parameter must be one of {valid_methods}, not '{method}'." - raise ValueError(msg) + raise ValueError( + msg + ) try: - step = pd.Timedelta(step) + step = _pd.Timedelta(step) + if not isinstance(step, _pd.Timedelta): + msg_0 = "step must be a valid timedelta or timedelta string." + raise ValueError(msg_0) except ValueError as e: msg = f"error parsing step: {e}" raise ValueError(msg) from e - t0 = convert_single_timestamp_arg( - starttime, allow_nat=False, err_prefix="error parsing starttime" - ) - t1 = convert_single_timestamp_arg( - endtime, allow_nat=False, err_prefix="error parsing endtime" - ) + try: + t0 = to_naive_utc_datetime(starttime, allow_nat=False) + t1 = to_naive_utc_datetime(endtime, allow_nat=False) + if not isinstance(t0, _pd.Timestamp) or not isinstance(t1, _pd.Timestamp): + msg_0 = "not convertible to Timestamp." + raise ValueError(msg_0) + if t0 >= t1: + msg_0 = "starttime is after or equal to endtime." + raise ValueError(msg_0) + except ValueError as e: + msg = f"error parsing starttime and endtime: {e}" + raise ValueError(msg) from None if t0 >= t1: msg = "starttime is after or equal to endtime." @@ -550,17 +562,19 @@ def _decimal_julian_century( pandas.to_datetime : Convert argument to datetime. """ - dt_ = to_naive_utc_datetime(dt, allow_nat=False, **kwargs) - if dt_ is None or isinstance(dt_, NaTType): + _dt = to_naive_utc_datetime(dt, allow_nat=False, **kwargs) + if _dt is None or isinstance(_dt, NaTType): msg = "dt cannot be NaT or None." - raise TypeError(msg) - if isinstance(dt_, pd.Timestamp): - dt_ = pd.DatetimeIndex([dt_]) - elif isinstance(dt_, (pd.DatetimeIndex, pd.Series)): - dt_ = pd.DatetimeIndex(dt_) + raise ValueError(msg) + if isinstance(_dt, _pd.Timestamp): + _dt = _pd.DatetimeIndex([_dt]) + elif isinstance(_dt, (_pd.DatetimeIndex, _pd.Series)): + _dt = _pd.DatetimeIndex(_dt) else: msg = "dt could not be resolved to a datetime, DatetimeIndex, Series, or array-like of datetimes." - raise TypeError(msg) + raise ValueError( + msg + ) julian_century_origin = pd.Timestamp("1899-12-31T12:00:00", tz=None) td_seconds = (dt_ - julian_century_origin).total_seconds() @@ -593,13 +607,15 @@ def _decimal_hour_of_day( The decimal hour. This is a scalar if ``date_time`` is a single value. """ - dt = to_naive_utc_datetime(date_time, allow_nat=False) - if isinstance(dt, NaTType) or dt is None: + _dt = to_naive_utc_datetime(date_time, allow_nat=False) + if isinstance(_dt, NaTType) or _dt is None: msg = "date_time cannot be NaT or None." raise ValueError(msg) - if not isinstance(dt, (pd.Timestamp, pd.Series, pd.DatetimeIndex)): + if not isinstance(_dt, (_pd.Timestamp, _pd.Series, _pd.DatetimeIndex)): msg = "date_time could not be resolved to a datetime, DatetimeIndex, Series, or array-like of datetimes." - raise TypeError(msg) + raise ValueError( + msg + ) if isinstance(dt, pd.Timestamp): dt = pd.DatetimeIndex([dt]) @@ -668,13 +684,17 @@ def __init__( **kwargs, ) -> None: - freq_start = to_1d_ndarray(freq_start, dtype=float) - s = freq_start.size - freq_stop = to_1d_ndarray(freq_stop, expected_size=s, dtype=float) - amplitude = to_1d_ndarray(amplitude, expected_size=s, dtype=float) - phase_lead = to_1d_ndarray(phase_lead, expected_size=s, dtype=float) - if wave_group is not None: - wave_group = to_1d_ndarray(wave_group, expected_size=s, dtype=str) + try: + freq_start = to_1d_ndarray(freq_start).astype(float) + s = freq_start.size + freq_stop = to_1d_ndarray(freq_stop, expected_size=s).astype(float) + amplitude = to_1d_ndarray(amplitude, expected_size=s).astype(float) + phase_lead = to_1d_ndarray(phase_lead, expected_size=s).astype(float) + if wave_group is not None: + wave_group = to_1d_ndarray(wave_group, expected_size=s).astype(str) + except ValueError as e: + msg = f"Error parsing tidal parameters: {e}" + raise ValueError(msg) self.data = pd.DataFrame.from_dict( data={ @@ -864,7 +884,9 @@ def from_excel(cls, fname: FilePath, sheet_name: str | int = 0) -> Self: df = pd.read_excel(fname, sheet_name=sheet_name) if isinstance(df, dict): msg = "Excel file contains multiple sheets. Please specify sheet_name." - raise ValueError(msg) # ruff: ignore[type-check-without-type-error] + raise ValueError( + msg + ) return cls.from_dataframe(df) @@ -1131,23 +1153,20 @@ def time_series( duration_uncorr = int( np.ceil(pd.to_timedelta(duration).total_seconds() / 3600.0) ) - if duration_uncorr <= 0: - msg = f"invalid duration {duration}: must be a positive timedelta or number of hours." - raise ValueError(msg) - - # pygtide outputs corrections from start of UTC day only - # so we must manipulate supplied starttime and duration - # such that the generated tides cover the specified interval - orig_endtime = starttime_uncorr + pd.Timedelta(hours=duration_uncorr) - - duration_final = int( - np.ceil((orig_endtime - starttime).total_seconds() / 3600.0) - ) + if _duration_uncorr <= 0: + msg = "duration must be a positive timedelta or number of hours." + raise ValueError( + msg + ) + _orig_endtime = _starttime_uncorr + _pd.Timedelta(hours=_duration_uncorr) + _duration = int(_np.ceil((_orig_endtime - _starttime).total_seconds() / 3600.0)) sample_interval = int(sample_interval) if sample_interval <= 0: - msg = f"invalid sample_interval {sample_interval}: must be a positive integer number of seconds." - raise ValueError(msg) + msg = "sample_interval must be a positive integer number of seconds." + raise ValueError( + msg + ) with warnings.catch_warnings(): self._pgt.msg = False @@ -1166,10 +1185,10 @@ def time_series( **this_run_kwargs, ) - tides_df = self._pgt.results() - if not isinstance(tides_df, pd.DataFrame): + df = self._pgt.results() + if not isinstance(df, _pd.DataFrame): msg = "No results returned from pygtide prediction." - raise ValueError(msg) # ruff: ignore[type-check-without-type-error] + raise ValueError(msg) normalised_cols = ["datetime", "signal", "tide", "pole_tide", "lod_tide"] df = df.rename( @@ -1244,6 +1263,9 @@ def tidal_correction( "site_id is a required parameter for " "EternaPredictTidalCorrection tidal_correction method." ) + raise ValueError( + msg + ) if isinstance(site_id, str): site_id = [site_id] * lat.size site_id = to_1d_ndarray(site_id, expected_size=lat.size, dtype=str) @@ -1274,19 +1296,17 @@ def tidal_correction( unit=unit, ).mul(-1.0) - if not isinstance(ts.index, pd.DatetimeIndex): + if not isinstance(ts.index, _pd.DatetimeIndex): msg = "Unexpected time series index type from pygtide results." - raise ValueError(msg) # ruff: ignore[type-check-without-type-error] + raise ValueError( + msg + ) ts = ts.set_index(ts.index.round(freq="1s")) - if not np.isnan(corrs[site_mask]).all(): - msg = ( - f"Some values for site {site_id}: have already been set: " - "this should not happen" - ) + if not (corrs[site_mask] == 0.0).all(): + msg = "Unexpected non-zero values in corrs for site mask." raise ValueError(msg) - - corrs[site_mask] = np.interp( + corrs[site_mask] = _np.interp( x=date_time[site_mask], xp=ts.index, fp=ts["signal"].to_numpy(), diff --git a/src/gsolve/tide/ocean_load.py b/src/gsolve/tide/ocean_load.py index a05820f..e52f8fc 100644 --- a/src/gsolve/tide/ocean_load.py +++ b/src/gsolve/tide/ocean_load.py @@ -135,9 +135,9 @@ def __init__( **metadata, ) -> None: if isinstance(site_id, str): - site_id = np.array([site_id] * len(date_time)) - site_id = to_1d_ndarray(site_id, dtype=str) - if site_id.ndim != 1: + _site_id = np.array([site_id] * len(date_time)) + _site_id = np.atleast_1d(site_id).astype(str) + if _site_id.ndim != 1: msg = "site_id argument must be 1-dimensional." raise ValueError(msg) @@ -152,7 +152,7 @@ def __init__( ) self.metadata: dict[str, Any] = metadata - def identifier(self, **kwargs) -> str: + def identifier(self) -> str: """Corrector identifier string.""" # ruff: ignore[docstring-missing-returns] return f"{type(self).__name__}()" @@ -193,7 +193,7 @@ def ocean_load_correction( else: site_id = np.atleast_1d(site_id).astype(str) - if len(site_id) != len(dt): + if len(_site_id) != len(dt): msg = "site_id and datetime arguments must have the same length." raise ValueError(msg) @@ -296,7 +296,7 @@ def __repr__(self) -> str: md = ",".join([f"{v}={k}" for v, k in self.metadata.items()]) return f"{cname}({md})" - def identifier(self, **kwargs) -> str: + def identifier(self) -> str: """Corrector identifier string.""" # ruff: ignore[docstring-missing-returns] return f"{self.__class__.__name__}()" @@ -379,15 +379,16 @@ def _datetimes_to_np_datetime64( dt: DatetimeScalar | DatetimeArray, dtype: str = "datetime64" ) -> np.ndarray: """Convert datetimes to numpy datetime64 array.""" # ruff: ignore[docstring-missing-returns] - _dt = to_naive_utc_datetime(dt, allow_nat=False) - if isinstance(_dt, pd.Timestamp): - return np.array([_dt], dtype=dtype) - if isinstance(_dt, (pd.DatetimeIndex, pd.Series)): - return np.atleast_1d(_dt).astype(dtype) - raise TypeError( + dt = to_naive_utc_datetime(dt, allow_nat=False) + if isinstance(dt, pd.Timestamp): + return np.array([dt], dtype=dtype) + if isinstance(dt, (pd.DatetimeIndex, pd.Series)): + return np.atleast_1d(dt).astype(dtype) + msg = ( "datetimes must be a pandas Timestamp, DatetimeIndex, or Series, not " f"{type(dt).__name__}." ) + raise TypeError(msg) def _validate_timeseries_data(df: pd.DataFrame) -> None: @@ -406,14 +407,14 @@ def _validate_timeseries_data(df: pd.DataFrame) -> None: msg = f"data must be a pandas DataFrame, not {type(df).__name__}." raise TypeError(msg) if not isinstance(df.index, pd.DatetimeIndex): - msg = "timeseries not indexed by datetime." - raise TypeError(msg) + msg_0 = "timeseries not indexed by datetime." + raise TypeError(msg_0) if df.shape[0] < 2: - msg = "timeseries must contain at least two rows." - raise ValueError(msg) + msg_0 = "timeseries must contain at least two rows." + raise ValueError(msg_0) if not df.index.is_monotonic_increasing: - msg = "timeseries not sorted in increasing order." - raise ValueError(msg) + msg_0 = "timeseries not sorted in increasing order." + raise ValueError(msg_0) # warn if non-uniform sampling interval/rate sample_intervals = (df.index[1:] - df.index[:-1]).total_seconds() @@ -481,6 +482,9 @@ def qtp_to_corrector( corrections=df["BergerLoadCorrection"].to_numpy().astype(float), **metadata, ) + else: + msg = f"invalid corr_type '{corr_type}', must be one of {'auto', 'timeseries', 'site-datetime'}." + raise ValueError(msg) msg = f"invalid corr_type '{corr_type}', must be one of {'auto', 'timeseries', 'site-datetime'}." raise ValueError(msg) @@ -505,7 +509,7 @@ def read_qtp_timeseries(file_path: FilePath) -> pd.DataFrame: if not all(col in df.columns for col in expected_columns): msg = f"Format error reading '{file_path}': expected columns {expected_columns} not found, not QTP timeseries format?" raise ValueError(msg) - if bool(df.isna().any(axis=None)): + if df.isna().any(axis=None): msg = f"Missing values detected while reading '{file_path}': not QTP timeseries format?" raise ValueError(msg) @@ -574,7 +578,7 @@ def read_qtp_multistation(file_path: FilePath) -> pd.DataFrame: ) if df.shape[1] != len(column_definitions): - msg = f"Format error reading '{file_path}': not QTP multistation ocean load format?" + msg = f"Format error reading '{file_path}': not QTP multiistation ocean load format?" raise ValueError(msg) if df.isna().any(axis=None): @@ -633,9 +637,12 @@ def generate_qtp_input( # ruff: ignore[too-many-positional-arguments] else: elevation = np.atleast_1d(elevation).astype(float) - if not (site_id.size == datetimes.size == lat.size == lon.size == elevation.size): + if not ( + _site_id.size == _datetimes.size == _lat.size == _lon.size == _elevation.size + ): msg = "site_id, datetimes, latitude, longitude, and elevation arguments must all have the same shape." raise ValueError(msg) + raise ValueError(msg) # initial data frame with station IDs and datetimes qtp_df = pd.DataFrame( @@ -648,13 +655,7 @@ def generate_qtp_input( # ruff: ignore[too-many-positional-arguments] }, ) - # wrap to [-180, 180] - def _wrap_180(x: float) -> float: - return (x + 180.0) % 360 - 180.0 - - qtp_df["Longitude"] = qtp_df["Longitude"].apply(_wrap_180) - - if any(qtp_df["Elevation"].isna()): + if qtp_df["Elevation"].isna().any(): msg = "Some site elevations are missing and no 'fill_elevation' was specified." raise ValueError(msg) @@ -722,7 +723,7 @@ def _get_model_parameters(self, f: FilePath) -> None: "ocean_tide_model": "", "center_mass_correction": False, } - with pathlib.Path(f).open() as fh: + with pathlib.Path(f).open() as fh: # ruff: ignore[unspecified-encoding] model_txt = [l.strip() for l in fh if l.startswith("$$")] for l in model_txt: if l.startswith("$$ Greens function:"): @@ -731,7 +732,7 @@ def _get_model_parameters(self, f: FilePath) -> None: metadata["ocean_tide_model"] = l.split(":", 1)[1].strip() elif l.startswith("$$ CMC"): v = l.split(":", 1)[1].strip().split()[0] - metadata["center_mass_correction"] = v.upper() != "NO" + matadata["center_mass_correction"] = v != "NO" elif l.startswith("$$ END HEADER:"): break self.metadata.update(metadata) From c6d977071e5cb17b70647f1fdef403aebfe0b46f Mon Sep 17 00:00:00 2001 From: Adrian Benson Date: Wed, 16 Sep 2026 13:42:25 +1200 Subject: [PATCH 06/36] This is a mess 1 --- src/gsolve/tide/earth_tide.py | 169 ++++++++++++++++------------------ 1 file changed, 78 insertions(+), 91 deletions(-) diff --git a/src/gsolve/tide/earth_tide.py b/src/gsolve/tide/earth_tide.py index 9e0c9dc..4b3b3f9 100644 --- a/src/gsolve/tide/earth_tide.py +++ b/src/gsolve/tide/earth_tide.py @@ -66,13 +66,13 @@ def tidal_correction( # ruff: ignore[undocumented-public-method] elev: FloatArray, date_time: DatetimeArray, site_id: SiteIDArray | None = None, - ) -> NDArray[_np.float64]: ... + ) -> NDArray[np.float64]: ... def identifier(self, **kwargs) -> str: ... # ruff: ignore[undocumented-public-method] def gravimetric_factor( - k2: float | FloatArray = 0.2980, h2: float | FloatArray = 0.6032 + k2: float = 0.2980, h2: float = 0.6032 ) -> np.float64 | NDArray[np.float64]: """Compute gravimetric factor from Love numbers ``k2`` and ``h2``. @@ -228,9 +228,7 @@ def gravity_accelerations( elev = to_1d_ndarray(elev, expected_size=lon.size).astype(float) except ValueError: msg = "Invalid lat, lon, or elev: must be equal sized 1d arrays of floats" - raise ValueError( - msg - ) + raise ValueError(msg) from None T = _decimal_julian_century(dt) t0 = _decimal_hour_of_day(dt) @@ -394,8 +392,8 @@ def tidal_correction( lon: FloatArray, elev: FloatArray, date_time: DatetimeArray, - site_id: SiteIDArray | None = None, # ruff: ignore[unused-method-argument] - **kwargs, # ruff: ignore[unused-method-argument] + site_id: SiteIDArray | None = None, + **kwargs, ) -> NDArray[np.float64]: """Compute tidal corrections at specified locations and times. @@ -465,15 +463,13 @@ def time_series( valid_methods = {"correction", "acceleration"} if method not in valid_methods: msg = f"method parameter must be one of {valid_methods}, not '{method}'." - raise ValueError( - msg - ) + raise ValueError(msg) try: - step = _pd.Timedelta(step) - if not isinstance(step, _pd.Timedelta): - msg_0 = "step must be a valid timedelta or timedelta string." - raise ValueError(msg_0) + step = pd.Timedelta(step) + if not isinstance(step, pd.Timedelta): + msg = "step must be a valid timedelta or timedelta string." + raise ValueError(msg) except ValueError as e: msg = f"error parsing step: {e}" raise ValueError(msg) from e @@ -481,16 +477,16 @@ def time_series( try: t0 = to_naive_utc_datetime(starttime, allow_nat=False) t1 = to_naive_utc_datetime(endtime, allow_nat=False) - if not isinstance(t0, _pd.Timestamp) or not isinstance(t1, _pd.Timestamp): - msg_0 = "not convertible to Timestamp." - raise ValueError(msg_0) - if t0 >= t1: - msg_0 = "starttime is after or equal to endtime." - raise ValueError(msg_0) except ValueError as e: msg = f"error parsing starttime and endtime: {e}" raise ValueError(msg) from None + if not isinstance(t0, pd.Timestamp): + msg = "startime not convertible to pandas.Timestamp." + raise ValueError(msg) + if not isinstance(t1, pd.Timestamp): + msg = "endtime not convertible to pandas.Timestamp." + raise ValueError(msg) if t0 >= t1: msg = "starttime is after or equal to endtime." raise ValueError(msg) @@ -500,7 +496,7 @@ def time_series( lon_arr = np.full(len(t_idx), lon) elev_array = np.full(len(t_idx), elev) if method == "correction": - time_series = pd.Series( + tseries = pd.Series( data=self.tidal_correction( site_id=None, lat=lat_arr, @@ -513,7 +509,7 @@ def time_series( ) elif method == "acceleration": a, b = self.gravity_accelerations(lat_arr, lon_arr, elev_array, t_idx) - time_series = pd.Series( + tseries = pd.Series( data=a + b, index=t_idx, name=method, @@ -527,8 +523,6 @@ def time_series( @overload def _decimal_julian_century(dt: DatetimeScalar, **kwargs) -> np.float64: ... - - @overload def _decimal_julian_century(dt: DatetimeArray, **kwargs) -> NDArray[np.float64]: ... @@ -562,19 +556,17 @@ def _decimal_julian_century( pandas.to_datetime : Convert argument to datetime. """ - _dt = to_naive_utc_datetime(dt, allow_nat=False, **kwargs) - if _dt is None or isinstance(_dt, NaTType): + dt_ = to_naive_utc_datetime(dt, allow_nat=False, **kwargs) + if dt_ is None or isinstance(dt_, NaTType): msg = "dt cannot be NaT or None." raise ValueError(msg) - if isinstance(_dt, _pd.Timestamp): - _dt = _pd.DatetimeIndex([_dt]) - elif isinstance(_dt, (_pd.DatetimeIndex, _pd.Series)): - _dt = _pd.DatetimeIndex(_dt) + if isinstance(dt_, pd.Timestamp): + dt_ = pd.DatetimeIndex([dt_]) + elif isinstance(dt_, (pd.DatetimeIndex, pd.Series)): + dt_ = pd.DatetimeIndex(dt_) else: msg = "dt could not be resolved to a datetime, DatetimeIndex, Series, or array-like of datetimes." - raise ValueError( - msg - ) + raise ValueError(msg) julian_century_origin = pd.Timestamp("1899-12-31T12:00:00", tz=None) td_seconds = (dt_ - julian_century_origin).total_seconds() @@ -607,15 +599,13 @@ def _decimal_hour_of_day( The decimal hour. This is a scalar if ``date_time`` is a single value. """ - _dt = to_naive_utc_datetime(date_time, allow_nat=False) - if isinstance(_dt, NaTType) or _dt is None: + dt = to_naive_utc_datetime(date_time, allow_nat=False) + if isinstance(dt, NaTType) or dt is None: msg = "date_time cannot be NaT or None." raise ValueError(msg) - if not isinstance(_dt, (_pd.Timestamp, _pd.Series, _pd.DatetimeIndex)): + if not isinstance(dt, (pd.Timestamp, pd.Series, pd.DatetimeIndex)): msg = "date_time could not be resolved to a datetime, DatetimeIndex, Series, or array-like of datetimes." - raise ValueError( - msg - ) + raise ValueError(msg) if isinstance(dt, pd.Timestamp): dt = pd.DatetimeIndex([dt]) @@ -684,17 +674,13 @@ def __init__( **kwargs, ) -> None: - try: - freq_start = to_1d_ndarray(freq_start).astype(float) - s = freq_start.size - freq_stop = to_1d_ndarray(freq_stop, expected_size=s).astype(float) - amplitude = to_1d_ndarray(amplitude, expected_size=s).astype(float) - phase_lead = to_1d_ndarray(phase_lead, expected_size=s).astype(float) - if wave_group is not None: - wave_group = to_1d_ndarray(wave_group, expected_size=s).astype(str) - except ValueError as e: - msg = f"Error parsing tidal parameters: {e}" - raise ValueError(msg) + freq_start = to_1d_ndarray(freq_start, dtype=float) + s = freq_start.size + freq_stop = to_1d_ndarray(freq_stop, expected_size=s, dtype=float) + amplitude = to_1d_ndarray(amplitude, expected_size=s, dtype=float) + phase_lead = to_1d_ndarray(phase_lead, expected_size=s, dtype=float) + if wave_group is not None: + wave_group = to_1d_ndarray(wave_group, expected_size=s, dtype=str) self.data = pd.DataFrame.from_dict( data={ @@ -884,9 +870,7 @@ def from_excel(cls, fname: FilePath, sheet_name: str | int = 0) -> Self: df = pd.read_excel(fname, sheet_name=sheet_name) if isinstance(df, dict): msg = "Excel file contains multiple sheets. Please specify sheet_name." - raise ValueError( - msg - ) + raise ValueError(msg) return cls.from_dataframe(df) @@ -954,7 +938,7 @@ def default( return cls.from_array([freq_start, freq_stop, amplitude_factor, phase_lead]) def pygtide_wavegroup_arg(self) -> NDArray[np.float64]: - """Return a copy of the parameters as ndarray. + """Return a copy of the paramaters as ndarray. The returned array is intended to be used as the argument to ``pygtide.set_wavegroup()`` method. @@ -1134,9 +1118,7 @@ def time_series( DataFrame DataFrame containing the tidal corrections. """ - if unit in {"mgal", "ugal", "nm/s^2"}: - unit = cast(Literal["mgal", "ugal", "nm/s^2"], unit.lower()) - else: + if unit not in {"mgal", "ugal", "nm/s^2"}: msg = f"invalid unit value '{unit}'" raise ValueError(msg) @@ -1153,20 +1135,23 @@ def time_series( duration_uncorr = int( np.ceil(pd.to_timedelta(duration).total_seconds() / 3600.0) ) - if _duration_uncorr <= 0: - msg = "duration must be a positive timedelta or number of hours." - raise ValueError( - msg - ) - _orig_endtime = _starttime_uncorr + _pd.Timedelta(hours=_duration_uncorr) - _duration = int(_np.ceil((_orig_endtime - _starttime).total_seconds() / 3600.0)) + if duration_uncorr <= 0: + msg = f"invalid duration {duration}: must be a positive timedelta or number of hours." + raise ValueError(msg) + + # pygtide outputs corrections from start of UTC day only + # so we must manipulate supplied starttime and duration + # such that the generated tides cover the specified interval + orig_endtime = starttime_uncorr + pd.Timedelta(hours=duration_uncorr) + + duration_final = int( + np.ceil((orig_endtime - starttime).total_seconds() / 3600.0) + ) sample_interval = int(sample_interval) if sample_interval <= 0: - msg = "sample_interval must be a positive integer number of seconds." - raise ValueError( - msg - ) + msg = f"invalid sample_interval {sample_interval}: must be a positive integer number of seconds." + raise ValueError(msg) with warnings.catch_warnings(): self._pgt.msg = False @@ -1185,22 +1170,24 @@ def time_series( **this_run_kwargs, ) - df = self._pgt.results() - if not isinstance(df, _pd.DataFrame): + tides_df = self._pgt.results() + if not isinstance(tides_df, pd.DataFrame): msg = "No results returned from pygtide prediction." raise ValueError(msg) normalised_cols = ["datetime", "signal", "tide", "pole_tide", "lod_tide"] - df = df.rename( - columns={a: b for a, b in zip(df.columns, normalised_cols)} + tides_df = tides_df.rename( + columns=dict(zip(tides_df.columns, normalised_cols, strict=True)) ).set_index("datetime") - df = df.set_index(to_naive_utc_datetime(df.index)) - if unit == "nm/s^2": - return df + tides_df = tides_df.set_index(to_naive_utc_datetime(tides_df.index)) + + # values in nm/s2, no conversion required if unit == "ugal": - return df * 1e-3 - if unit == "mgal": - return df * 1e-4 + tides_df *= 1e-3 + elif unit == "mgal": + tides_df *= 1e-4 + + return tides_df # TODO: site_id is not truly required, so remove and infer sites from lat/lon/elev def tidal_correction( @@ -1213,6 +1200,7 @@ def tidal_correction( site_id: SiteIDArray | None = None, unit: Literal["mgal", "ugal", "nm/s^2"] = "mgal", sample_interval: int = 60, + **kwargs, ) -> NDArray[np.float64]: """Compute tidal corrections at specified locations and times. @@ -1247,9 +1235,9 @@ def tidal_correction( times for that site, 3. linearly interpolate tidal corrections at the exact observation times. """ - lat = to_1d_ndarray(lat, dtype=float) - lon = to_1d_ndarray(lon, expected_size=lat.size, dtype=float) - elev = to_1d_ndarray(elev, expected_size=lat.size, dtype=float) + lat = to_1d_ndarray(lat).astype(float) + lon = to_1d_ndarray(lon, expected_size=lat.size).astype(float) + elev = to_1d_ndarray(elev, expected_size=lat.size).astype(float) date_time = pd.DatetimeIndex( to_naive_utc_datetime(date_time, allow_nat=False) ).round(freq="1s") @@ -1258,19 +1246,20 @@ def tidal_correction( # - set to a day before the minimum ensure that all are captured t0 = date_time.floor(freq="s").min() - pd.Timedelta(days=1) + # date_time_seconds = (date_time - t0).total_seconds().to_numpy(float) + date_time_seconds = date_time.astype("int64") + if site_id is None: msg = ( "site_id is a required parameter for " "EternaPredictTidalCorrection tidal_correction method." ) - raise ValueError( - msg - ) + raise ValueError(msg) if isinstance(site_id, str): site_id = [site_id] * lat.size site_id = to_1d_ndarray(site_id, expected_size=lat.size, dtype=str) - corrs = np.full_like(lat, np.nan, dtype=np.float64) + corrs = np.zeros_like(lat, dtype=float) for site in np.unique(site_id): site_mask = site_id == site @@ -1282,7 +1271,7 @@ def tidal_correction( # TODO: need to break this up into multiple calls to time_series if the # duration is too long for pygtide to handle # e.g. sites visited days/weeks/years apart -> lots of work for nowt - duration_hrs = ( + _duration_hrs = ( int(np.ceil((date_time[site_mask].max() - t0).total_seconds() / 3600.0)) ) + 1 @@ -1296,17 +1285,15 @@ def tidal_correction( unit=unit, ).mul(-1.0) - if not isinstance(ts.index, _pd.DatetimeIndex): + if not isinstance(ts.index, pd.DatetimeIndex): msg = "Unexpected time series index type from pygtide results." - raise ValueError( - msg - ) + raise ValueError(msg) ts = ts.set_index(ts.index.round(freq="1s")) if not (corrs[site_mask] == 0.0).all(): msg = "Unexpected non-zero values in corrs for site mask." raise ValueError(msg) - corrs[site_mask] = _np.interp( + corrs[site_mask] = np.interp( x=date_time[site_mask], xp=ts.index, fp=ts["signal"].to_numpy(), From 122fd9a5b97102d42df8ea349ec253afa3da1934 Mon Sep 17 00:00:00 2001 From: Adrian Benson Date: Sat, 19 Sep 2026 02:07:15 +1200 Subject: [PATCH 07/36] Refactor: many edits and fixes to adress lint and type issues. --- pyproject.toml | 52 ++-- src/gsolve/__main__.py | 2 +- src/gsolve/core/data.py | 81 ++--- src/gsolve/core/excel_io.py | 46 ++- src/gsolve/core/utils.py | 160 +++------- src/gsolve/core/xr_accessor.py | 10 +- src/gsolve/core/xr_methods.py | 18 +- src/gsolve/gsolve_algorithms.py | 8 +- src/gsolve/gsolve_outputs.py | 13 +- src/gsolve/meter_conversion.py | 47 ++- src/gsolve/observations.py | 88 +++--- src/gsolve/reductions/anomalies.py | 31 +- src/gsolve/reductions/corrections.py | 84 +++--- src/gsolve/reductions/terrain_corrections.py | 281 ++++++++---------- src/gsolve/reports.py | 20 +- src/gsolve/scintrex.py | 92 +++--- src/gsolve/sites.py | 47 +-- src/gsolve/tide/earth_tide.py | 80 ++--- src/gsolve/tide/ocean_load.py | 27 +- tests/test_scintrex.py | 8 +- tests/test_terrain_corrections_consistency.py | 10 +- 21 files changed, 520 insertions(+), 685 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 51edc62..6426185 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -146,25 +146,38 @@ extend-select = [ "T20", # flake8-print "UP", # pyupgrade "YTT", # flake8-2020 + # "E501", + "PLR0917", ] ignore = [ - "ANN003", - "D105", - "DOC502", - "DOC501", - "D100", - "ISC001", # Conflicts with formatter + "ANN003", # alloww unannotated **kwargs + "D105", # allow undoc magic methods + "DOC501", # don't NEED to document exceptions + "D100", # allow unocumented modules "PLR09", # Too many <...> "PLR2004", # Magic value used in comparison "RET504", # Allow variable assignment only for return "PT001", # Conventions for parenthesis on pytest.fixture "D200", # Allow single line docstrings in their own line "B028", # Allow no stacklevel in warnings.warn + "UP040", # Allow old style type alias - will fix later ] [tool.ruff.lint.per-file-ignores] -"tests/**" = ["ANN001", "ANN201", "ANN202", "D", "DOC", "PLR6301"] -"examples/scripts/**" = ["D100"] +"examples/scripts/**" = [ + "D", # Docs not required for scripts + "DOC", # ditto + "T201", # Allow print() in scripts +] +"tests/**" = [ + "ANN001", # type annotations not required for func args + "ANN201", # or public func return + "ANN202", # or private func return + "D", # Docstrings not required + "DOC", # Docstrings not required + "PLR6301", # allow unused self in test classess + "PLC2701", # permit import of private functions etc - we are need to test em +] [tool.ruff.lint.pydocstyle] convention = "numpy" @@ -175,29 +188,6 @@ ignore-overlong-task-comments = true [tool.ruff.lint.flake8-annotations] allow-star-arg-any = true -[tool.ty.src] -include = ["gsolve/", "examples/scripts/"] - -[tool.ty.rules] -# Strict type safety defaults with mild allowances for edge cases -# all = "error" -possibly-unresolved-reference = "ignore" -division-by-zero = "ignore" - -[tool.numpydoc_validation] -checks = ["all", "EX01", "SA01"] - -[tool.basedpyright] -exclude = [ - "**/node_modules", - "**/__pycache__", - "**/.venv", - "**/env", - "**/build", - "**/dist", -] - - [tool.burocrata] notice = ''' # gSolve - gravity processing software. diff --git a/src/gsolve/__main__.py b/src/gsolve/__main__.py index 724be72..d278510 100644 --- a/src/gsolve/__main__.py +++ b/src/gsolve/__main__.py @@ -182,7 +182,7 @@ def main() -> None: # ruff: ignore[undocumented-public-function] try: args = parse_args() processing(args) - except Exception as e: # ruff: ignore[blind-except] + except Exception as e: sys.stderr.write(e + "\n") diff --git a/src/gsolve/core/data.py b/src/gsolve/core/data.py index 57ca023..68b8844 100644 --- a/src/gsolve/core/data.py +++ b/src/gsolve/core/data.py @@ -18,10 +18,12 @@ """Base class and function definitions for Gsolve data structures.""" +import copy import dataclasses import warnings from collections.abc import Callable from copy import deepcopy +from types import MappingProxyType from typing import Any, ClassVar, Self import pandas as pd @@ -397,7 +399,7 @@ class GSolveTable(_HasKnownFields, abc.ABC): The primary data storage object. """ - _known_fields: ClassVar[dict[str, DataFieldSpecification]] + _known_fields: ClassVar[MappingProxyType[str, DataFieldSpecification]] _default_excel_sheet_name: ClassVar[str | tuple[str, ...]] = "" def __init__(self) -> None: @@ -408,7 +410,7 @@ def __repr__(self) -> str: if hasattr(self, "data"): rval.append(f"data:shape={self.data.shape}") if hasattr(self, "params") and isinstance(self.params, GSolveParameters): - rval.append(self.params.__param_str__()) + rval.append(self.params._param_str()) rval = ", ".join(rval) @@ -702,7 +704,7 @@ def write_to_csv( expand_datetime: str | None = None, drop_datetime: bool = False, bool_to_int: bool = False, - include_unknown_fields: bool = True, + # include_unknown_fields: bool = True, **kwargs, ) -> None: """Write data to a csv file. @@ -770,7 +772,7 @@ def write_to_csv( class GSolveParameters: """Base class to store parameters related to GSolveTable derived classes.""" - def __param_str__(self) -> str: + def _param_str(self) -> str: # Return a string representation of the parameters return repr(self).partition("(")[2].rpartition(")")[0] @@ -780,7 +782,7 @@ def __copy__(self) -> Self: def copy(self) -> Self: """Return a deep copy of object.""" # ruff: ignore[docstring-missing-returns] - return deepcopy(self) + return copy.copy(self) def to_dict(self) -> dict: """Return parameters as a dict.""" # ruff: ignore[docstring-missing-returns] @@ -847,26 +849,26 @@ def from_series( GSolveParameters """ - _ds = ds.copy() - if _ds.index.nlevels > 1: + ds = ds.copy() + if ds.index.nlevels > 1: msg = "MultiIndex series not supported." raise ValueError(msg) + args: dict[str, Any] = { str(k): v for k, v in ds.items() if k in cls.__dataclass_fields__ } - missing_args = [k for k in cls.__dataclass_fields__ if k not in args] + missing_args = [k for k in cls.__dataclass_fields__ if k not in args] if not skip_missing and missing_args: msg = f"skip_missing=False: missing required parameters: {missing_args}" - raise TypeError( - msg - ) - extra_args = [k for k in _ds.index if k not in cls.__dataclass_fields__] + raise TypeError(msg) + + extra_args = [k for k in ds.index if k not in cls.__dataclass_fields__] if extra_args and not skip_unknown_parameters: - msg = f"series contains unknown parameters: {_ds.index[extra_args].to_list()}" - raise TypeError( - msg + msg = ( + f"series contains unknown parameters: {ds.index[extra_args].to_list()}" ) + raise TypeError(msg) return cls(**args) @@ -886,6 +888,7 @@ def to_excel( self, fname: FilePath, sheet_name: str | None = None, + *, if_workbook_exists: IfWorkbookExists = "error", if_sheet_exists: IfSheetExists = "error", parameter_name_label: str = "parameter", @@ -930,9 +933,7 @@ def to_excel( "sheet_name is None and object has no " "_default_excel_sheet_name attribute." ) - raise ValueError( - msg - ) + raise ValueError(msg) write_excel_worksheet( prepare_writable_df(params_ds.to_frame(), normalize_column_names=True), @@ -986,19 +987,15 @@ def _concat_gsolvetable_dataframes_with_fill( f"incompatible kwarg axis={kwargs['axis']}, " "function operates in vstack (axis=0) mode only." ) - raise ValueError( - msg - ) + raise ValueError(msg) kwargs["axis"] = 0 use_known_fields = False if known_fields is not None: use_known_fields = True - if not all([hasattr(f, "default") for f in known_fields.values()]): - msg = f"if specified, known_fields must be a dict of DataFieldSpecification objects" - raise TypeError( - msg - ) + if not all(hasattr(f, "default") for f in known_fields.values()): + msg = "if specified, known_fields must be a dict of DataFieldSpecification objects" + raise TypeError(msg) do_str_fill = fill_str is not None if do_str_fill: @@ -1015,12 +1012,15 @@ def _concat_gsolvetable_dataframes_with_fill( in_df1_only = [c for c in df1.columns if c not in df2.columns] idx = df2.index for c in in_df1_only: - if use_known_fields and c in known_fields: - if known_fields[c].default is not None: - combined_df.loc[idx, c] = combined_df.loc[idx, c].fillna( - known_fields[c].default - ) - continue + if ( + use_known_fields + and c in known_fields + and known_fields[c].default is not None + ): + combined_df.loc[idx, c] = combined_df.loc[idx, c].fillna( + known_fields[c].default + ) + continue if do_str_fill and is_string_dtype(df1[c]): combined_df.loc[idx, c] = combined_df.loc[idx, c].fillna(fill_str) elif do_bool_fill and is_bool_dtype(df1[c]): @@ -1029,16 +1029,19 @@ def _concat_gsolvetable_dataframes_with_fill( in_df2_only = [c for c in df2.columns if c not in df1.columns] idx = df1.index for c in in_df2_only: - if use_known_fields and c in known_fields: - if known_fields[c].default is not None: - combined_df.loc[idx, c] = combined_df.loc[idx, c].fillna( - known_fields[c].default - ) - continue + if ( + use_known_fields + and c in known_fields + and known_fields[c].default is not None + ): + combined_df.loc[idx, c] = combined_df.loc[idx, c].fillna( + known_fields[c].default + ) + continue if do_str_fill and is_string_dtype(df2[c]): combined_df.loc[idx, c] = combined_df.loc[idx, c].fillna(fill_str) continue - elif do_bool_fill and is_bool_dtype(df2[c]): + if do_bool_fill and is_bool_dtype(df2[c]): combined_df.loc[idx, c] = combined_df.loc[idx, c].fillna(fill_bool) return combined_df diff --git a/src/gsolve/core/excel_io.py b/src/gsolve/core/excel_io.py index 10e924e..5c31a80 100644 --- a/src/gsolve/core/excel_io.py +++ b/src/gsolve/core/excel_io.py @@ -82,9 +82,7 @@ def get_true_sheet_name( except IndexError as err: if raise_error: msg = f"excel file {excel_file} has no sheet at index: {sheet_name}" - raise ValueError( - msg - ) + raise ValueError(msg) from err return None else: sheet_names_lc = [ @@ -94,26 +92,27 @@ def get_true_sheet_name( return sheet_names[sheet_names_lc.index(sheet_name.lower())] if raise_error: msg = f"excel file {excel_file} has no sheet named '{sheet_name}'" - raise ValueError( - msg - ) + raise ValueError(msg) return None def _parse_sheet_name_arg( sheet_name: str | int | Sequence[str | int], ) -> list[str | int]: - """Parse and validate sheet_name argument.""" # ruff: ignore[docstring-missing-returns] - sheet_name_list: list[str | int] - if is_list_like(sheet_name): - sheet_name_list = [s for s in sheet_name] # pyrefly:ignore[not-iterable] - else: - sheet_name_list = [sheet_name] # pyrefly:ignore[bad-assignment] + """Parse and validate sheet_name argument. + + Returns + ------- + list : str + Sheet names as a list + """ + sheet_name_list: list[str | int] = ( + list(sheet_name) if is_list_like(sheet_name) else [sheet_name] + ) if not all(isinstance(s, (str, int)) for s in sheet_name_list): - msg = "sheet_name args must be either a str (sheet name) or an int (sheet index)" - raise TypeError( - msg + msg = ( + "sheet_name args must be either a str (sheet name) or an int (sheet index)" ) raise TypeError(msg) @@ -224,18 +223,14 @@ def write_excel_worksheet( f"invalid value for {if_workbook_exists=}, must be one of " f"{get_args(IfWorkbookExists)}" ) - raise ValueError( - msg - ) + raise ValueError(msg) if if_sheet_exists not in get_args(IfSheetExists): msg = ( f"invalid value for {if_sheet_exists=}, must be one of " f"{get_args(IfSheetExists)}" ) - raise ValueError( - msg - ) + raise ValueError(msg) writer_kwargs: dict[str, Any] = { # "engine": "openpyxl", # "xlsxwriter", "openpyxl", "xlwt" @@ -248,9 +243,7 @@ def write_excel_worksheet( if excel_file.exists(): if if_workbook_exists == "error": msg = f"file {excel_file} already exists, and arg {if_workbook_exists=}" - raise ValueError( - msg - ) + raise ValueError(msg) if if_workbook_exists == "append": writer_kwargs["mode"] = "a" @@ -261,8 +254,7 @@ def write_excel_worksheet( with pd.ExcelWriter(excel_file, **writer_kwargs) as writer: df.to_excel(writer, sheet_name=sheet_name, **kwargs) except PermissionError: - msg = f"Cannot write to {excel_file}, it is probably open in another application" - raise PermissionError( - msg + msg = ( + f"Cannot write to {excel_file}, it is probably open in another application" ) raise PermissionError(msg) from None diff --git a/src/gsolve/core/utils.py b/src/gsolve/core/utils.py index 0a395f6..8caaa24 100644 --- a/src/gsolve/core/utils.py +++ b/src/gsolve/core/utils.py @@ -22,18 +22,18 @@ import itertools import sys -import warnings from collections.abc import Sequence -from os import PathLike -from typing import Any, Literal, Type, TypeAliasType, get_args, get_origin, overload +from typing import Any, Literal, TypeAliasType, get_args, get_origin, overload import numpy as np import pandas as pd from numpy.typing import ArrayLike, DTypeLike, NDArray from pandas.api.types import ( + is_bool_dtype, is_datetime64_any_dtype, is_dict_like, is_list_like, + is_string_dtype, ) from pandas.api.typing import NaTType, NAType @@ -161,9 +161,7 @@ def to_points3d( """ if not is_points3d_like(v): msg = "object is not Points3D-like so cannot be converted to a true Points3D" - raise TypeError( - msg - ) + raise TypeError(msg) x, y, z = v x = to_1d_ndarray(v[0]).astype(np.float64) y = to_1d_ndarray(v[1], expected_size=x.size).astype(np.float64) @@ -234,9 +232,8 @@ def _nat_check[T](v: T) -> T: if not allow_nat: if isinstance(v, (pd.DatetimeIndex, pd.Series)): if any(v.isna()): - msg_0 = "input contains values that resolve to NaT and allow_nat=False" - raise ValueError( - msg_0 + msg_0 = ( + "input contains values that resolve to NaT and allow_nat=False" ) raise ValueError(msg_0) elif v is pd.NaT: @@ -248,7 +245,7 @@ def _nat_check[T](v: T) -> T: if isinstance(t, (NaTType, NAType)): return _nat_check(pd.NaT) - if isinstance(t, pd.Timestamp) or isinstance(t, pd.DatetimeIndex): + if isinstance(t, (pd.Timestamp, pd.DatetimeIndex)): return _nat_check(t if t.tz is None else t.tz_convert("UTC").tz_localize(None)) if isinstance(t, pd.Series): ds = t if t.dtype == "datetime64[ns]" else pd.to_datetime(t, **kwargs) @@ -270,14 +267,12 @@ def _nat_check[T](v: T) -> T: return_scalar = True try: idx = pd.to_datetime(t, **kwargs) - except Exception: + except Exception as err: msg = ( f"unable to convert input '{t}' of type {type(t).__name__} " "to Timestamp or DateTimeIndex" ) - raise ValueError( - msg - ) + raise ValueError(msg) from err rval = _nat_check( idx if idx.tz is None else idx.tz_convert("UTC").tz_localize(None) @@ -291,40 +286,13 @@ def to_1d_ndarray( a: ArrayLike, expected_size: int | None = None, extend_len_1_array: bool = False, + dtype: DTypeLike | None = None, ) -> NDArray: - _a = np.atleast_1d(a) - if _a.ndim > 1: - _a = np.squeeze(_a) - if _a.ndim != 1: - msg = f"input not convertible to 1d array" - raise ValueError(msg) - - Replicates the functionality of numpy.atleast_1d, but with additional - checks for expected size and optional extension of length-1 arrays. - - Parameters - ---------- - a : array-like - The input to be converted to a 1D numpy array. - expected_size : int, optional - If specified, the function will raise a ValueError if the resulting - array does not have this size. - extend_len_1_array : bool, default False - If True and the input is a length-1 array, it will be extended to - the specified expected_size. - dtype : data-type, optional - If specified, the resulting array will be cast to this data type. - - Returns - ------- - numpy.ndarray - A 1D numpy array with the specified properties. - """ a = np.atleast_1d(a) if a.ndim > 1: a = np.squeeze(a) if a.ndim != 1: - msg = "input not convertible to 1d array" + msg = f"input not convertible to 1d array" raise ValueError(msg) if extend_len_1_array and a.size == 1: @@ -332,14 +300,10 @@ def to_1d_ndarray( a = np.full(expected_size, a[0]) else: msg_0 = "expected_size must be specified if extend_len_1_array is True" - raise ValueError( - msg_0 - ) - if expected_size is not None and _a.size != expected_size: - msg = f"expected array of size {expected_size}, got size = {_a.size}" - raise ValueError( - msg - ) + raise ValueError(msg_0) + if expected_size is not None and a.size != expected_size: + msg = f"expected array of size {expected_size}, got size = {a.size}" + raise ValueError(msg) if dtype is not None: a = a.astype(dtype=dtype) @@ -348,28 +312,21 @@ def to_1d_ndarray( def to_1d_ndarray_or_float(a: ArrayLike) -> NDArray[np.float64] | np.float64: - _a = to_1d_ndarray(a).astype(np.float64) - if _a.size == 1: - return _a[0] - return _a + a = to_1d_ndarray(a).astype(np.float64) + return a[0] if a.size == 1 else a # Remove in future release def check_duplicate_index(idx: pd.Index | pd.DataFrame | pd.Series) -> None: """Raise a ValueError if the index contains duplicate values.""" if isinstance(idx, (pd.DataFrame, pd.Series)): - _idx = idx.index - elif isinstance(idx, pd.Index): - _idx = idx - else: + idx = idx.index + elif not isinstance(idx, pd.Index): msg = f"idx must be a pandas Index, DataFrame, or Series, not {type(idx).__name__}" - raise TypeError( - msg - ) + raise TypeError(msg) - if _idx.duplicated().any(): - idx_name = _idx.name or "index" - idx_dupes = _idx[_idx.duplicated().tolist()] + if idx.duplicated().any(): + idx_dupes = idx[idx.duplicated().tolist()] msg = f"duplicate index values: {idx_dupes.unique().to_list()}" raise ValueError(msg) @@ -550,9 +507,7 @@ def columns_to_timestamp( ts_columns = ts_columns or DEFAULT_TIMESTAMP_COLUMNS if not is_list_like(ts_columns): msg = f"ts_columns must be list-like, not {type(ts_columns).__name__}" - raise TypeError( - msg - ) + raise TypeError(msg) n_ts_columns = len(ts_columns) if not 3 <= n_ts_columns <= len(DEFAULT_TIMESTAMP_COLUMNS): @@ -560,9 +515,7 @@ def columns_to_timestamp( f"length of ts_columns is {n_ts_columns}, " "must be between 3 (=ymd) and 8 (=ymdHMSun)" ) - raise ValueError( - msg - ) + raise ValueError(msg) matched_columns = [c for c in ts_columns if c in df.columns] n_matched = len(matched_columns) @@ -572,18 +525,14 @@ def columns_to_timestamp( f"found {n_matched} columns matching ts_columns, " f"must be >= 3 (ymd) and <= {n_ts_columns} the length of ts_columns" ) - raise ValueError( - msg - ) + raise ValueError(msg) if matched_columns != list(ts_columns[:n_matched]): msg = ( f"expected columns {ts_columns[:n_matched]} " f"!= matched columns {matched_columns}" ) - raise ValueError( - msg - ) - rename_map = dict(zip(matched_columns, DEFAULT_TIMESTAMP_COLUMNS)) + raise ValueError(msg) + rename_map = dict(zip(matched_columns, DEFAULT_TIMESTAMP_COLUMNS, strict=False)) return pd.to_datetime( df.loc[:, matched_columns].rename(columns=rename_map), **kwargs ) @@ -637,9 +586,7 @@ def timestamp_to_columns( if resolution is not None: if resolution not in AVAIL_TRUNCATION_COLUMNS: msg = f"resolution '{resolution}' is not in {AVAIL_TRUNCATION_COLUMNS}" - raise ValueError( - msg - ) + raise ValueError(msg) res = _TIMESTAMP_COLUMNS_TO_RESOLUTION[resolution] @@ -650,7 +597,7 @@ def timestamp_to_columns( elif round_method == "ceil": ds = ds.dt.ceil(res) else: - msg_0 = "unreconised rounding method '{round_method}'" + msg_0 = f"unreconised rounding method '{round_method}'" raise ValueError(msg_0) df = pd.DataFrame( @@ -762,9 +709,7 @@ def expand_datetime_column( cols_to_split = list(column_name) else: msg = f"column_name must be a string or list-like, not {type(column_name).__name__}" - raise TypeError( - msg - ) + raise TypeError(msg) cols_to_split = [str(c) for c in cols_to_split if str(c) in candidate_columns] if not cols_to_split: @@ -772,10 +717,13 @@ def expand_datetime_column( f"the specified column_name(s) are either missing or or are " "not datetime-like columns" ) - raise ValueError( - msg - ) + raise ValueError(msg) + if prefix is None or not prefix: + if len(cols_to_split) > 1: # ruff: ignore[if-else-block-instead-of-if-exp] + prefixes = [f"{n}_" for n in cols_to_split] + else: + prefixes = [""] else: prefixes = list(prefix) if is_list_like(prefix) else [prefix] if len(prefixes) != len(cols_to_split): @@ -783,9 +731,7 @@ def expand_datetime_column( f"inconsisitent 'column_name' and 'prefix' arg lengths: " f"{len(cols_to_split)} != {len(prefixes)}" ) - raise ValueError( - msg - ) + raise ValueError(msg) for col, pre in zip(cols_to_split, prefixes, strict=True): ts_df = timestamp_to_columns( @@ -794,10 +740,10 @@ def expand_datetime_column( existing = ts_df.columns.intersection(df.columns).to_list() if existing: if not overwrite: - msg = f"overwrite=False and splitting would overwrite columns: {existing}" - raise ValueError( - msg + msg = ( + f"overwrite=False and splitting would overwrite columns: {existing}" ) + raise ValueError(msg) df = df.drop(columns=existing) if not insert_after: @@ -806,9 +752,7 @@ def expand_datetime_column( i_dt = df.columns.get_loc(col) if not isinstance(i_dt, int): msg_0 = "unexpected error: could not locate resolution column in output dataframe" - raise TypeError( - msg_0 - ) + raise TypeError(msg_0) i_dt += 1 df = pd.concat([df.iloc[:, :i_dt], ts_df, df.iloc[:, i_dt:]], axis=1) @@ -966,9 +910,7 @@ def generate_loop_intervals( db = to_naive_utc_datetime(datetime_bounds, allow_nat=False) if not isinstance(db, (pd.Series, pd.DatetimeIndex)) or len(db) < 2: msg = "datetimes must be an array-like object with at least two elements." - raise ValueError( - msg - ) + raise ValueError(msg) db = pd.Series(db) if not db.is_monotonic_increasing: @@ -1006,9 +948,7 @@ def identify_loop_blocks( dt = to_naive_utc_datetime(datetimes, allow_nat=False) if not isinstance(dt, (pd.Series, pd.DatetimeIndex)) or len(dt) < 2: msg = "datetimes must be an array-like object with at least two elements." - raise ValueError( - msg - ) + raise ValueError(msg) dt = pd.Series(dt) # ensure we have a Series for diff() and indexing if not dt.is_monotonic_increasing: msg = "datetimes must be sorted in increasing order." @@ -1019,11 +959,9 @@ def identify_loop_blocks( gaps = dt.diff().gt(gap) one_sec = pd.Timedelta("1s") - gap_bounds: list[pd.Timestamp] = [ - dt.iloc[0] - one_sec, - *dt.loc[gaps].to_list(), - dt.iloc[-1] + one_sec, - ] + gap_bounds: list[pd.Timestamp] = ( + [dt.iloc[0] - one_sec] + dt.loc[gaps].to_list() + [dt.iloc[-1] + one_sec] # ruff: ignore[collection-literal-concatenation] + ) if as_intervals: return pd.IntervalIndex.from_tuples( list(itertools.pairwise(gap_bounds)), closed="left" @@ -1066,9 +1004,7 @@ def loops_from_gaps( dt = to_naive_utc_datetime(datetimes, allow_nat=False) if not isinstance(dt, (pd.Series, pd.DatetimeIndex)) or len(dt) < 2: msg = "datetimes must be an array-like object with at least two elements." - raise ValueError( - msg - ) + raise ValueError(msg) loop_intervals = identify_loop_blocks(dt, gap, as_intervals=True) loop_ids = generate_loop_names( len(loop_intervals), start=loop_start, step=loop_step, format_str=loop_format @@ -1159,7 +1095,7 @@ def dms2rad( return np.deg2rad(deg) -def convert_single_timestamp_arg( +def _convert_single_timestamp_arg( t: DatetimeScalar, allow_nat: bool = False, err_prefix: str | None = None, **kwargs ) -> pd.Timestamp: diff --git a/src/gsolve/core/xr_accessor.py b/src/gsolve/core/xr_accessor.py index 42b4970..410e997 100644 --- a/src/gsolve/core/xr_accessor.py +++ b/src/gsolve/core/xr_accessor.py @@ -151,9 +151,7 @@ def clip_to_points( "GravitySites object missing required point columns: " f"{self.xdim}, {self.ydim}" ) - raise TypeError( - msg - ) + raise TypeError(msg) x = points.data[self.xdim].to_numpy() y = points.data[self.ydim].to_numpy() @@ -437,11 +435,9 @@ def generate_distance_mask( np.ndarray A boolean array of same dimensions as the calling DataArray. """ - if mask_type not in ("radial", "rectangular"): + if mask_type not in {"radial", "rectangular"}: msg = f"mask_type must be 'radial' or 'rectangular', not '{mask_type}'" - raise ValueError( - msg - ) + raise ValueError(msg) if max_dist is not None and max_dist <= min_dist: msg = f"invalid {max_dist=}, must be > {min_dist=}" raise ValueError(msg) diff --git a/src/gsolve/core/xr_methods.py b/src/gsolve/core/xr_methods.py index 8e96ad6..5a0e930 100644 --- a/src/gsolve/core/xr_methods.py +++ b/src/gsolve/core/xr_methods.py @@ -139,23 +139,17 @@ def prepare_dem( "DataArray object. Use 'var_name' to specify the variable to " f"convert. Variables in dem: {list(dem.data_vars)}" ) - raise ValueError( - msg - ) - input_var_name = str(list(dem.data_vars.keys())[0]) + raise ValueError(msg) + input_var_name = str(next(iter(dem.data_vars))) dem = dem[input_var_name] if not isinstance(dem, xr.DataArray): msg = f"dem must be an xarray Dataset or DataArray, not {type(dem).__name__}" - raise TypeError( - msg - ) + raise TypeError(msg) dem = dem.squeeze() if dem.ndim != 2: msg = f"Dem must be a 2D array. Object is {dem.ndim}D with shape {dem.shape}" - raise ValueError( - msg - ) + raise ValueError(msg) # Drop singleton coordinate variables that are not dimensions (e.g. 'band', 'spatial_ref') # These can cause xarray/rioxarray broadcasting/indexing issues during operations @@ -172,9 +166,7 @@ def prepare_dem( f"prepare_dem(): dropping unused coordinate '{coord_name}' " f"failed with error: {e}" ) - raise RuntimeError( - msg - ) from e + raise RuntimeError(msg) from e # set dimension names if y_dim and dem.tcorr.ydim != y_dim: diff --git a/src/gsolve/gsolve_algorithms.py b/src/gsolve/gsolve_algorithms.py index 3f23bee..60ee014 100644 --- a/src/gsolve/gsolve_algorithms.py +++ b/src/gsolve/gsolve_algorithms.py @@ -294,9 +294,7 @@ def g_solver_lstsq( # ruff: ignore[too-many-positional-arguments] f"invalid percentile value {percentile_clipping}, " "must be between 0 and 100 inclusive" ) - raise ValueError( - msg - ) + raise ValueError(msg) n_obs = np.size(obs_g) n_ties = np.size(ties_site_id) @@ -349,9 +347,7 @@ def g_solver_lstsq( # ruff: ignore[too-many-positional-arguments] "obs_g_not_detided must be provided when " "calculate_calibration_factor is True" ) - raise ValueError( - msg_0 - ) + raise ValueError(msg_0) A[i, n_sites + (2 * n_loops)] = float(obs_g_not_detided[i]) # Ties to absolute sites diff --git a/src/gsolve/gsolve_outputs.py b/src/gsolve/gsolve_outputs.py index ee8098e..e816388 100644 --- a/src/gsolve/gsolve_outputs.py +++ b/src/gsolve/gsolve_outputs.py @@ -150,6 +150,12 @@ def __init__( calculate_calibration_factor=calculate_calibration_factor, ) + self.obs_solution: pd.DataFrame + self.site_solution: pd.DataFrame + self.loop_solution: pd.DataFrame + self.observations_input: pd.DataFrame + self.reference_sites_input: pd.DataFrame + def set_inputs(self, obs: pd.DataFrame, ref_sites: pd.DataFrame) -> None: """Add input data used in the gsolve run.""" self.observations_input = obs.copy() @@ -173,9 +179,7 @@ def set_solutions(self, results: GSolveSolverReturn) -> None: "calibration factor was not calculated but " "calculate_calibration_factor is True." ) - raise ValueError( - msg - ) + raise ValueError(msg) # store the calculated calibration factor in the params object self.params.calculated_calibration_factor = calibration_factor @@ -278,8 +282,7 @@ def plot_residual_cdf( loops = df_loops ax_title = f"{ax_title} each loop" elif is_list_like(loop): - loops: list[str] = [str(l) for l in loop] # type: ignore[bad-assignment-type] - ax_title = f"{ax_title} loops {', '.join(loops)}" + loops: list[str] = [str(l) for l in loop] elif loop == "all": loops = ["all"] df["loop"] = "all" diff --git a/src/gsolve/meter_conversion.py b/src/gsolve/meter_conversion.py index 5fdcb3e..c974168 100644 --- a/src/gsolve/meter_conversion.py +++ b/src/gsolve/meter_conversion.py @@ -19,10 +19,9 @@ """Module for converting Lacoste-Romberg G and D meter readings to mGal.""" import pathlib -import warnings from collections.abc import Sequence from io import StringIO -from typing import Any, Protocol, TextIO, runtime_checkable +from typing import Protocol, Self, TextIO, runtime_checkable import numpy as np import numpy.typing as npt @@ -113,28 +112,28 @@ def __init__( c_reading = np.atleast_1d(np.array(counter_reading, dtype=np.float64)) value_mgal = np.atleast_1d(np.array(value_mgal, dtype=np.float64)) - if _c_reading.ndim != 1 or _c_reading.size == 0: + if c_reading.ndim != 1 or c_reading.size == 0: msg = "counter_reading must be a non-empty 1-dimensional array." raise ValueError(msg) - if _np.isnan(_c_reading).any(): + if np.isnan(c_reading).any(): msg = "counter_reading contains NaN." raise ValueError(msg) - if _value_mgal.ndim != 1 or _value_mgal.size == 0: + if value_mgal.ndim != 1 or value_mgal.size == 0: msg = "value_mgal must be a non-empty 1-dimensional array." raise ValueError(msg) - if _np.isnan(_value_mgal).any(): + if np.isnan(value_mgal).any(): msg = "value_mgal contains NaN." raise ValueError(msg) - if _c_reading.size != _value_mgal.size: + if c_reading.size != value_mgal.size: msg = "counter_reading and value_mgal arrays must be the same shape." raise ValueError(msg) nrows: int = c_reading.size if interval_factor is not None: - _interval_factor = _np.atleast_1d(interval_factor).astype(float) - if _interval_factor.ndim != 1 or _interval_factor.size == 0: + interval_factor = np.atleast_1d(interval_factor).astype(float) + if interval_factor.ndim != 1 or interval_factor.size == 0: msg = "if specified, interval_factor must be a non-empty 1-dimensional array." raise ValueError(msg) @@ -144,11 +143,11 @@ def __init__( interval_factor = np.append(interval_factor, np.nan) else: msg = ( - f"invalid interval_factor: array size {_interval_factor.size} is not " + f"invalid interval_factor: array size {interval_factor.size} is not " f"the same as or 1 less than counter_reading ({nrows})." ) raise ValueError(msg) - if _np.isnan(_interval_factor[:-1]).any(): + if np.isnan(interval_factor[:-1]).any(): msg_0 = "interval_factor is specified, but contains NaN values." raise ValueError(msg_0) recalc_value_mgal = True @@ -237,7 +236,7 @@ def set_datetime_range( st = to_naive_utc_datetime(starttime, allow_nat=False) except ValueError as e: msg = f"Error setting starttime: {e}" - raise ValueError(msg) + raise ValueError(msg) from None else: msg = f"invalid starttime type {type(starttime)}. Should be datetimelike or None." raise TypeError(msg) @@ -249,7 +248,7 @@ def set_datetime_range( et = to_naive_utc_datetime(endtime, allow_nat=False) except ValueError as e: msg = f"Error setting endtime: {e}" - raise ValueError(msg) + raise ValueError(msg) from None else: msg = ( f"invalid endtime type {type(endtime)}. Should be datetimelike or None." @@ -264,19 +263,19 @@ def set_datetime_range( self._endtime = et @property - def starttime(self) -> _pd.Timestamp | None: + def starttime(self) -> pd.Timestamp | None: """The date from which correction parameters are valid.""" st = getattr(self, "_starttime", None) - if st is not None and not isinstance(st, _pd.Timestamp): + if st is not None and not isinstance(st, pd.Timestamp): msg = f"invalid starttime type {type(st)}. Should be pandas.Timestamp or None." raise TypeError(msg) return st @property - def endtime(self) -> _pd.Timestamp | None: + def endtime(self) -> pd.Timestamp | None: """The date up to which correction parameters are valid.""" r = getattr(self, "_endtime", None) - if r is not None and not isinstance(r, _pd.Timestamp): + if r is not None and not isinstance(r, pd.Timestamp): msg = f"invalid endtime type {type(r)}. Should be pandas.Timestamp or None." raise TypeError(msg) return r @@ -331,16 +330,16 @@ def convert_readings( if meter_id is not None: m_meter_id = np.atleast_1d(meter_id).astype(str) == self.meter_id - if _m_meter_id.size == 0: + if m_meter_id.size == 0: msg_0 = "invalid meter_id arg: empty array." raise ValueError(msg_0) - if _m_meter_id.ndim != 1: + if m_meter_id.ndim != 1: msg_0 = "invalid meter_id arg: must be a scalar or 1-dimensional array." raise ValueError(msg_0) - if _m_meter_id.size == 1 and _readings.size > 1: - _m_meter_id = _np.full(_readings.shape, _m_meter_id[0]) - elif _m_meter_id.size != _readings.size: + if m_meter_id.size == 1 and readings.size > 1: + m_meter_id = np.full(readings.shape, m_meter_id[0]) + elif m_meter_id.size != readings.size: msg_0 = "invalid meter_id arg: length must match readings array." raise ValueError(msg_0) else: @@ -358,10 +357,10 @@ def convert_readings( ) raise TypeError(msg_0) - if _date_time.size != _readings.size: + if date_time.size != readings.size: msg_0 = "invalid date_time array: date_time values must be the same length as readings." raise ValueError(msg_0) - if any(_date_time.isna()): + if any(date_time.isna()): msg_0 = "date_time contains NaT values." raise ValueError(msg_0) diff --git a/src/gsolve/observations.py b/src/gsolve/observations.py index d76ad8d..b19eda7 100644 --- a/src/gsolve/observations.py +++ b/src/gsolve/observations.py @@ -237,33 +237,43 @@ class GravityObservations(GSolveTable): """ - _known_fields: dict[str, DataFieldSpecification] = { - "site_id": COMMON_FIELDS["site_id"], - "datetime": COMMON_FIELDS["datetime"], - "meter_id": DataFieldSpecification( - "meter_id", str, "", True, legacy_name="meter" - ), - "loop": COMMON_FIELDS["loop"], - "active": COMMON_FIELDS["active"], - "meter_reading": DataFieldSpecification( - "meter_reading", float, np.nan, False, legacy_name="reading" - ), - "meter_reading_mgal": DataFieldSpecification( - "meter_reading_mgal", float, np.nan, False - ), - "loop_tdelta": DataFieldSpecification("loop_tdelta", float, np.nan, False), - "survey_tdelta": DataFieldSpecification("survey_tdelta", float, np.nan, False), - "calibration_factor": DataFieldSpecification( - "calibration_factor", float, 1.0, False - ), - "earth_tide_corr": DataFieldSpecification("earth_tide_corr", float, 0.0, False), - "ocean_load_corr": DataFieldSpecification("ocean_load_corr", float, 0.0, False), - "custom_corr": DataFieldSpecification("custom_corr", float, 0.0, False), - "gravity_corr": DataFieldSpecification("gravity_corr", float, np.nan, False), - "meter_reading_converter_id": DataFieldSpecification( - "meter_reading_converter_id", str, "NA", False - ), - } + _known_fields: MappingProxyType[str, DataFieldSpecification] = MappingProxyType( + { + "site_id": COMMON_FIELDS["site_id"], + "datetime": COMMON_FIELDS["datetime"], + "meter_id": DataFieldSpecification( + "meter_id", str, "", True, legacy_name="meter" + ), + "loop": COMMON_FIELDS["loop"], + "active": COMMON_FIELDS["active"], + "meter_reading": DataFieldSpecification( + "meter_reading", float, np.nan, False, legacy_name="reading" + ), + "meter_reading_mgal": DataFieldSpecification( + "meter_reading_mgal", float, np.nan, False + ), + "loop_tdelta": DataFieldSpecification("loop_tdelta", float, np.nan, False), + "survey_tdelta": DataFieldSpecification( + "survey_tdelta", float, np.nan, False + ), + "calibration_factor": DataFieldSpecification( + "calibration_factor", float, 1.0, False + ), + "earth_tide_corr": DataFieldSpecification( + "earth_tide_corr", float, 0.0, False + ), + "ocean_load_corr": DataFieldSpecification( + "ocean_load_corr", float, 0.0, False + ), + "custom_corr": DataFieldSpecification("custom_corr", float, 0.0, False), + "gravity_corr": DataFieldSpecification( + "gravity_corr", float, np.nan, False + ), + "meter_reading_converter_id": DataFieldSpecification( + "meter_reading_converter_id", str, "NA", False + ), + } + ) _index_field: str = "obs_id" _default_excel_sheet_name: str | tuple[str, ...] = ("observations", "Survey Data") @@ -451,7 +461,7 @@ def set_obs_id( elif is_list_like(idx) and isinstance(idx, Iterable): # a sequence will converted to index - new_idx = pd.Index(_idx, name=self._index_field, dtype=str) + new_idx = pd.Index([str(i) for i in idx], name=self._index_field, dtype=str) else: msg = f"invalid idx arg of type '{type(idx).__name__}'" @@ -607,10 +617,10 @@ def set_fixed_time_datum( if t is None or pd.isna(t): self._fixed_time_datum = None else: - _t = to_naive_utc_datetime(t) - if isinstance(_t, pd.Timestamp): - self._fixed_time_datum = _t - elif _t is pd.NaT: + t = to_naive_utc_datetime(t) + if isinstance(t, pd.Timestamp): + self._fixed_time_datum = t + elif t is pd.NaT: self._fixed_time_datum = None else: msg = f"invalid fixed_time_datum of type '{type(t).__name__}'" @@ -973,7 +983,9 @@ def _activate_deactivate( ------ ValueError If any specified ``obs_id``, ``site_id`` or ``loop`` values are not found in the data. - """ + TypeError + If any input is not a string or iterable of strings. + """ # ruff: ignore[docstring-extraneous-exception] def _parse_inputs(o: str | Iterable[str] | None) -> list[str]: if o is None: @@ -1135,7 +1147,7 @@ def plot_observed_data( y_column: str = "meter_reading_mgal", savefilename: FilePath | None = None, figsize: tuple[float, float] = (12, 8), - ax=None, # ruff: ignore[missing-type-function-argument] + ax: plt.Axes | None = None, show: bool = True, **kwargs, ) -> tuple[plt.Figure, plt.Axes]: @@ -1228,9 +1240,9 @@ def _make_network(self, sites: GravitySites) -> pd.DataFrame: ) # merge with 'site' object to get location information - network_df = pd.merge( - left=station_order, right=sites.data, on="site_id", how="inner" - ).loc[:, ["site_id", "loop", "latitude", "longitude"]] + network_df = station_order.merge(sites.data, on="site_id", how="inner").loc[ + :, ["site_id", "loop", "latitude", "longitude"] + ] station_occupations = network_df.site_id.value_counts() return network_df, station_occupations @@ -1639,7 +1651,7 @@ def __copy__(self) -> Self: return type(self)(obs=self.observations.copy(), sites=self.sites.copy()) def copy(self) -> Self: - """Return a deep copy.""" + """Return a deep copy.""" # ruff: ignore[docstring-missing-returns] return copy.copy(self) @classmethod diff --git a/src/gsolve/reductions/anomalies.py b/src/gsolve/reductions/anomalies.py index 0251d91..d3fb0f0 100644 --- a/src/gsolve/reductions/anomalies.py +++ b/src/gsolve/reductions/anomalies.py @@ -19,8 +19,10 @@ """Functions and classes to compute standard gravity anomalies.""" -import numpy as _np -import pandas as _pd +from types import MappingProxyType + +import numpy as np +import pandas as pd from numpy.typing import ArrayLike from gsolve.core.data import DataFieldSpecification, GSolveTable @@ -249,7 +251,7 @@ def compute_free_air_anomaly( if any(_args_contain_nulls(absolute_gravity, normal_gravity, free_air_correction)): msg = "inputs contain nan's" raise ValueError(msg) - return _np.atleast_1d( + return np.atleast_1d( to_1d_ndarray_or_float(absolute_gravity) - ( to_1d_ndarray_or_float(normal_gravity) @@ -390,7 +392,6 @@ class GravityAnomalies(GSolveTable): ), } ) - _default_excel_sheet_name: str = "gravity_anomalies" def __init__( self, @@ -419,9 +420,7 @@ def __init__( abs_grav_df = absolute_gravity.to_frame(name="absolute_gravity") else: msg = f"invalid type for arg 'absolute_gravity': {type(absolute_gravity)}" - raise TypeError( - msg - ) + raise TypeError(msg) if abs_grav_df is None: msg_0 = "absolute_gravity has no site_solution data" raise ValueError(msg_0) @@ -460,15 +459,12 @@ def __init__( "invalid type for corrections_provider argument: " f"{type(corrections_parameters).__name__}" ) - raise TypeError( - msg - ) + raise TypeError(msg) # ensure we have entry in `sites` for all absolute gravity data sites if not abs_grav_df.index.isin(sites_df.index).all(): - msg_0 = "absolute_gravity has sites with no corresponding site info in sites" - raise ValueError( - msg_0 + msg_0 = ( + "absolute_gravity has sites with no corresponding site info in sites" ) raise ValueError(msg_0) @@ -483,9 +479,8 @@ def __init__( corrs = corr_provider.compute(sites=sites_df) else: if not self.data.index.isin(precomputed_corrections.data.index).all(): - msg_0 = "precomputed corrections do not provide corrections for all sites" - raise ValueError( - msg_0 + msg_0 = ( + "precomputed corrections do not provide corrections for all sites" ) raise ValueError(msg_0) precomputed_corrections.data = precomputed_corrections.data.loc[ @@ -503,9 +498,7 @@ def __init__( "required TerrainCorrectionData: " f"got {type(terrain_corrections).__name__}" ) - raise TypeError( - msg - ) + raise TypeError(msg) tc = terrain_corrections.get_corrections( self.data.index, if_missing="fill", fill_value=np.nan ) diff --git a/src/gsolve/reductions/corrections.py b/src/gsolve/reductions/corrections.py index 292225d..7755c07 100644 --- a/src/gsolve/reductions/corrections.py +++ b/src/gsolve/reductions/corrections.py @@ -21,7 +21,7 @@ from collections.abc import Sequence from dataclasses import dataclass from types import MappingProxyType -from typing import ClassVar, Literal, cast +from typing import Literal import boule import numpy as np @@ -180,9 +180,7 @@ def normal_gravity_at_ellipsoid( b = 6356774.5161 else: msg = f"Unknown ellipsoid '{ellipsoid}': must be one of {valid_ellipsoids}" - raise ValueError( - msg - ) + raise ValueError(msg) lat = np.deg2rad(latitude) normal_gravity = ( @@ -475,11 +473,10 @@ def bouguer_slab_curvature_corrected( if isinstance(er, boule.Ellipsoid): Ro = float(er.mean_radius) else: - msg = f"Unknown ellipsoid '{ellipsoid_or_radius}': must be 'WGS84' or 'GRS80'" - raise ValueError( - msg + msg = ( + f"Unknown ellipsoid '{ellipsoid_or_radius}': must be 'WGS84' or 'GRS80'" ) - raise ValueError(msg) # ruff: ignore[type-check-without-type-error] + raise ValueError(msg) elif isinstance(ellipsoid_or_radius, boule.Ellipsoid): Ro = float(ellipsoid_or_radius.mean_radius) @@ -604,40 +601,37 @@ class GravityCorrections(GSolveTable): Parameters used to compute the gravity corrections. """ - _known_fields: ClassVar[MappingProxyType[str, DataFieldSpecification]] = ( - MappingProxyType( - { - COMMON_FIELDS["site_id"].name: COMMON_FIELDS["site_id"], - COMMON_FIELDS["latitude"].name: COMMON_FIELDS["latitude"], - COMMON_FIELDS["longitude"].name: COMMON_FIELDS["longitude"], - COMMON_FIELDS["height_ellipsoidal"].name: COMMON_FIELDS[ - "height_ellipsoidal" - ], - "normal_gravity_at_stn_elevation": DataFieldSpecification( - "normal_gravity_at_stn_elevation", float, default=np.nan - ), - "normal_gravity_at_ellipsoid": DataFieldSpecification( - "normal_gravity_at_ellipsoid", float, default=np.nan - ), - "free_air_correction": DataFieldSpecification( - "free_air_correction", float, default=np.nan - ), - "bouguer_slab_correction": DataFieldSpecification( - "bouguer_slab_correction", float, default=np.nan - ), - "bouguer_slab_curvature_corrected": DataFieldSpecification( - "bouguer_slab_curvature_corrected", float, default=np.nan - ), - "atmospheric_correction": DataFieldSpecification( - "atmospheric_correction", float, default=np.nan - ), - COMMON_FIELDS["absolute_gravity"].name: COMMON_FIELDS[ - "absolute_gravity" - ], - } - ) + _known_fields: MappingProxyType[str, DataFieldSpecification] = MappingProxyType( + { + "site_id": DataFieldSpecification("site_id", str, required=True), + "longitude": DataFieldSpecification("longitude", float, required=False), + "latitude": DataFieldSpecification("latitude", float, required=False), + "height_ellipsoidal": DataFieldSpecification( + "height_ellipsoidal", float, required=False, legacy_name="height" + ), + "normal_gravity_at_stn_elevation": DataFieldSpecification( + "normal_gravity_at_stn_elevation", float, required=False, default=np.nan + ), + "normal_gravity_at_ellipsoid": DataFieldSpecification( + "normal_gravity_at_ellipsoid", float, required=False, default=np.nan + ), + "free_air_correction": DataFieldSpecification( + "free_air_correction", float, required=False, default=np.nan + ), + "bouguer_slab_correction": DataFieldSpecification( + "bouguer_slab_correction", float, required=False, default=np.nan + ), + "bouguer_slab_curvature_corrected": DataFieldSpecification( + "bouguer_slab_curvature_corrected", + float, + required=False, + default=np.nan, + ), + "atmospheric_correction": DataFieldSpecification( + "atmospheric_correction", float, required=False, default=np.nan + ), + } ) - _default_excel_sheet_name: str = "gravity_corrections" data: pd.DataFrame params: GravityCorrectionParameters @@ -713,9 +707,7 @@ def __init__( "params must be None or a GravityCorrectionParameters object, " f"not '{type(params)}'" ) - raise TypeError( - msg - ) + raise TypeError(msg) def __repr__(self) -> str: return f"{type(self).__name__}({self.params._param_str()})" @@ -787,9 +779,7 @@ def compute( "argument 'sites' must be a Dataframe or GravitySites object, not " f"'{type(sites)}'" ) - raise TypeError( - msg - ) + raise TypeError(msg) lon = sites_df[cols["longitude"]].to_numpy() lat = sites_df[cols["latitude"]].to_numpy() diff --git a/src/gsolve/reductions/terrain_corrections.py b/src/gsolve/reductions/terrain_corrections.py index 2194901..26a0b3c 100644 --- a/src/gsolve/reductions/terrain_corrections.py +++ b/src/gsolve/reductions/terrain_corrections.py @@ -18,7 +18,9 @@ """Functions and classes for computing gravity terrain corrections.""" import dataclasses +import operator import pathlib +import sys import warnings from collections.abc import Iterable, Sequence from types import MappingProxyType @@ -28,8 +30,9 @@ import numpy as np import numpy.typing as npt import pandas as pd +import tqdm import xarray as xr -from pandas.core.series import Series +from numpy.f2py.auxfuncs import throw_error from tqdm import tqdm as _tqdm from gsolve.core._typing import ( @@ -133,18 +136,14 @@ def calculate_terrain_correction( "density_dataset must be an xarray.DataArray not " f"'{type(density_dataset).__name__}'" ) - raise TypeError( - msg - ) + raise TypeError(msg) if not dem.tcorr.is_compatible(density_dataset): msg_0 = ( "Specified density_dataset is incompatible with dem. " "Check that the DataArrays have the same shape and coordinates." ) - raise ValueError( - msg_0 - ) + raise ValueError(msg_0) # format the points if len(points) != 3: @@ -157,7 +156,7 @@ def calculate_terrain_correction( pts_z = to_1d_ndarray(points[2], expected_size=pts_x.size).astype(np.float64) except Exception as e: msg = f"Points must contain 1d x,y,z arrays of equal size: {e}" - raise ValueError(msg) + raise ValueError(msg) from None # check the correction distances use_distance_mask = True @@ -166,9 +165,7 @@ def calculate_terrain_correction( raise ValueError(msg) if max_dist <= min_dist: msg = f"Incompatible distance args, {max_dist=} not greater than {min_dist=}" - raise ValueError( - msg - ) + raise ValueError(msg) # get land sea mask land_sea_mask: xr.DataArray = dem.tcorr.get_land_sea_mask(sea_level_elevation) @@ -267,7 +264,7 @@ def calculate_terrain_correction( pt_topo_density = topo_density tcorr_topo[i] = tcorr_harmonica_topography( - (px, py, pz), + point=(px, py, pz), topography=pt_topo_elev, topography_density=pt_topo_density, ) @@ -285,7 +282,7 @@ def calculate_terrain_correction( pt_bathy_density = bathy_density tcorr_bathy[i] = tcorr_harmonica_bathymetry( - (px, py, pz), + point=(px, py, pz), bathymetry=pt_bathy_depth, bathymetry_density=pt_bathy_density, sea_level_elevation=sea_level_elevation, @@ -509,16 +506,21 @@ class TerrainCorrectionParameters(GSolveParameters): def __post_init__(self) -> None: self._sanity_check(if_errors="warn") - def _normalize_fields(self) -> None: - if self.name is None: - msg = "'name' attribute must be a non-zero length string" + def __setattr__(self, name: str, value: Any) -> None: # ruff: ignore[any-type] + fieldnames = [] + if name not in (n.name for n in dataclasses.fields(self)): + msg = f"unrecopgnised field name {name}" raise ValueError(msg) - name = str(self.name) - if not name: - msg = "'name' attribute must be a non-zero length string" - raise ValueError(msg) - object.__setattr__(self, "name", name) + if name in { + "method", + "name", + "site_height_field", + "site_easting_field", + "site_northing_field", + "distance_mask_type", + }: + return super().__setattr__(name, str(value)) if name in {"compute_topography", "compute_bathymetry"}: return super().__setattr__(name, bool(value)) @@ -534,16 +536,26 @@ def _normalize_fields(self) -> None: if name == "density_dataset_source" and value is None: return super().__setattr__(name, "") - if not value: - object.__setattr__(self, field_name, "") - continue - msg = ( - f"{field_name} attribute must be a DataArray, str, or " - f"Path-like object, not a {type(value).__name__}" - ) - raise TypeError( - msg - ) + msg = f"invalid type for {name} field: {type(value).__name__}" + raise TypeError(msg) + + return super().__setattr__(name, bool(value)) + + def _sanity_check(self, if_errors: Literal["warn", "error"] = "error") -> None: + """Check that all parameters are valid. + + Parameters + ---------- + if_errors : {"warn", "error"}, default "error" + How to handle anry validation errors. If ``'error'``, then raise + an exception. If ``'warn'``, issue a warning and continue checking. + """ + throw_error: bool = if_errors == "error" + # error_count: int = 0 + + def warn_(m: str) -> None: + warnings.warn(m, category=UserWarning) + # error_count += 1 if ( np.isnan(self.min_dist) @@ -552,48 +564,61 @@ def _normalize_fields(self) -> None: or self.max_dist <= self.min_dist ): msg = ( - "invalid 'min_dist' and 'max_dist' parameters. Must be real values where" + "invalid/incompatible 'min_dist' and 'max_dist'. Must be real values where" "0.0 <= min_dist < max_dist: " f"got min_dist={self.min_dist}, max_dist={self.max_dist}" ) - raise ValueError( - msg - ) + raise ValueError(msg) if throw_error else warn_(msg) + # check distance msk type is valid if not is_in_literal(self.distance_mask_type, TCorrDistanceMaskType): msg = ( f"invalid 'distance_mask_type': {self.distance_mask_type}. " f"Expected one of: {get_args(TCorrDistanceMaskType.__value__)}" ) - raise ValueError( - msg + raise ValueError(msg) if throw_error else warn_(msg) + + # check dem_source + if not _is_dataarray(self.dem_source) and not is_filepath_like(self.dem_source): + msg = ( + f"invalid dem_source: must be an xarray.DataArray or file path" + f", not {type(self.dem_source).__name__}" ) + raise TypeError(msg) if throw_error else warn_(msg) if _is_dataarray(self.dem_source): if not self.dem_source.tcorr.is_valid_dem: - msg_0 = "invalid dem_source DataArray: must be a 2D array of floats" - raise ValueError( - msg_0 - ) - elif not self.dem_source: - msg_0 = "invalid dem_source: must be an xarray.DataArray or file path" - raise TypeError( - msg_0 + msg = "invalid dem_source DataArray: must be a 2D array of floats" + raise ValueError(msg) if throw_error else warn_(msg) + elif is_filepath_like(self.dem_source): + if not self.dem_source: + msg = "invalid dem_source file-path like" + raise ValueError(msg) if throw_error else warn_(msg) + else: + msg = ( + f"invalid dem_source: must be an xarray.DataArray or file-path like" + f", not {type(self.dem_source).__name__}" ) - if throw_error: - raise ValueError(msg) - warn_(msg) + raise TypeError(msg) if throw_error else warn_(msg) # check density_dataset_source if _is_dataarray(self.density_dataset_source): if not self.density_dataset_source.tcorr.is_valid_dem(): - msg_0 = ( - "density_dataset_source is an xr.DataArray object but is not a valid DEM. " - "Check that it has the correct dimensions and coordinates." - ) - raise ValueError( - msg_0 + msg = ( + "invalid density_dataset_source DataArray: " + "must be a 2D array of floats" ) + raise ValueError(msg) if throw_error else warn_(msg) + elif is_filepath_like(self.density_dataset_source): + if not self.density_dataset_source: + msg = "" + elif self.density_dataset_source is not None: + msg = ( + "invalid density_dataset_source type: should be a file-path like, " + f"DataArray or None, not {type(self.density_dataset_source).__name__}" + ) + raise TypeError(msg) + # if throw_error else warn_(msg) def to_series( self, @@ -642,15 +667,11 @@ def to_series( elif is_list_like(index_prefix): if len(index_prefix) != 2: msg = "if index_prefix is list-like, it must have length 2" - raise ValueError( - msg - ) + raise ValueError(msg) idx_val, idx_name = index_prefix else: msg = "index_prefix must be a string or list-like of length 2" - raise ValueError( - msg - ) + raise ValueError(msg) ds[idx_name] = idx_val ds = ds.set_index([idx_name, "parameter"])[series_name] @@ -719,10 +740,8 @@ def from_dataframe(cls, df: pd.DataFrame) -> dict[str, Self]: params.append(ds) else: msg = f"params dataframe must have 2 or 3 columns: found {df.shape[1]}" - raise ValueError( - msg - ) - if not all([isinstance(ds, pd.Series) for ds in params]): + raise ValueError(msg) + if not all(isinstance(ds, pd.Series) for ds in params): msg_0 = "error converting dataframe to series" raise ValueError(msg_0) params = [cls.from_series(p) for p in params] @@ -764,18 +783,14 @@ def __init__( elif isinstance(params, Iterable): if not all(isinstance(p, TerrainCorrectionParameters) for p in params): msg = "if params is a list-like, all items must be TerrainCorrectionParameters objects" - raise TypeError( - msg - ) + raise TypeError(msg) params = list(params) else: msg = ( "params arg must be a TerrainCorrectionParameters object or a list-like" f" of TerrainCorrectionParameters objects, not '{type(params)}'" ) - raise TypeError( - msg - ) + raise TypeError(msg) for p in params: self.add_zone(params=p) @@ -793,9 +808,7 @@ def add_zone(self, params: TerrainCorrectionParameters) -> None: "params must be a TerrainCorrectionParameters object, " f"not {type(params)}" ) - raise TypeError( - msg - ) + raise TypeError(msg) self.params[params.name] = params @@ -868,9 +881,7 @@ def compute( "points must be a sequence of arrays of form (x, y, z) " f"or a GravitySites object, not {type(points)}" ) - raise TypeError( - msg - ) + raise TypeError(msg) # Establish if we need to get points for each zone. # - If the points is a tuple, get 1 set of x,y,z now # - If the points is a GravitySites object, check if the site coordinate fields @@ -927,12 +938,12 @@ def compute( params=None, ) - nan_error_description_displayed = False + nan_error_desciption_displayed = False for zone in self.zones: pars = self.params[zone].copy() if show_progress: - sys.stderr.write(f"Calculating terrain corrections for zone: {zone}\n") + sys.stderr.write(f"Calculating terrain corrections for zone: {zone}") # get points if necessary if get_xyz_per_zone: @@ -944,9 +955,7 @@ def compute( ) except Exception as e: msg = f"Error extracting site coordinates from GravitySites object: {e}" - raise ValueError( - msg - ) + raise ValueError(msg) from None # get the dem for this zone # maybe do not copy here @@ -960,9 +969,7 @@ def compute( f"DEM not specified or zone='{zone}': TerrainCorrectionParameter " "object must provide source file or an xarray.DataArray object." ) - raise ValueError( - msg - ) + raise ValueError(msg) # get the density model if defined if _is_dataarray(pars.density_dataset_source): @@ -1002,12 +1009,12 @@ def compute( indent = " " if show_progress else "" sys.stderr.write( f"{indent}Warning: zone '{zone}': terrain corrections " - f"not calculated for {n_missing_tc} of {len(x)} sites.\n" + f"not calculated for {n_missing_tc} of {len(x)} sites." ) - if not nan_error_description_displayed: - nan_error_description_displayed = True + if not nan_error_desciption_displayed: + nan_error_desciption_displayed = True sys.stderr.write( - f"{indent} This is probably due to:\n", + f"{indent} This is probably due to:", ) sys.stderr.write( f"{indent} (1) insufficient DEM coverage and/or\n" @@ -1115,17 +1122,13 @@ def __init__( "params is specified but terrain_corrections is None: " "must specify both or neither" ) - raise ValueError( - msg - ) + raise ValueError(msg) if params is None and terrain_corrections is not None: msg = ( "terrain_corrections is specified but params is None: " "must specify both or neither" ) - raise ValueError( - msg - ) + raise ValueError(msg) # initialise data frame with site_id's as index sids = to_1d_ndarray(site_id).astype(str) @@ -1155,59 +1158,47 @@ def __init__( if isinstance(params, TerrainCorrectionParameters): params_list = [params] elif isinstance(params, (list, tuple)): - _params_list = list(params) - if not all( - isinstance(p, TerrainCorrectionParameters) for p in _params_list - ): + params_list = list(params) + if not all(isinstance(p, TerrainCorrectionParameters) for p in params_list): msg_0 = ( "if params is a list or tuple, all items must be " "TerrainCorrectionParameters objects" ) - raise TypeError( - msg_0 - ) + raise TypeError(msg_0) else: msg = ( "params arg must be a TerrainCorrectionParameters object, None or a " "list or tuple of TerrainCorrectionParameters objects, " f"not '{type(params)}'" ) - raise TypeError( - msg - ) + raise TypeError(msg) if terrain_corrections is None: tc_list = [None] * len(params_list) elif isinstance(terrain_corrections, FloatArray): tc_list = [terrain_corrections] elif isinstance(terrain_corrections, (list, tuple)): - _tc_list = list(terrain_corrections) - if not all(isinstance(tc, FloatArray) for tc in _tc_list): + tc_list = list(terrain_corrections) + if not all(isinstance(tc, FloatArray) for tc in tc_list): msg_0 = ( "if terrain_corrections is a list or tuple, all items must be " "array-like (e.g. numpy arrays or pandas Series)" ) - raise TypeError( - msg_0 - ) + raise TypeError(msg_0) else: msg = ( "terrain_corrections arg must be an array-like, None or a list or tuple " f"of array-likes, not '{type(terrain_corrections)}'" ) - raise TypeError( - msg - ) + raise TypeError(msg) - if len(_params_list) != len(_tc_list): + if len(params_list) != len(tc_list): msg = ( "inconsistent params and terrain_corrections arg lengths: " f"{len(params_list)} params and {len(tc_list)} " "terrain_corrections specified" ) - raise ValueError( - msg - ) + raise ValueError(msg) for p, tc in zip(params_list, tc_list, strict=True): self.set_corrections(p, tc) @@ -1263,17 +1254,13 @@ def set_corrections( "params must be a TerrainCorrectionParameters object, " f"not {type(params)}" ) - raise TypeError( - msg - ) + raise TypeError(msg) if bathymetry_corrections is None and topography_corrections is None: msg_0 = ( "Must specify at least one of topography_corrections or " "bathymetry_corrections" ) - raise ValueError( - msg_0 - ) + raise ValueError(msg_0) tcorr_prefix = "tcorr" @@ -1302,9 +1289,7 @@ def set_corrections( ) except ValueError as e: msg = f"{corr_type} must be a 1d array of floats of the len as site_id: {e} " - raise ValueError( - msg - ) + raise ValueError(msg) from None self.set_column(label=col_name, data=c, dtype=float) # now set the total column @@ -1362,14 +1347,17 @@ def set_corrections( # insertion_point = self.data.columns.get_loc["easting"] + 1 # self.data.insert(insertion_point, output_height_field, np.nan) - if isinstance(elevations, SitesLike): - if not hasattr(elevations, "data"): - msg = "elevations arg is a SitesLike object but does not have a " - raise TypeError( - msg - ) - # z = elevations.data[input_height_field].astype(float).to_numpy() - # df = elevations.data[] + # if isinstance(elevations, SitesLike): + # if not hasattr(elevations, "data"): + # msg = "elevations arg is a SitesLike object but does not have a " + # raise TypeError(msg) + + # elevations = elevations.loc[self.data] + # else: + # elevations = to_1d_ndarray( + # elevations, expected_size=self.data.shape[0], dtype=float + # ) + # self.data[output_height_field] = elevations def __repr__(self) -> str: zones = ", ".join( @@ -1426,9 +1414,7 @@ def from_dataframe( "params must be a Dataframe, Series or TerrainCorrectionParameters " f"object, not {type(params)}" ) - raise TypeError( - msg - ) + raise TypeError(msg) consumed_columns = ["tcorr:total"] @@ -1455,9 +1441,7 @@ def from_dataframe( kk = f"{k}:topo" if kk not in df.columns: msg = f"terrain correction data missing for zone '{k}': '{kk}'" - raise KeyError( - msg - ) + raise KeyError(msg) tc_args["topography_corrections"] = df[kk].to_numpy() consumed_columns.append(kk) @@ -1465,9 +1449,7 @@ def from_dataframe( kk = f"{k}:bath" if kk not in df.columns: msg = f"terrain correction data missing for zone '{k}': '{kk}'" - raise KeyError( - msg - ) + raise KeyError(msg) tc_args["bathymetry_corrections"] = df[kk].to_numpy() consumed_columns.append(kk) @@ -1483,9 +1465,7 @@ def from_dataframe( "terrain corrections parameters and values are inconsistent: " f"no parameters for terrain_correction data {unconsumed_columns}" ) - raise ValueError( - msg - ) + raise ValueError(msg) return obj @@ -1660,7 +1640,8 @@ def to_csv(self, fname: FilePath | None = None, **kwargs) -> str | None: csv = "\n".join(csv) if fname is None: return csv - pathlib.Path(fname).write_text(csv) + pathlib.Path(fname).write_text(csv) # ruff: ignore[unspecified-encoding] + return None @classmethod def from_csv( @@ -1682,7 +1663,7 @@ def from_csv( TerrainCorrectionOutput """ - with pathlib.Path(fname).open() as f: + with pathlib.Path(fname).open() as f: # ruff: ignore[unspecified-encoding] lines = f.readlines() params = [l.lstrip("#").strip().split(",") for l in lines if l.startswith("#")] if len(params) == 0: @@ -1734,11 +1715,9 @@ def get_corrections( otherwise a DataFrame is returned. """ - if if_missing not in ["drop", "raise", "fill"]: + if if_missing not in {"drop", "raise", "fill"}: msg = f"invalid if_missing arg '{if_missing}'" - raise ValueError( - msg - ) # fixed typo in message + raise ValueError(msg) # fixed typo in message site_id_idx = pd.Index(np.atleast_1d(site_id).astype(str).tolist()) tcorrs = self.data.reset_index().set_index("site_id") diff --git a/src/gsolve/reports.py b/src/gsolve/reports.py index 8298906..b135c13 100644 --- a/src/gsolve/reports.py +++ b/src/gsolve/reports.py @@ -101,9 +101,7 @@ def __init__( if sites is None or results is None or observations is None: msg = "'observations', 'sites' and 'results' arguments must be provided." - raise ValueError( - msg - ) + raise ValueError(msg) if isinstance(observations, GravitySurvey): observations = observations.observations @@ -128,7 +126,7 @@ def __init__( def copy(self) -> Self: """Return a deep copy.""" # ruff: ignore[docstring-missing-returns] - return self.__copy__() + return self.__copy__() # ruff: ignore[unnecessary-dunder-call] def __copy__(self) -> Self: """Return a deep copy.""" # ruff: ignore[docstring-missing-returns] @@ -315,7 +313,7 @@ def to_excel( filename: FilePath, if_workbook_exists: IfWorkbookExists = "error", if_sheet_exists: IfSheetExists = "error", - **kwargs: Any, # ruff: ignore[any-type] + **kwargs: Any, ) -> None: """ Save the report data to an Excel file. @@ -347,9 +345,7 @@ def to_excel( if filename.exists(): if if_workbook_exists == "error": msg = f"file {filename} already exists, and arg {if_workbook_exists=}" - raise ValueError( - msg - ) + raise ValueError(msg) if if_workbook_exists == "append" and if_sheet_exists == "error": existing_worksheets = [ @@ -361,9 +357,7 @@ def to_excel( "Use 'if_workbook_exists' and 'if_sheet_exists' parameters " "to specify behaviour." ) - raise ValueError( - msg - ) + raise ValueError(msg) # observations write_excel_worksheet( @@ -421,7 +415,7 @@ def to_excel( # This is a kludge - should create method on parameter objects to # to normalise parameter outputs for writing to excel. def _format_value(x: Any) -> str | float | int | bool: # ruff: ignore[any-type] - if isinstance(x, _pd.Timedelta): + if isinstance(x, pd.Timedelta): return x.total_seconds() if isinstance(x, Path): return str(x) @@ -442,7 +436,7 @@ def _format_value(x: Any) -> str | float | int | bool: # ruff: ignore[any-type] write_excel_worksheet( df=pd.concat(all_params), - excel_file=filename, + filename=filename, sheet_name="metadata", if_workbook_exists="append", if_sheet_exists=if_sheet_exists, diff --git a/src/gsolve/scintrex.py b/src/gsolve/scintrex.py index 9e02945..a51eb0c 100644 --- a/src/gsolve/scintrex.py +++ b/src/gsolve/scintrex.py @@ -17,12 +17,11 @@ # Copyright (c) 2025 Earth Sciences New Zealand. import abc -import dataclasses import pathlib import warnings from collections.abc import Callable, Mapping, Sequence from types import MappingProxyType -from typing import Literal, Self +from typing import Literal, Self, TypeAlias import numpy as np import numpy.typing as npt @@ -287,35 +286,24 @@ def _set_data(self, data: pd.DataFrame, on_error: str = "raise") -> None: # if this_dtype is float: try: df[c] = df[c].astype(this_dtype) - except Exception: - if on_error in ["warn", "ignore"]: - if on_error == "warn": - warnings.warn( - f"bad data encountered in column '{c}', setting to nan" - ) - try: - df[c] = ( - df[c] - .replace(to_replace=["--", "****", "******"], value=np.nan) - .astype(this_dtype) - ) - except Exception as err: - msg = f"unfixable error converting data in column '{c}' to {this_dtype}" - raise TypeError( - msg - ) from err - else: + except Exception as err_read: + if on_error not in {"warn", "ignore"}: msg = f"error converting data in column '{c}' to {this_dtype}" - raise TypeError( - msg + raise TypeError(msg) from err_read + + if on_error == "warn": + warnings.warn( + f"bad data encountered in column '{c}', setting to nan" + ) + try: + df[c] = ( + df[c] + .replace(to_replace=["--", "****", "******"], value=np.nan) + .astype(this_dtype) ) except Exception as err_cant_replace: msg = f"unfixable error converting data in column '{c}' to {this_dtype}" raise TypeError(msg) from err_cant_replace - else: - if on_error == "warn": - msg = f"bad data encountered in column '{c}', setting to nan" - warnings.warn(msg) if ( "datetime" not in df.columns @@ -332,9 +320,9 @@ def _set_data(self, data: pd.DataFrame, on_error: str = "raise") -> None: df.insert(i_date_col, "datetime", dt) # ty:ignore[invalid-argument-type] df = df.drop(columns=["date", "time"]) - corr_flag_col_name = "corrections[drift-temp-na-tide-tilt]" - if corr_flag_col_name in df.columns: - flag_labels = corr_flag_col_name.rstrip("]").rpartition("[")[-1].split("-") + corr_flag_colname = "corrections[drift-temp-na-tide-tilt]" + if corr_flag_colname in df.columns: + flag_labels = corr_flag_colname.rstrip("]").rpartition("[")[-1].split("-") flag_labels = [f"correction_{f}" for f in flag_labels] flags = df.pop(corr_flag_col_name).str.split("").str[1:-1].to_list() @@ -389,6 +377,10 @@ def from_file( msg = f"No data read from {cg6_file}" raise ValueError(msg) + if not file_data[0].startswith("/"): + msg = f"No header data found in {cg6_file}" + raise ValueError(msg) + idx_column_names = 0 for i, line in enumerate(file_data): if not line.startswith(r"/"): @@ -485,9 +477,7 @@ def set_loop( args = (field, array, datetimes, time_gap) if all(a is None for a in args): msg = "At least one of 'field', 'array', 'datetimes' or 'time_gap' must be set." - raise ValueError( - msg - ) + raise ValueError(msg) if sum(a is not None for a in args) > 1: msg = "Only one of 'field', 'array', or 'datetimes' can be set." raise ValueError(msg) @@ -495,9 +485,7 @@ def set_loop( if field is not None: if field not in self.data.columns: msg = f"arg {field=}, but not column name '{field}' found in obj.data." - raise KeyError( - msg - ) + raise KeyError(msg) self.data[output_column] = self.data[field].astype(str) return @@ -505,9 +493,7 @@ def set_loop( array = np.atleast_1d(array) if len(array) != len(self.data): msg_0 = "Length of 'array' must match the number of observations." - raise ValueError( - msg_0 - ) + raise ValueError(msg_0) self.data[output_column] = array.astype(str).tolist() return @@ -525,9 +511,7 @@ def set_loop( ) else: msg_0 = "datetimes must be a dictionary, Series or array-like object." - raise TypeError( - msg_0 - ) + raise TypeError(msg_0) if not dates.is_monotonic_increasing: msg_0 = "datetimes must be sorted in increasing order." @@ -537,12 +521,10 @@ def set_loop( f"First datetime in 'datetimes' ({dates[0]}) must be <= " f"earliest observation time ({self.data.datetime.min()})" ) - raise ValueError( - msg - ) + raise ValueError(msg) if dates[-1] < self.data["datetime"].max(): - t_max = self.data["datetime"].max() + pd.Timedelta(seconds=1) - dates = pd.DatetimeIndex([*dates.to_list(), t_max]) + tmax = self.data["datetime"].max() + pd.Timedelta(seconds=1) + dates = pd.DatetimeIndex([*dates.to_list(), tmax]) loop_intervals = generate_loop_intervals(dates) loop_namer = pd.Series(loop_ids, index=loop_intervals) @@ -636,9 +618,7 @@ def to_gsolve_observations( missing = [f for f in include_non_standard_fields if f not in df.columns] if missing: msg = f"Requested non-standard fields not found in data: {missing}" - raise KeyError( - msg - ) + raise KeyError(msg) to_drop = set(df.columns) - set( GravityObservations.known_fields() + include_non_standard_fields @@ -689,11 +669,9 @@ def to_gsolve_sites( GravitySites """ - if coords_source not in ("user", "gps"): + if coords_source not in {"user", "gps"}: msg = f"coords_source must be 'user' or 'gps', not {coords_source}." - raise ValueError( - msg - ) + raise ValueError(msg) coord_cols = [f"{c}{coords_source}" for c in ("lat", "lon", "elev")] agg_method = "mean" if coords_source == "gps" else "first" @@ -753,11 +731,9 @@ def set_drift_correction( drift_zero_time = to_naive_utc_datetime(drift_zero_time) drift_rate = float(drift_rate) - if not isinstance(_drift_zero_time, pd.Timestamp): + if not isinstance(drift_zero_time, pd.Timestamp): msg = "drift_zero_time could not be converted to a valid Timestamp." - raise ValueError( - msg - ) + raise ValueError(msg) drift_corr = ( (self.data["datetime"] - drift_zero_time) @@ -808,7 +784,7 @@ def _scintrex_header_type_conversion( header_val: _ScintrexMetadataDataTypes, data_type: type | Callable | None = None, ) -> _ScintrexMetadataDataTypes | None: - if not header_val or data_type is None: + if header_val is None or data_type is None: return "" if data_type is pd.Timestamp: diff --git a/src/gsolve/sites.py b/src/gsolve/sites.py index 63caa3c..bb7ef42 100644 --- a/src/gsolve/sites.py +++ b/src/gsolve/sites.py @@ -20,7 +20,7 @@ from __future__ import annotations -from collections.abc import Iterable, Mapping +from collections.abc import Iterable, Mapping, Sequence from types import MappingProxyType from typing import Literal, Self @@ -300,9 +300,7 @@ def activate_ties(self, site_id: str | npt.ArrayLike | None = None) -> None: site_id = [str(s) for s in site_id] else: msg = "site_id must be None, a string, or an array-like of strings" - raise TypeError( - msg - ) + raise TypeError(msg) self._check_bad_site_ids(site_id) @@ -430,7 +428,7 @@ def _get_writable_df( normalize_column_names: bool = True, bool_to_int: bool = True, include_unknown_fields: bool = True, - ) -> _pd.DataFrame: + ) -> pd.DataFrame: """ Return GravitySite data as a DataFrame suitable for writing to an excel or csv file. @@ -570,7 +568,7 @@ def sample_elevation( xcol: str = "easting", ycol: str = "northing", method: str = "nearest", - ) -> _pd.Series | None: + ) -> pd.Series | None: """Get elevations at site locations from an DEM/xarray grid. Parameters @@ -596,7 +594,7 @@ def sample_elevation( """ if is_filepath_like(dem): dem = load_dem(dem) - elif isinstance(dem, DatasetOrArray.__value__): + elif isinstance(dem, DatasetOrArray): dem = prepare_dem(dem) else: msg = "dem must be file path or an xarray Dataset/DataArray" @@ -605,7 +603,7 @@ def sample_elevation( z = ( dem.interp( {dem.dims[0]: self.data[ycol], dem.dims[1]: self.data[xcol]}, - method=method, + method=method, # type: ignore[invalid-argument-type, ty:invalid-argument-type] ) .to_numpy() .diagonal() @@ -680,16 +678,12 @@ def merge( f"invalid type for other: " f"expected {type(self).__name__}, got {type(other)}" ) - raise TypeError( - msg - ) + raise TypeError(msg) valid_duplicates_args = {"drop", "error"} if if_duplicate not in valid_duplicates_args: msg = f"duplicates must be one of {valid_duplicates_args}, not '{if_duplicate}'" - raise ValueError( - msg - ) + raise ValueError(msg) other_df = other.data is_duplicate = other_df.index.isin(self.data.index) @@ -770,22 +764,17 @@ def __init__( "creating ReferenceGravity object: " f"site_id field contains duplicated values: {duplicates}" ) - raise ValueError( - msg - ) + raise ValueError(msg) # catch empty site_id - if idx.isna().any() or (idx == "").any(): # type: ignore[unresolved-attribute, ty:unresolved-attribute] - m = idx.isna() | (idx == "") - empty = _pd.Series(m) + if (m := idx.isna() | (idx == "")).any(): # ruff: ignore[compare-to-empty-string] + empty = pd.Series(m) empty = empty.loc[m.tolist()].index.to_list() msg = ( "creating ReferenceGravity object: " f"site_id field contains empty values at rows: {empty}" ) - raise ValueError( - msg - ) + raise ValueError(msg) self.data = pd.DataFrame(index=idx, data=None) self.set_column("gravity", gravity) @@ -797,9 +786,7 @@ def __init__( "creating ReferenceGravity object: " f"gravity field contains null values for sites: {nodata}" ) - raise ValueError( - msg - ) + raise ValueError(msg) for k, v in kwargs.items(): self.set_column(k, v) @@ -1012,16 +999,12 @@ def merge( f"invalid type for other: " f"expected {type(self).__name__}, got {type(other)}" ) - raise TypeError( - msg - ) + raise TypeError(msg) valid_duplicates_args = {"drop", "error"} if if_duplicate not in valid_duplicates_args: msg = f"duplicates must be one of {valid_duplicates_args}, not '{if_duplicate}'" - raise ValueError( - msg - ) + raise ValueError(msg) other_df = other.data is_duplicate = other_df.index.isin(self.data.index) diff --git a/src/gsolve/tide/earth_tide.py b/src/gsolve/tide/earth_tide.py index 4b3b3f9..7287ad0 100644 --- a/src/gsolve/tide/earth_tide.py +++ b/src/gsolve/tide/earth_tide.py @@ -20,7 +20,7 @@ import dataclasses import warnings -from typing import Any, Literal, Protocol, Self, cast, overload, runtime_checkable +from typing import Any, Literal, Protocol, Self, overload, runtime_checkable import numpy as np import pandas as pd @@ -40,7 +40,7 @@ ) from gsolve.core.utils import ( GSolveDataWarning, - convert_single_timestamp_arg, + _convert_single_timestamp_arg, dms2rad, to_1d_ndarray, to_naive_utc_datetime, @@ -66,6 +66,7 @@ def tidal_correction( # ruff: ignore[undocumented-public-method] elev: FloatArray, date_time: DatetimeArray, site_id: SiteIDArray | None = None, + **kwargs: Any, ) -> NDArray[np.float64]: ... def identifier(self, **kwargs) -> str: ... # ruff: ignore[undocumented-public-method] @@ -133,13 +134,15 @@ class LongmanConstants: c: float = 3.84399e10 # Mean distance between the centers earth-moon (cm) c1: float = 1.495983e13 # Mean distance between centers earth-sun (cm) e: float = 0.054900489 # Eccentricity of the moon's orbit - i: float = round(deg2rad(5.145, dtype=np.float64), ndigits=9) - # = 0.08979719 Inclination of moon's orbit to the ecliptic + i: float = round( + deg2rad(5.145), ndigits=9 + ) # = 0.08979719 Inclination of moon's orbit to the ecliptic m: float = 0.074804 # Ratio of mean motion of the sun to that of the moon mu: float = 6.67428e-08 # Newton's gravitational constant, 6.670e-8 in orig. M: float = 7.3477e25 # Mass of the moon in grams - omega: float = round(deg2rad(23.452, dtype=np.float64), ndigits=9) - # = 0.409315 Incl. of Earth's equator to ecliptic + omega: float = round( + deg2rad(23.452), ndigits=9 + ) # = 0.409315 Incl. of Earth's equator to ecliptic S: float = 1.98840987e33 # Mass of the sun in grams # https://aa.usno.navy.mil/downloads/publications/Constants_2021.pdf @@ -392,8 +395,8 @@ def tidal_correction( lon: FloatArray, elev: FloatArray, date_time: DatetimeArray, - site_id: SiteIDArray | None = None, - **kwargs, + site_id: SiteIDArray | None = None, # ruff: ignore[unused-method-argument] + **kwargs, # ruff: ignore[unused-method-argument] ) -> NDArray[np.float64]: """Compute tidal corrections at specified locations and times. @@ -467,26 +470,21 @@ def time_series( try: step = pd.Timedelta(step) - if not isinstance(step, pd.Timedelta): - msg = "step must be a valid timedelta or timedelta string." - raise ValueError(msg) except ValueError as e: msg = f"error parsing step: {e}" raise ValueError(msg) from e - try: - t0 = to_naive_utc_datetime(starttime, allow_nat=False) - t1 = to_naive_utc_datetime(endtime, allow_nat=False) - except ValueError as e: - msg = f"error parsing starttime and endtime: {e}" - raise ValueError(msg) from None + if not isinstance(step, pd.Timedelta): + msg = "step must be a valid timedelta or timedelta string." + raise TypeError(msg) + + t0 = _convert_single_timestamp_arg( + starttime, allow_nat=False, err_prefix="error parsing starttime" + ) + t1 = _convert_single_timestamp_arg( + endtime, allow_nat=False, err_prefix="error parsing endtime" + ) - if not isinstance(t0, pd.Timestamp): - msg = "startime not convertible to pandas.Timestamp." - raise ValueError(msg) - if not isinstance(t1, pd.Timestamp): - msg = "endtime not convertible to pandas.Timestamp." - raise ValueError(msg) if t0 >= t1: msg = "starttime is after or equal to endtime." raise ValueError(msg) @@ -523,6 +521,8 @@ def time_series( @overload def _decimal_julian_century(dt: DatetimeScalar, **kwargs) -> np.float64: ... + + @overload def _decimal_julian_century(dt: DatetimeArray, **kwargs) -> NDArray[np.float64]: ... @@ -763,7 +763,7 @@ def sort_and_validate(self, gap_threshold: float | None = None) -> None: if gaps.any(): warnings.warn( message=( - f"some frequency {gaps.sum()} intervals separated by" + "some frequency intervals separated by " f"greater than {gap_threshold} " ), category=UserWarning, @@ -1118,6 +1118,7 @@ def time_series( DataFrame DataFrame containing the tidal corrections. """ + unit = unit.lower() if unit not in {"mgal", "ugal", "nm/s^2"}: msg = f"invalid unit value '{unit}'" raise ValueError(msg) @@ -1178,8 +1179,9 @@ def time_series( normalised_cols = ["datetime", "signal", "tide", "pole_tide", "lod_tide"] tides_df = tides_df.rename( columns=dict(zip(tides_df.columns, normalised_cols, strict=True)) - ).set_index("datetime") - tides_df = tides_df.set_index(to_naive_utc_datetime(tides_df.index)) + ) + tides_df["datetime"] = to_naive_utc_datetime(tides_df["datetime"]) + tides_df = tides_df.set_index("datetime") # values in nm/s2, no conversion required if unit == "ugal": @@ -1200,7 +1202,6 @@ def tidal_correction( site_id: SiteIDArray | None = None, unit: Literal["mgal", "ugal", "nm/s^2"] = "mgal", sample_interval: int = 60, - **kwargs, ) -> NDArray[np.float64]: """Compute tidal corrections at specified locations and times. @@ -1231,13 +1232,13 @@ def tidal_correction( Corrections are computed by: 1. for each unique site_id, - 2. generate a time series of tidal corrections covering the observation + 2. generate a time series of tidal corrections covering the obsevarvation times for that site, 3. linearly interpolate tidal corrections at the exact observation times. """ - lat = to_1d_ndarray(lat).astype(float) - lon = to_1d_ndarray(lon, expected_size=lat.size).astype(float) - elev = to_1d_ndarray(elev, expected_size=lat.size).astype(float) + lat = to_1d_ndarray(lat, dtype=float) + lon = to_1d_ndarray(lon, expected_size=lat.size, dtype=float) + elev = to_1d_ndarray(elev, expected_size=lat.size, dtype=float) date_time = pd.DatetimeIndex( to_naive_utc_datetime(date_time, allow_nat=False) ).round(freq="1s") @@ -1246,9 +1247,6 @@ def tidal_correction( # - set to a day before the minimum ensure that all are captured t0 = date_time.floor(freq="s").min() - pd.Timedelta(days=1) - # date_time_seconds = (date_time - t0).total_seconds().to_numpy(float) - date_time_seconds = date_time.astype("int64") - if site_id is None: msg = ( "site_id is a required parameter for " @@ -1257,9 +1255,9 @@ def tidal_correction( raise ValueError(msg) if isinstance(site_id, str): site_id = [site_id] * lat.size - site_id = to_1d_ndarray(site_id, expected_size=lat.size, dtype=str) + site_id = to_1d_ndarray(site_id, expected_size=lat.size, dtype=float) - corrs = np.zeros_like(lat, dtype=float) + corrs = np.full_like(lat, np.nan) for site in np.unique(site_id): site_mask = site_id == site @@ -1271,7 +1269,7 @@ def tidal_correction( # TODO: need to break this up into multiple calls to time_series if the # duration is too long for pygtide to handle # e.g. sites visited days/weeks/years apart -> lots of work for nowt - _duration_hrs = ( + duration_hrs = ( int(np.ceil((date_time[site_mask].max() - t0).total_seconds() / 3600.0)) ) + 1 @@ -1290,9 +1288,13 @@ def tidal_correction( raise ValueError(msg) ts = ts.set_index(ts.index.round(freq="1s")) - if not (corrs[site_mask] == 0.0).all(): - msg = "Unexpected non-zero values in corrs for site mask." + if not np.isnan(corrs[site_mask]).all(): + msg = ( + f"Some values for site {site_id}: have already been set: " + "this should not happen" + ) raise ValueError(msg) + corrs[site_mask] = np.interp( x=date_time[site_mask], xp=ts.index, diff --git a/src/gsolve/tide/ocean_load.py b/src/gsolve/tide/ocean_load.py index e52f8fc..26674f4 100644 --- a/src/gsolve/tide/ocean_load.py +++ b/src/gsolve/tide/ocean_load.py @@ -135,9 +135,9 @@ def __init__( **metadata, ) -> None: if isinstance(site_id, str): - _site_id = np.array([site_id] * len(date_time)) - _site_id = np.atleast_1d(site_id).astype(str) - if _site_id.ndim != 1: + site_id = np.array([site_id] * len(date_time)) + site_id = np.atleast_1d(site_id).astype(str) + if site_id.ndim != 1: msg = "site_id argument must be 1-dimensional." raise ValueError(msg) @@ -193,7 +193,7 @@ def ocean_load_correction( else: site_id = np.atleast_1d(site_id).astype(str) - if len(_site_id) != len(dt): + if len(site_id) != len(dt): msg = "site_id and datetime arguments must have the same length." raise ValueError(msg) @@ -407,14 +407,14 @@ def _validate_timeseries_data(df: pd.DataFrame) -> None: msg = f"data must be a pandas DataFrame, not {type(df).__name__}." raise TypeError(msg) if not isinstance(df.index, pd.DatetimeIndex): - msg_0 = "timeseries not indexed by datetime." - raise TypeError(msg_0) + msg = "timeseries not indexed by datetime." + raise TypeError(msg) if df.shape[0] < 2: - msg_0 = "timeseries must contain at least two rows." - raise ValueError(msg_0) + msg = "timeseries must contain at least two rows." + raise ValueError(msg) if not df.index.is_monotonic_increasing: - msg_0 = "timeseries not sorted in increasing order." - raise ValueError(msg_0) + msg = "timeseries not sorted in increasing order." + raise ValueError(msg) # warn if non-uniform sampling interval/rate sample_intervals = (df.index[1:] - df.index[:-1]).total_seconds() @@ -482,9 +482,6 @@ def qtp_to_corrector( corrections=df["BergerLoadCorrection"].to_numpy().astype(float), **metadata, ) - else: - msg = f"invalid corr_type '{corr_type}', must be one of {'auto', 'timeseries', 'site-datetime'}." - raise ValueError(msg) msg = f"invalid corr_type '{corr_type}', must be one of {'auto', 'timeseries', 'site-datetime'}." raise ValueError(msg) @@ -637,9 +634,7 @@ def generate_qtp_input( # ruff: ignore[too-many-positional-arguments] else: elevation = np.atleast_1d(elevation).astype(float) - if not ( - _site_id.size == _datetimes.size == _lat.size == _lon.size == _elevation.size - ): + if not (site_id.size == datetimes.size == lat.size == lon.size == elevation.size): msg = "site_id, datetimes, latitude, longitude, and elevation arguments must all have the same shape." raise ValueError(msg) raise ValueError(msg) diff --git a/tests/test_scintrex.py b/tests/test_scintrex.py index 6c310c4..f5ea7cb 100644 --- a/tests/test_scintrex.py +++ b/tests/test_scintrex.py @@ -189,10 +189,10 @@ def test_cg6data_set_loop(cg6_file: pathlib.Path) -> None: cg6.set_loop( datetimes=dict(zip(times, ["a", "b"], strict=True)), loop_start=200, - output_column="x_loop", + output_column="xloop", ) - assert cg6.data["x_loop"].iloc[0] == "a" - assert cg6.data["x_loop"].iloc[i_mid] == "b" + assert cg6.data["xloop"].iloc[0] == "a" + assert cg6.data["xloop"].iloc[i_mid] == "b" # 2.3 from a series cg6.set_loop( @@ -212,6 +212,8 @@ def test_cg6data_set_loop(cg6_file: pathlib.Path) -> None: .eq(fmt_str.format(LOOP=301)) .all() ) + assert cg6.data["zloop"].iloc[0] == fstr.format(LOOP=300) + assert cg6.data.loc[cg6.data["line"].eq(2), "zloop"].eq(fstr.format(LOOP=301)).all() # Case X: bad args with pytest.raises(TypeError): diff --git a/tests/test_terrain_corrections_consistency.py b/tests/test_terrain_corrections_consistency.py index a144555..f58d44b 100644 --- a/tests/test_terrain_corrections_consistency.py +++ b/tests/test_terrain_corrections_consistency.py @@ -22,6 +22,8 @@ TerrainCorrectionData, TerrainCorrectionParameters, TerrainCorrector, + _is_dataarray, + calculate_terrain_correction, ) from gsolve.sites import GravitySites @@ -66,7 +68,7 @@ def sites() -> GravitySites: return sites -def pre_calculated_tcorr_data() -> TerrainCorrectionData: +def pre_calced_tcorr_data() -> TerrainCorrectionData: # Output from these test methods as at 09-04-2026 # - These values are not certain to be correct # - Catch changes that alter the results @@ -138,12 +140,12 @@ def test_terrain_correction_consistency(): assert results.data.loc[bad_points, tcorr_cols].isna().all(axis=None) # test that pre-calculated results are close to calculated results - pre_calculated_tcorr_data_ = pre_calculated_tcorr_data() + pre_calced_tcorr_data_ = pre_calced_tcorr_data() for col in tcorr_cols: - assert col in pre_calculated_tcorr_data_.data.columns + assert col in pre_calced_tcorr_data_.data.columns assert np.allclose( results.data[col].to_numpy(), - pre_calculated_tcorr_data_.data[col].to_numpy(), + pre_calced_tcorr_data_.data[col].to_numpy(), atol=1e-6, equal_nan=True, ) From 22bc7f5c2fecd17b05d4c8975227aac2e39cd09b Mon Sep 17 00:00:00 2001 From: Adrian Benson Date: Mon, 21 Sep 2026 00:01:01 +1200 Subject: [PATCH 08/36] Fix tests for keyword only args --- tests/test_anomalies.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_anomalies.py b/tests/test_anomalies.py index 0cd79dd..dc250a2 100644 --- a/tests/test_anomalies.py +++ b/tests/test_anomalies.py @@ -15,9 +15,11 @@ # SPDX-License-Identifier: GPLv3 from __future__ import annotations +from tornado.routing import AnyMatches import numpy as np import pytest +from scipy.constants import atm from gsolve.reductions.anomalies import ( compute_complete_bouguer_anomaly, @@ -39,6 +41,7 @@ def anomaly_args(): def test_compute_complete_bouguer_anomaly_basic(): + c = anomaly_args() ag = np.array([100.0, 200.0]) ng = np.array([10.0, 20.0]) fac = np.array([1.0, 2.0]) From fe7ce17bd48a588cf2734399e9f738d726eec293 Mon Sep 17 00:00:00 2001 From: Adrian Benson Date: Mon, 21 Sep 2026 01:54:24 +1200 Subject: [PATCH 09/36] Fix tests for keyword only args and other issues --- pyproject.toml | 3 +++ tests/test_anomalies.py | 2 +- tests/test_corrections.py | 4 ++-- tests/test_data_structures.py | 16 +++++++--------- tests/test_observations.py | 6 +++--- tests/test_reports.py | 4 ++-- tests/test_rongotai_survey.py | 2 +- 7 files changed, 19 insertions(+), 18 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6426185..8594c9c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -161,6 +161,7 @@ ignore = [ "D200", # Allow single line docstrings in their own line "B028", # Allow no stacklevel in warnings.warn "UP040", # Allow old style type alias - will fix later + "PLR6104", # Don't prefer augmented assignment - breaks pandas ] [tool.ruff.lint.per-file-ignores] @@ -177,6 +178,8 @@ ignore = [ "DOC", # Docstrings not required "PLR6301", # allow unused self in test classess "PLC2701", # permit import of private functions etc - we are need to test em + "PT011", # allow tests to catch exceptions without a 'match' arg + "PT030", # allow tests to catch warnings without a 'match' arg ] [tool.ruff.lint.pydocstyle] diff --git a/tests/test_anomalies.py b/tests/test_anomalies.py index dc250a2..8946e1e 100644 --- a/tests/test_anomalies.py +++ b/tests/test_anomalies.py @@ -15,11 +15,11 @@ # SPDX-License-Identifier: GPLv3 from __future__ import annotations -from tornado.routing import AnyMatches import numpy as np import pytest from scipy.constants import atm +from tornado.routing import AnyMatches from gsolve.reductions.anomalies import ( compute_complete_bouguer_anomaly, diff --git a/tests/test_corrections.py b/tests/test_corrections.py index c8c22ba..8b0b4bc 100644 --- a/tests/test_corrections.py +++ b/tests/test_corrections.py @@ -320,8 +320,8 @@ class TestGravityCorrectionParameters: def test_defaults(self): p = GravityCorrectionParameters() assert p.ellipsoid == "GRS80" - assert np.isclose(p.density_crust, 2670.0) - assert np.isclose(p.density_water, 1030.0) + assert p.density_crust == 2670.0 # ruff: ignore[float-equality-comparison] + assert p.density_water == 1030.0 # ruff: ignore[float-equality-comparison] assert p.use_curvature_corrected is True assert p.use_atmospheric_correction is True diff --git a/tests/test_data_structures.py b/tests/test_data_structures.py index 9d0839f..592c800 100644 --- a/tests/test_data_structures.py +++ b/tests/test_data_structures.py @@ -32,14 +32,12 @@ @pytest.fixture def gsolve_table_subclass(): class TestClass(data.GSolveTable): - _known_fields = MappingProxyType( - { - "a": data.DataFieldSpecification("a", str, "", True), - "b": data.DataFieldSpecification("b", float, 0.0, True), - "c": data.DataFieldSpecification("c", "datetime", pd.NaT, False), - "d": data.DataFieldSpecification("d", bool, False, False), - } - ) + _known_fields = { # ruff: ignore[mutable-class-default] + "a": data.DataFieldSpecification("a", str, "", True), + "b": data.DataFieldSpecification("b", float, 0.0, True), + "c": data.DataFieldSpecification("c", "datetime", pd.NaT, False), + "d": data.DataFieldSpecification("d", bool, False, False), + } _index_field = "a" return TestClass @@ -59,7 +57,7 @@ def test_data_field_specification(self): fs = data.DataFieldSpecification("longitude", float, 0.0) assert fs.name == "longitude" assert fs.dtype == float - assert np.isclose(fs.default, 0.0) + assert fs.default == 0.0 # ruff: ignore[float-equality-comparison] assert fs.required is False # def test_data_field_specification_convert(self): diff --git a/tests/test_observations.py b/tests/test_observations.py index 5eb652f..30beb91 100644 --- a/tests/test_observations.py +++ b/tests/test_observations.py @@ -97,8 +97,8 @@ def test_gravity_observations_init_has_obs_id(self) -> None: data = dummy_data() # obs_id not specified - obj_unspecified = GravityObservations(**data) - assert obj_unspecified.data.index.dtype.name in {"object", "str"} + obj_unspec = GravityObservations(**data) + assert obj_unspec.data.index.dtype.name in {"object", "str"} prefixes = obj_unspecified.data.index.str.partition(".").get_level_values(0) assert_index_equal( @@ -263,7 +263,7 @@ def test_gravity_observations_tdelta(self) -> None: for loop in obj3.loop_ids: m = obj3.data["loop"].eq(loop).to_list() - assert np.isclose(obj3.data[m]["loop_tdelta"].min(), 0.0) + assert obj3.data[m]["loop_tdelta"].min() == 0.0 # ruff: ignore[float-equality-comparison] def test_gravity_observations_timedelta_unit( self, diff --git a/tests/test_reports.py b/tests/test_reports.py index aca4e3e..2acd8a3 100644 --- a/tests/test_reports.py +++ b/tests/test_reports.py @@ -239,7 +239,7 @@ def test_to_excel_writes_expected_sheets_without_terrain( calls: list[dict[str, Any]] = [] - def _fake_write_excel_worksheet(df, excel_file, sheet_name, **kwargs): + def _fake_write_excel_worksheet(df, filename, sheet_name, **kwargs): # ruff: ignore[unused-function-argument] calls.append( { "excel_file": excel_file, @@ -294,7 +294,7 @@ def test_to_excel_writes_terrain_sheet_when_present( sheet_names: list[str] = [] - def _fake_write_excel_worksheet(df, excel_file, sheet_name, **_kwargs): # ruff: ignore[unused-function-argument] + def _fake_write_excel_worksheet(df, filename, sheet_name, **_kwargs): # ruff: ignore[unused-function-argument] sheet_names.append(sheet_name) monkeypatch.setattr( diff --git a/tests/test_rongotai_survey.py b/tests/test_rongotai_survey.py index a42b958..3dc17b1 100644 --- a/tests/test_rongotai_survey.py +++ b/tests/test_rongotai_survey.py @@ -53,7 +53,7 @@ def test_rongotai_network_adjustment(shared_datadir: pathlib.Path) -> None: for method in [1, 2, 3]: for percentile_clipping in [95, 100]: # reference site solution file - suffix = f"_ci{percentile_clipping:1d}" + suffix = "_ci%1i" % percentile_clipping # ruff: ignore[printf-string-formatting] site_solution_file = data_path / ( f"RIG_G106_site_solution_method{method}{suffix}.csv" ) From 343e3663a877208703710cb94f7cdd24585942db Mon Sep 17 00:00:00 2001 From: Adrian Benson Date: Tue, 22 Sep 2026 11:02:14 +1200 Subject: [PATCH 10/36] Add config for pre-commit and spell. --- cspell.json | 9 +-- prek.toml | 20 +++++- project-words.txt | 179 ---------------------------------------------- 3 files changed, 19 insertions(+), 189 deletions(-) diff --git a/cspell.json b/cspell.json index 491a245..1453755 100644 --- a/cspell.json +++ b/cspell.json @@ -9,11 +9,6 @@ } ], "dictionaries": [ - "project-words", - "python", - "python-common", - "data-science-tools", - "git", - "en-au" + "project-words" ] -} \ No newline at end of file +} diff --git a/prek.toml b/prek.toml index 6ab1784..7c7e002 100644 --- a/prek.toml +++ b/prek.toml @@ -4,7 +4,11 @@ hooks = [ { id = "trailing-whitespace" }, { id = "end-of-file-fixer" }, { id = "check-added-large-files" }, - + { id = "check-case-conflict" }, + { id = "detect-private-key" }, + { id = "mixed-line-ending", args = [ + "--fix=lf", + ] }, ] [[repos]] @@ -12,10 +16,20 @@ repo = "https://github.com/astral-sh/ruff-pre-commit" rev = "v0.16.7" # Ruff version. hooks = [ # Run the linter. - { id = "ruff-check", args = ["--fix"], types_or = ["python", "pyi"] }, + { id = "ruff-check", args = [ + "--fix", + ], types_or = [ + "python", + "pyi", + "pyproject", + ] }, # Run the formatter. - { id = "ruff-format", types_or = ["python", "pyi", "markdown"] }, + { id = "ruff-format", types_or = [ + "python", + "pyi", + "markdown", + ] }, ] diff --git a/project-words.txt b/project-words.txt index a006a95..adf28b5 100644 --- a/project-words.txt +++ b/project-words.txt @@ -1,194 +1,15 @@ -addopts -afactor -airgap -Aleksandr -alisonk -amsmath -amtruncate -anom -arccos -arcsin -arctan -argnames -argvalues -asanyarray -atleast -autoapi -automodule -autosummary -auxfuncs -Bartels -basedpyright -bathy -bathymetric -boldsymbol -Bouguer -Bullard -burocrata -contextily -copybutton -corrgrav -corrs -craigm -Cubbine -cumcount -currentmodule -deduplicator -detided -Docstrings -driftcorr -DSIR -earthtide ecdf -elevgps -elevuser -Elsevier -emsg -EPSG -errmsg -Eterna -ETGTAB -etide -extlinks -Fatiando -Fehr -figsize -filt fontsize fout -Frese -FURB -gcal -gcorr -GDAL -gfactor -Gotze -Götze -grav -gref -gsolve -gsolvetable -HARDISP -Hayford -Heiskanen -Hinze -Hofmann -horizontalalignment -IERS -ifac -ifactor -instrheight -interp isactive -isel -isscalar -issubdtype -jcentury -Kirkby -Kudryavtsev -kwargs -Lacoste -Lakshmanan -lamda -latgps -latuser linalg -lmbda -lodtidecor -Longman -longps -lonuser -lstsq -Maari markerfmt -mathjax -mdates -measurdur -mgal -milli -milligal -milligals -minversion -Moritz -multistation -ndarray -ndigits -NIWA -nlevels -nparam -numpydoc -olmpp -otide perc -pgtide pinv -poletidecor -pydata -pydatetime -pygrep -pygtide -pytest -pyupgrade -quicktide -rawgrav rcond -refurb regen -Renamer -rioxarray -Rongotai -rtol -rval -Saad -samprate -savetxt -sbcc -Schureman -SCINTREX -sensortemp setdiff -skipna -Somigliana -Spesivtsev -spher -stacklevel -Stagpoole -startdate -Tamura -tcorr -tcorrs -tdelta -temaari -tempcorr -Tenzer -testpaths -TGKB -tidalcompo -TIDALPARAM -tidalpoten -tidecorr -tiltcorr -toctree -Tontini tstamp -tstamps -typehints -ufeff -ugal -uncorr -undoc -UNSO -viewcode -vmatrix -wavegroup -Wellenhof -Wenzel -xcol -xdim xlabel -xticklabels -ycol -ydim ylabel yticks -zcol From 79ebf83b3f9fccc0319e377a249fa5a88aa6a389 Mon Sep 17 00:00:00 2001 From: Adrian Benson Date: Tue, 22 Sep 2026 11:02:39 +1200 Subject: [PATCH 11/36] Spell checking --- src/gsolve/core/data.py | 4 ++-- src/gsolve/core/utils.py | 4 ++-- src/gsolve/core/xr_accessor.py | 4 ++-- src/gsolve/gsolve_outputs.py | 15 ++++++++++++--- src/gsolve/observations.py | 4 ++-- src/gsolve/reductions/corrections.py | 2 +- src/gsolve/reductions/terrain_corrections.py | 14 +++++++------- src/gsolve/reports.py | 2 +- src/gsolve/tide/earth_tide.py | 8 ++++---- src/gsolve/tide/ocean_load.py | 4 ++-- 10 files changed, 35 insertions(+), 26 deletions(-) diff --git a/src/gsolve/core/data.py b/src/gsolve/core/data.py index 68b8844..1af6f15 100644 --- a/src/gsolve/core/data.py +++ b/src/gsolve/core/data.py @@ -803,7 +803,7 @@ def to_series( index_name : str | None, default is None The series index name. index_prefix : str | None, optional - Create a multiinidex where level 0 is 'index_prefix` and level 1 are + Create a multiindex where level 0 is 'index_prefix` and level 1 are the parameter names. Returns @@ -833,7 +833,7 @@ def from_series( Parameters ---------- ds : pd.Series - The input Series is parsed in a dict-like manner with indicies as parameter + The input Series is parsed in a dict-like manner with indices as parameter names and series data as values. skip_missing: bool, default False: How to handle cases where ``ds`` does not provide values for all parameters. diff --git a/src/gsolve/core/utils.py b/src/gsolve/core/utils.py index 8caaa24..95f47cb 100644 --- a/src/gsolve/core/utils.py +++ b/src/gsolve/core/utils.py @@ -597,7 +597,7 @@ def timestamp_to_columns( elif round_method == "ceil": ds = ds.dt.ceil(res) else: - msg_0 = f"unreconised rounding method '{round_method}'" + msg_0 = f"unrecognized rounding method '{round_method}'" raise ValueError(msg_0) df = pd.DataFrame( @@ -728,7 +728,7 @@ def expand_datetime_column( prefixes = list(prefix) if is_list_like(prefix) else [prefix] if len(prefixes) != len(cols_to_split): msg = ( - f"inconsisitent 'column_name' and 'prefix' arg lengths: " + f"inconsistent 'column_name' and 'prefix' arg lengths: " f"{len(cols_to_split)} != {len(prefixes)}" ) raise ValueError(msg) diff --git a/src/gsolve/core/xr_accessor.py b/src/gsolve/core/xr_accessor.py index 410e997..3dafa11 100644 --- a/src/gsolve/core/xr_accessor.py +++ b/src/gsolve/core/xr_accessor.py @@ -254,8 +254,8 @@ def generate_bathymetry_density( Generate a bathymetry density grid from a boolean mask grid or DEM. The output density DataArray can be used in calculating bathymetric terrain - corrections. Bathymetry cells are assigned a density of - ``terrain_density - water_density``. Density in topography cells is set to 0.0. + corrections. Bathymety cells are assigned a density of + terrain_density - water_density. Density in topography cells is set to 0.0. Parameters ---------- diff --git a/src/gsolve/gsolve_outputs.py b/src/gsolve/gsolve_outputs.py index e816388..a15716a 100644 --- a/src/gsolve/gsolve_outputs.py +++ b/src/gsolve/gsolve_outputs.py @@ -132,7 +132,16 @@ class GSolveResults: The final 'absolute_gravity' solution for each site after adjustment, with solution statistics. - """ + Parameters + ---------- + method : {1, 2, 3} + The gsolve algorithm used. + use_loops: bool + If loops were used in the solution. + calculate_calibration_factor : bool + If solution solved for gravity meter calibration factor. + percentile_clipping: float + The percentile clip applied. """ # ruff: ignore[incorrect-section-order] @@ -272,7 +281,7 @@ def plot_residual_cdf( unit_label = "mGal" precision = ".04f" else: - msg = f"unrecgnised unit '{unit}'. Must be 'mGal' or 'uGal'" + msg = f"unrecognized unit '{unit}'. Must be 'mGal' or 'uGal'" raise ValueError(msg) df_loops: list[str] = [str(l) for l in df["loop"].unique()] @@ -407,7 +416,7 @@ def plot_residual_drift( unit_label = "mGal" precision = ".04f" if unit_label is None: - msg = f"unrecgnised unit '{unit}'. Must be 'mGal' or 'uGal'" + msg = f"unrecognized unit '{unit}'. Must be 'mGal' or 'uGal'" raise ValueError(msg) x = df[x_col].to_numpy() diff --git a/src/gsolve/observations.py b/src/gsolve/observations.py index b19eda7..6ac47b1 100644 --- a/src/gsolve/observations.py +++ b/src/gsolve/observations.py @@ -377,7 +377,7 @@ def _default_index_generator(self) -> pd.Index: site_id_tstamp_labels = ( self.data["site_id"].astype(str).str.cat(tstamps, sep=".") ) - new_idx = pd.Index(siteid_tstamp_labels, name=self._index_field, dtype=str) + new_idx = pd.Index(site_id_tstamp_labels, name=self._index_field, dtype=str) return self._index_deduplicator(new_idx) @staticmethod @@ -1843,7 +1843,7 @@ def solve_calibration_factor( meter_ids = self.observations.data["meter_id"].unique() if len(meter_ids) > 1: msg = ( - "Calibration factor can only be calulated for a single instrument. " + "Calibration factor can only be calculated for a single instrument. " f"Observations include data from {len(meter_ids)} meter_id's = {meter_ids}" ) raise ValueError(msg) diff --git a/src/gsolve/reductions/corrections.py b/src/gsolve/reductions/corrections.py index 7755c07..02ae9d2 100644 --- a/src/gsolve/reductions/corrections.py +++ b/src/gsolve/reductions/corrections.py @@ -796,7 +796,7 @@ def compute( c for c in corrs if c not in self.available_corrections() ] if has_bad_corrections: - msg = f"Unrecognised corrections: {has_bad_corrections}" + msg = f"Unrecognized corrections: {has_bad_corrections}" raise ValueError(msg) else: corrs = self.params.bouguer_correction_fields() diff --git a/src/gsolve/reductions/terrain_corrections.py b/src/gsolve/reductions/terrain_corrections.py index 26a0b3c..472a83c 100644 --- a/src/gsolve/reductions/terrain_corrections.py +++ b/src/gsolve/reductions/terrain_corrections.py @@ -509,7 +509,7 @@ def __post_init__(self) -> None: def __setattr__(self, name: str, value: Any) -> None: # ruff: ignore[any-type] fieldnames = [] if name not in (n.name for n in dataclasses.fields(self)): - msg = f"unrecopgnised field name {name}" + msg = f"unrecognized field name {name}" raise ValueError(msg) if name in { @@ -547,7 +547,7 @@ def _sanity_check(self, if_errors: Literal["warn", "error"] = "error") -> None: Parameters ---------- if_errors : {"warn", "error"}, default "error" - How to handle anry validation errors. If ``'error'``, then raise + How to handle any validation errors. If ``'error'``, then raise an exception. If ``'warn'``, issue a warning and continue checking. """ throw_error: bool = if_errors == "error" @@ -938,7 +938,7 @@ def compute( params=None, ) - nan_error_desciption_displayed = False + nan_error_description_displayed = False for zone in self.zones: pars = self.params[zone].copy() @@ -1011,8 +1011,8 @@ def compute( f"{indent}Warning: zone '{zone}': terrain corrections " f"not calculated for {n_missing_tc} of {len(x)} sites." ) - if not nan_error_desciption_displayed: - nan_error_desciption_displayed = True + if not nan_error_description_displayed: + nan_error_description_displayed = True sys.stderr.write( f"{indent} This is probably due to:", ) @@ -1460,7 +1460,7 @@ def from_dataframe( for c in df.columns if c.startswith("tcorr:") and c not in consumed_columns ] - if uncomsumed_columns: + if unconsumed_columns: msg = ( "terrain corrections parameters and values are inconsistent: " f"no parameters for terrain_correction data {unconsumed_columns}" @@ -1741,7 +1741,7 @@ def get_corrections( raise ValueError(err_msg) if if_missing == "drop": - warnings.warn(f"{err_msg}, dropping from ouput") + warnings.warn(f"{err_msg}, dropping from output") return tcorrs.loc[site_id_found, cols] warnings.warn(f"{err_msg}, filling with {fill_value}") diff --git a/src/gsolve/reports.py b/src/gsolve/reports.py index b135c13..4fb13cf 100644 --- a/src/gsolve/reports.py +++ b/src/gsolve/reports.py @@ -413,7 +413,7 @@ def to_excel( all_params = [] # This is a kludge - should create method on parameter objects to - # to normalise parameter outputs for writing to excel. + # to normalize parameter outputs for writing to excel. def _format_value(x: Any) -> str | float | int | bool: # ruff: ignore[any-type] if isinstance(x, pd.Timedelta): return x.total_seconds() diff --git a/src/gsolve/tide/earth_tide.py b/src/gsolve/tide/earth_tide.py index 7287ad0..65d01f0 100644 --- a/src/gsolve/tide/earth_tide.py +++ b/src/gsolve/tide/earth_tide.py @@ -938,7 +938,7 @@ def default( return cls.from_array([freq_start, freq_stop, amplitude_factor, phase_lead]) def pygtide_wavegroup_arg(self) -> NDArray[np.float64]: - """Return a copy of the paramaters as ndarray. + """Return a copy of the parameters as ndarray. The returned array is intended to be used as the argument to ``pygtide.set_wavegroup()`` method. @@ -1176,9 +1176,9 @@ def time_series( msg = "No results returned from pygtide prediction." raise ValueError(msg) - normalised_cols = ["datetime", "signal", "tide", "pole_tide", "lod_tide"] + normalized_cols = ["datetime", "signal", "tide", "pole_tide", "lod_tide"] tides_df = tides_df.rename( - columns=dict(zip(tides_df.columns, normalised_cols, strict=True)) + columns=dict(zip(tides_df.columns, normalized_cols, strict=True)) ) tides_df["datetime"] = to_naive_utc_datetime(tides_df["datetime"]) tides_df = tides_df.set_index("datetime") @@ -1232,7 +1232,7 @@ def tidal_correction( Corrections are computed by: 1. for each unique site_id, - 2. generate a time series of tidal corrections covering the obsevarvation + 2. generate a time series of tidal corrections covering the observation times for that site, 3. linearly interpolate tidal corrections at the exact observation times. """ diff --git a/src/gsolve/tide/ocean_load.py b/src/gsolve/tide/ocean_load.py index 26674f4..1854b25 100644 --- a/src/gsolve/tide/ocean_load.py +++ b/src/gsolve/tide/ocean_load.py @@ -575,7 +575,7 @@ def read_qtp_multistation(file_path: FilePath) -> pd.DataFrame: ) if df.shape[1] != len(column_definitions): - msg = f"Format error reading '{file_path}': not QTP multiistation ocean load format?" + msg = f"Format error reading '{file_path}': not QTP multistation ocean load format?" raise ValueError(msg) if df.isna().any(axis=None): @@ -727,7 +727,7 @@ def _get_model_parameters(self, f: FilePath) -> None: metadata["ocean_tide_model"] = l.split(":", 1)[1].strip() elif l.startswith("$$ CMC"): v = l.split(":", 1)[1].strip().split()[0] - matadata["center_mass_correction"] = v != "NO" + metadata["center_mass_correction"] = v != "NO" elif l.startswith("$$ END HEADER:"): break self.metadata.update(metadata) From 4a1c67a85020079aaf754338925ad8366afb1b33 Mon Sep 17 00:00:00 2001 From: Adrian Benson Date: Tue, 22 Sep 2026 13:32:01 +1200 Subject: [PATCH 12/36] Add newline ate file end --- prek.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/prek.toml b/prek.toml index 7c7e002..099c42e 100644 --- a/prek.toml +++ b/prek.toml @@ -6,9 +6,6 @@ hooks = [ { id = "check-added-large-files" }, { id = "check-case-conflict" }, { id = "detect-private-key" }, - { id = "mixed-line-ending", args = [ - "--fix=lf", - ] }, ] [[repos]] From a12e6bbc50873226829abd608b70379f27a1e553 Mon Sep 17 00:00:00 2001 From: Adrian Benson Date: Tue, 22 Sep 2026 13:33:59 +1200 Subject: [PATCH 13/36] Update for excel_file arg --- src/gsolve/core/data.py | 4 ++-- src/gsolve/reports.py | 2 +- src/gsolve/scintrex.py | 16 +++++++++------- src/gsolve/tide/earth_tide.py | 2 +- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/gsolve/core/data.py b/src/gsolve/core/data.py index 1af6f15..30eef12 100644 --- a/src/gsolve/core/data.py +++ b/src/gsolve/core/data.py @@ -936,8 +936,8 @@ def to_excel( raise ValueError(msg) write_excel_worksheet( - prepare_writable_df(params_ds.to_frame(), normalize_column_names=True), - fname, + df=prepare_writable_df(params_ds.to_frame(), normalize_column_names=True), + excel_file=fname, sheet_name=sheet_name, if_workbook_exists=if_workbook_exists, if_sheet_exists=if_sheet_exists, diff --git a/src/gsolve/reports.py b/src/gsolve/reports.py index 4fb13cf..2c42a2e 100644 --- a/src/gsolve/reports.py +++ b/src/gsolve/reports.py @@ -436,7 +436,7 @@ def _format_value(x: Any) -> str | float | int | bool: # ruff: ignore[any-type] write_excel_worksheet( df=pd.concat(all_params), - filename=filename, + excel_file=filename, sheet_name="metadata", if_workbook_exists="append", if_sheet_exists=if_sheet_exists, diff --git a/src/gsolve/scintrex.py b/src/gsolve/scintrex.py index a51eb0c..f055a34 100644 --- a/src/gsolve/scintrex.py +++ b/src/gsolve/scintrex.py @@ -48,6 +48,8 @@ _ScintrexMetadataDataTypes: TypeAlias = str | float | int | bool | pd.Timestamp +type _SCINTREX_ON_ERROR_OPTIONS = Literal["raise", "warn", "ignore"] + class ScintrexData(abc.ABC): """Base class for classes that read and manipulate Scintrex data.""" @@ -242,7 +244,7 @@ def __init__( metadata: dict[str, _ScintrexMetadataDataTypes], metadata_units: dict[str, str] | None = None, loop_from_line: bool = False, - on_error: _ScintrexOnErrorOptions = "warn", + on_error: _SCINTREX_ON_ERROR_OPTIONS = "warn", ) -> None: super().__init__(data, metadata, metadata_units, on_error) @@ -273,7 +275,7 @@ def _set_metadata( def _set_data(self, data: pd.DataFrame, on_error: str = "raise") -> None: """Set data attribute.""" - if not is_in_literal(on_error, _ScintrexOnErrorOptions): + if not is_in_literal(on_error, _SCINTREX_ON_ERROR_OPTIONS): msg = f"invalid on_error arg {on_error}" raise ValueError(msg) @@ -287,14 +289,10 @@ def _set_data(self, data: pd.DataFrame, on_error: str = "raise") -> None: try: df[c] = df[c].astype(this_dtype) except Exception as err_read: - if on_error not in {"warn", "ignore"}: + if on_error == "raise": msg = f"error converting data in column '{c}' to {this_dtype}" raise TypeError(msg) from err_read - if on_error == "warn": - warnings.warn( - f"bad data encountered in column '{c}', setting to nan" - ) try: df[c] = ( df[c] @@ -304,6 +302,10 @@ def _set_data(self, data: pd.DataFrame, on_error: str = "raise") -> None: except Exception as err_cant_replace: msg = f"unfixable error converting data in column '{c}' to {this_dtype}" raise TypeError(msg) from err_cant_replace + else: + if on_error == "warn": + msg = f"bad data encountered in column '{c}', setting to nan" + warnings.warn(msg) if ( "datetime" not in df.columns diff --git a/src/gsolve/tide/earth_tide.py b/src/gsolve/tide/earth_tide.py index 65d01f0..4489baf 100644 --- a/src/gsolve/tide/earth_tide.py +++ b/src/gsolve/tide/earth_tide.py @@ -1255,7 +1255,7 @@ def tidal_correction( raise ValueError(msg) if isinstance(site_id, str): site_id = [site_id] * lat.size - site_id = to_1d_ndarray(site_id, expected_size=lat.size, dtype=float) + site_id = to_1d_ndarray(site_id, expected_size=lat.size, dtype=str) corrs = np.full_like(lat, np.nan) From 172377901ad1107127e56e3c80ffa7f804317bf3 Mon Sep 17 00:00:00 2001 From: Adrian Benson Date: Tue, 22 Sep 2026 13:48:29 +1200 Subject: [PATCH 14/36] Ensure checks only touch python. --- prek.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/prek.toml b/prek.toml index 099c42e..ba72978 100644 --- a/prek.toml +++ b/prek.toml @@ -1,8 +1,8 @@ [[repos]] repo = "builtin" hooks = [ - { id = "trailing-whitespace" }, - { id = "end-of-file-fixer" }, + { id = "trailing-whitespace", files = "\\.py$" }, + { id = "end-of-file-fixer", files = "\\.py$" }, { id = "check-added-large-files" }, { id = "check-case-conflict" }, { id = "detect-private-key" }, From 67a818453c9644a18d84feeb87c51a858688dfb8 Mon Sep 17 00:00:00 2001 From: Adrian Benson Date: Wed, 23 Sep 2026 13:11:38 +1200 Subject: [PATCH 15/36] Spell checking applied to tests --- cspell.json | 4 +- project-words.txt | 141 ++++++++++++++++++ tests/test_observations.py | 4 +- tests/test_reports.py | 4 +- tests/test_scintrex.py | 8 +- tests/test_terrain_corrections_consistency.py | 8 +- tests/test_tide/test_ocean_load.py | 1 + 7 files changed, 155 insertions(+), 15 deletions(-) diff --git a/cspell.json b/cspell.json index 1453755..c261852 100644 --- a/cspell.json +++ b/cspell.json @@ -1,6 +1,6 @@ { "version": "0.2", - "language": "en", + "language": "en,en-au", "dictionaryDefinitions": [ { "name": "project-words", @@ -11,4 +11,4 @@ "dictionaries": [ "project-words" ] -} +} \ No newline at end of file diff --git a/project-words.txt b/project-words.txt index adf28b5..221e80d 100644 --- a/project-words.txt +++ b/project-words.txt @@ -1,15 +1,156 @@ +afactor +alisonk +amtruncate +anom +arccos +arcsin +arctan +argnames +argvalues +asanyarray +atleast +auxfuncs +Bartels +basedpyright +bathy +bathymetric +Bouguer +Bullard +contextily +corrgrav +corrs +craigm +cumcount +deduplicator +detided +driftcorr +earthtide ecdf +elevgps +elevuser +Elsevier +emsg +errmsg +Eterna +ETGTAB +etide +Fatiando +Fehr +figsize +filt fontsize fout +Frese +gcal +gcorr +gfactor +Gotze +Götze +grav +gref +gsolve +gsolvetable +HARDISP +Hayford +Heiskanen +Hinze +Hofmann +horizontalalignment +IERS +ifac +ifactor +instrheight +interp isactive +isel +isscalar +issubdtype +jcentury +Kudryavtsev +Lacoste +Lakshmanan +lamda +latgps +latuser linalg +lmbda +lodtidecor +Longman +longps +lonuser +lstsq +Maari markerfmt +mdates +measurdur +mgal +milli +milligal +milligals +Moritz +multistation +ndigits +nlevels +nparam +olmpp +otide perc +pgtide pinv +poletidecor +pydatetime +pygtide +pytest +quicktide +rawgrav rcond regen +Renamer +rioxarray +Rongotai +rtol +rval +Saad +samprate +savetxt +sbcc +Schureman +SCINTREX +sensortemp setdiff +skipna +Somigliana +spher +Stagpoole +startdate +Tamura +tcorr +tcorrs +tdelta +temaari +tempcorr +Tenzer +TGKB +tidalcompo +TIDALPARAM +tidalpoten +tidecorr +tiltcorr tstamp +tstamps +ufeff +ugal +uncorr +UNSO +wavegroup +Wellenhof +Wenzel +xcol +xdim xlabel +xticklabels +ycol +ydim ylabel yticks +zcol diff --git a/tests/test_observations.py b/tests/test_observations.py index 30beb91..7bb8775 100644 --- a/tests/test_observations.py +++ b/tests/test_observations.py @@ -97,8 +97,8 @@ def test_gravity_observations_init_has_obs_id(self) -> None: data = dummy_data() # obs_id not specified - obj_unspec = GravityObservations(**data) - assert obj_unspec.data.index.dtype.name in {"object", "str"} + obj_unspecified = GravityObservations(**data) + assert obj_unspecified.data.index.dtype.name in {"object", "str"} prefixes = obj_unspecified.data.index.str.partition(".").get_level_values(0) assert_index_equal( diff --git a/tests/test_reports.py b/tests/test_reports.py index 2acd8a3..aca4e3e 100644 --- a/tests/test_reports.py +++ b/tests/test_reports.py @@ -239,7 +239,7 @@ def test_to_excel_writes_expected_sheets_without_terrain( calls: list[dict[str, Any]] = [] - def _fake_write_excel_worksheet(df, filename, sheet_name, **kwargs): # ruff: ignore[unused-function-argument] + def _fake_write_excel_worksheet(df, excel_file, sheet_name, **kwargs): calls.append( { "excel_file": excel_file, @@ -294,7 +294,7 @@ def test_to_excel_writes_terrain_sheet_when_present( sheet_names: list[str] = [] - def _fake_write_excel_worksheet(df, filename, sheet_name, **_kwargs): # ruff: ignore[unused-function-argument] + def _fake_write_excel_worksheet(df, excel_file, sheet_name, **_kwargs): # ruff: ignore[unused-function-argument] sheet_names.append(sheet_name) monkeypatch.setattr( diff --git a/tests/test_scintrex.py b/tests/test_scintrex.py index f5ea7cb..6c310c4 100644 --- a/tests/test_scintrex.py +++ b/tests/test_scintrex.py @@ -189,10 +189,10 @@ def test_cg6data_set_loop(cg6_file: pathlib.Path) -> None: cg6.set_loop( datetimes=dict(zip(times, ["a", "b"], strict=True)), loop_start=200, - output_column="xloop", + output_column="x_loop", ) - assert cg6.data["xloop"].iloc[0] == "a" - assert cg6.data["xloop"].iloc[i_mid] == "b" + assert cg6.data["x_loop"].iloc[0] == "a" + assert cg6.data["x_loop"].iloc[i_mid] == "b" # 2.3 from a series cg6.set_loop( @@ -212,8 +212,6 @@ def test_cg6data_set_loop(cg6_file: pathlib.Path) -> None: .eq(fmt_str.format(LOOP=301)) .all() ) - assert cg6.data["zloop"].iloc[0] == fstr.format(LOOP=300) - assert cg6.data.loc[cg6.data["line"].eq(2), "zloop"].eq(fstr.format(LOOP=301)).all() # Case X: bad args with pytest.raises(TypeError): diff --git a/tests/test_terrain_corrections_consistency.py b/tests/test_terrain_corrections_consistency.py index f58d44b..b8fd068 100644 --- a/tests/test_terrain_corrections_consistency.py +++ b/tests/test_terrain_corrections_consistency.py @@ -68,7 +68,7 @@ def sites() -> GravitySites: return sites -def pre_calced_tcorr_data() -> TerrainCorrectionData: +def pre_calculated_tcorr_data() -> TerrainCorrectionData: # Output from these test methods as at 09-04-2026 # - These values are not certain to be correct # - Catch changes that alter the results @@ -140,12 +140,12 @@ def test_terrain_correction_consistency(): assert results.data.loc[bad_points, tcorr_cols].isna().all(axis=None) # test that pre-calculated results are close to calculated results - pre_calced_tcorr_data_ = pre_calced_tcorr_data() + pre_calculated_tcorr_data_ = pre_calculated_tcorr_data() for col in tcorr_cols: - assert col in pre_calced_tcorr_data_.data.columns + assert col in pre_calculated_tcorr_data_.data.columns assert np.allclose( results.data[col].to_numpy(), - pre_calced_tcorr_data_.data[col].to_numpy(), + pre_calculated_tcorr_data_.data[col].to_numpy(), atol=1e-6, equal_nan=True, ) diff --git a/tests/test_tide/test_ocean_load.py b/tests/test_tide/test_ocean_load.py index 1fcb5c0..8e8ce8c 100644 --- a/tests/test_tide/test_ocean_load.py +++ b/tests/test_tide/test_ocean_load.py @@ -17,6 +17,7 @@ from pathlib import Path import numpy as np +import numpy.testing as npt import pandas as pd import pytest From ec66ebad3907ccd71accf96500604eb5b32b44ae Mon Sep 17 00:00:00 2001 From: Adrian Benson Date: Thu, 24 Sep 2026 13:10:58 +1200 Subject: [PATCH 16/36] Add pandas stubs and ty as dev dependencies. Fix typos. --- cspell.json | 9 +++++-- docs/source/conf.py | 2 +- project-words.txt | 25 +++++++++++++++++++ pyproject.toml | 61 ++++++++++++++++++++++++++------------------- 4 files changed, 68 insertions(+), 29 deletions(-) diff --git a/cspell.json b/cspell.json index c261852..491a245 100644 --- a/cspell.json +++ b/cspell.json @@ -1,6 +1,6 @@ { "version": "0.2", - "language": "en,en-au", + "language": "en", "dictionaryDefinitions": [ { "name": "project-words", @@ -9,6 +9,11 @@ } ], "dictionaries": [ - "project-words" + "project-words", + "python", + "python-common", + "data-science-tools", + "git", + "en-au" ] } \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index f540cf6..7dda8a6 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -22,7 +22,7 @@ import sys from pathlib import Path -sys.path.insert(0, Path("../../src").resolve()) +sys.path.insert(0, Path.resolve("../../src")) # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information diff --git a/project-words.txt b/project-words.txt index 221e80d..42e7fcc 100644 --- a/project-words.txt +++ b/project-words.txt @@ -1,5 +1,8 @@ +addopts afactor +Aleksandr alisonk +amsmath amtruncate anom arccos @@ -9,6 +12,8 @@ argnames argvalues asanyarray atleast +autoapi +autosummary auxfuncs Bartels basedpyright @@ -16,13 +21,16 @@ bathy bathymetric Bouguer Bullard +burocrata contextily +copybutton corrgrav corrs craigm cumcount deduplicator detided +Docstrings driftcorr earthtide ecdf @@ -34,6 +42,7 @@ errmsg Eterna ETGTAB etide +extlinks Fatiando Fehr figsize @@ -41,6 +50,7 @@ filt fontsize fout Frese +FURB gcal gcorr gfactor @@ -66,7 +76,9 @@ isel isscalar issubdtype jcentury +Kirkby Kudryavtsev +kwargs Lacoste Lakshmanan lamda @@ -81,29 +93,36 @@ lonuser lstsq Maari markerfmt +mathjax mdates measurdur mgal milli milligal milligals +minversion Moritz multistation ndigits nlevels nparam +numpydoc olmpp otide perc pgtide pinv poletidecor +pydata pydatetime +pygrep pygtide pytest +pyupgrade quicktide rawgrav rcond +refurb regen Renamer rioxarray @@ -120,7 +139,9 @@ sensortemp setdiff skipna Somigliana +Spesivtsev spher +stacklevel Stagpoole startdate Tamura @@ -130,6 +151,7 @@ tdelta temaari tempcorr Tenzer +testpaths TGKB tidalcompo TIDALPARAM @@ -138,10 +160,13 @@ tidecorr tiltcorr tstamp tstamps +typehints ufeff ugal uncorr +undoc UNSO +viewcode wavegroup Wellenhof Wenzel diff --git a/pyproject.toml b/pyproject.toml index 8594c9c..23a3f77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -147,39 +147,39 @@ extend-select = [ "UP", # pyupgrade "YTT", # flake8-2020 # "E501", - "PLR0917", + "too-many-positional-arguments", # need to make keyword args using * ] ignore = [ - "ANN003", # alloww unannotated **kwargs - "D105", # allow undoc magic methods - "DOC501", # don't NEED to document exceptions - "D100", # allow unocumented modules - "PLR09", # Too many <...> - "PLR2004", # Magic value used in comparison - "RET504", # Allow variable assignment only for return - "PT001", # Conventions for parenthesis on pytest.fixture - "D200", # Allow single line docstrings in their own line - "B028", # Allow no stacklevel in warnings.warn - "UP040", # Allow old style type alias - will fix later - "PLR6104", # Don't prefer augmented assignment - breaks pandas + "missing-type-kwargs", # AN003 allow unannotated **kwargs + "undocumented-magic-method", # allow undocumented magic methods + "docstring-missing-exception", # don't NEED to document exceptions + "undocumented-public-module", # allow undocumented modules + "PLR09", # Too many branches, args etc + "magic-value-comparison", # Magic value used in comparison + "unnecessary-assign", # Allow variable assignment only for return + "pytest-fixture-incorrect-parentheses-style", # Conventions for parenthesis on pytest.fixture + "unnecessary-multiline-docstring", # Allow single line doc strings in their own line + "no-explicit-stacklevel", # Allow warnings.warn without stacklevel + "non-pep695-type-alias", # Allow old style type alias - will fix later + "non-augmented-assignment", # Don't suggest/enforce augmented assignment (+=, /= etc) - breaks pandas ] [tool.ruff.lint.per-file-ignores] "examples/scripts/**" = [ - "D", # Docs not required for scripts - "DOC", # ditto - "T201", # Allow print() in scripts + "D", # Docs not required for scripts + "DOC", # ditto + "print", # Allow print() in scripts ] "tests/**" = [ - "ANN001", # type annotations not required for func args - "ANN201", # or public func return - "ANN202", # or private func return - "D", # Docstrings not required - "DOC", # Docstrings not required - "PLR6301", # allow unused self in test classess - "PLC2701", # permit import of private functions etc - we are need to test em - "PT011", # allow tests to catch exceptions without a 'match' arg - "PT030", # allow tests to catch warnings without a 'match' arg + "missing-type-function-argument", # type annotations not required for func args + "missing-return-type-undocumented-public-function", # or public func return + "missing-return-type-private-function", # or private func return + "D", # Docstrings not required + "DOC", # Docstrings not required + "no-self-use", # allow unused self in test classes + "import-private-name", # permit import of private functions etc - we are need to test em + "pytest-raises-too-broad", # allow tests to catch exceptions without a 'match' arg + "pytest-warns-too-broad", # allow tests to catch warnings without a 'match' arg ] [tool.ruff.lint.pydocstyle] @@ -215,6 +215,15 @@ exclude = ["__init__.py"] [tool.ty.src] include = ["gsolve/", "examples/scripts/"] - [tool.numpydoc_validation] checks = ["all", "EX01", "SA01"] + +[tool.basedpyright] +exclude = [ + "**/node_modules", + "**/__pycache__", + "**/.venv", + "**/env", + "**/build", + "**/dist", +] From 5340c6d878fcfcc6335adfa7cf53dac3139693c6 Mon Sep 17 00:00:00 2001 From: Adrian Benson Date: Thu, 24 Sep 2026 13:17:10 +1200 Subject: [PATCH 17/36] Refactor and improve code quality across multiple modules - Updated import statements to include new utility functions. - Enhanced readability by breaking long lines and improving variable names. - Replaced instances of `isinstance` checks with more type-safe alternatives. - Corrected typos and improved documentation for clarity. - Ensured consistent naming conventions for variables and parameters. - Refactored logic in various functions to streamline processing and enhance performance. - Added type hints for better type checking and code clarity. - Improved error messages for better debugging experience. --- src/gsolve/core/_typing.py | 20 +- src/gsolve/core/data.py | 305 +-------------------------- src/gsolve/core/excel_io.py | 4 +- src/gsolve/core/utils.py | 10 +- src/gsolve/core/xr_accessor.py | 4 +- src/gsolve/meter_conversion.py | 6 +- src/gsolve/reductions/corrections.py | 67 +++--- src/gsolve/scintrex.py | 10 +- src/gsolve/sites.py | 2 +- src/gsolve/tide/earth_tide.py | 30 ++- src/gsolve/tide/ocean_load.py | 3 +- 11 files changed, 82 insertions(+), 379 deletions(-) diff --git a/src/gsolve/core/_typing.py b/src/gsolve/core/_typing.py index 11a6c04..364525c 100644 --- a/src/gsolve/core/_typing.py +++ b/src/gsolve/core/_typing.py @@ -22,13 +22,13 @@ import datetime from collections.abc import Callable, Hashable, Mapping, Sequence from os import PathLike -from typing import Any, Literal, Protocol, runtime_checkable +from typing import Any, Literal, Protocol, TypeAlias, runtime_checkable import numpy as np import pandas as pd import xarray as xr from numpy.typing import ArrayLike, NDArray -from pandas import DatetimeIndex, Index, Series +from pandas import DataFrame, DatetimeIndex, Index, Series # from pandas.api.typing.aliases import TimedeltaConvertibleTypes @@ -74,20 +74,8 @@ type Renamer = Mapping[Any, Hashable] | Callable[[Any], Hashable] -DateTimeConvertibleTypes: TypeAlias = ( - str - | int - | float - | datetime.timedelta - | list - | tuple - | range - | ArrayLike - | Index - | Series -) -DatetimeScalar: TypeAlias = ( - int | float | str | datetime.date | np.datetime64 | pd.Timestamp +type DateTimeConvertibleTypes = ( + str | int | float | datetime.timedelta | list | tuple | ArrayLike | Index | Series ) type DatetimeScalar = int | float | str | datetime.date | np.datetime64 | pd.Timestamp diff --git a/src/gsolve/core/data.py b/src/gsolve/core/data.py index 30eef12..8796645 100644 --- a/src/gsolve/core/data.py +++ b/src/gsolve/core/data.py @@ -18,13 +18,14 @@ """Base class and function definitions for Gsolve data structures.""" +import abc import copy import dataclasses import warnings from collections.abc import Callable from copy import deepcopy from types import MappingProxyType -from typing import Any, ClassVar, Self +from typing import Any, ClassVar, Protocol, Self import pandas as pd from pandas.api.types import is_bool_dtype, is_string_dtype @@ -118,11 +119,11 @@ def __copy__(self) -> Self: return deepcopy(self) def copy(self) -> Self: - """Return a deep copy of object.""" + """Return a deep copy of object.""" # ruff: ignore[docstring-missing-returns] return copy.copy(self) def to_dict(self) -> dict: - """Return parameters as a dict.""" + """Return parameters as a dict.""" # ruff: ignore[docstring-missing-returns] return dataclasses.asdict(self) def to_series( @@ -211,13 +212,13 @@ def from_series( @classmethod def default_values(cls) -> dict: - """Return dict of default parameter values.""" + """Return dict of default parameter values.""" # ruff: ignore[docstring-missing-returns] return { k: cls.__dataclass_fields__[k].default for k in cls.__dataclass_fields__ } def non_default_values(self) -> dict: - """Return dict of non-default parameter values.""" + """Return dict of non-default parameter values.""" # ruff: ignore[docstring-missing-returns] defaults = self.default_values() return {k: v for k, v in self.to_dict().items() if defaults.get(k, None) != v} @@ -399,11 +400,8 @@ class GSolveTable(_HasKnownFields, abc.ABC): The primary data storage object. """ - _known_fields: ClassVar[MappingProxyType[str, DataFieldSpecification]] - _default_excel_sheet_name: ClassVar[str | tuple[str, ...]] = "" - - def __init__(self) -> None: - self.data: pd.DataFrame + data: pd.DataFrame + params: GSolveParameters | None def __repr__(self) -> str: rval = [] @@ -486,10 +484,10 @@ def set_column( break if dtype == "datetime": - data_ = to_naive_utc_datetime(data) # ty:ignore[no-matching-overload] # pyrefly:ignore + data_ = to_naive_utc_datetime(pd.to_datetime(data)) dtype = None elif dtype == "timedelta": - data_ = pd.to_timedelta(data) # ty:ignore[no-matching-overload] # pyrefly:ignore + data_ = pd.to_timedelta(data) dtype = None elif data is None: if default is not None: @@ -635,7 +633,7 @@ def from_csv( def from_excel( cls, excel_file: FilePath, - sheet_name: str | int | list[str | int] | None = None, + sheet_name: str | int | list[str | int] | tuple[str] | None = None, ignore_unknown_fields: bool = False, parse_split_datetime: bool = True, mapper: Renamer | None = None, @@ -675,7 +673,6 @@ def from_excel( pandas.read_excel : For available ``kwargs`` . pandas.DataFrame.rename : For full details of ``mapper`` argument. """ - sheet_name_: str | int | list[str | int] if sheet_name is None: try: sheet_name_ = cls._default_excel_sheet_name @@ -704,7 +701,6 @@ def write_to_csv( expand_datetime: str | None = None, drop_datetime: bool = False, bool_to_int: bool = False, - # include_unknown_fields: bool = True, **kwargs, ) -> None: """Write data to a csv file. @@ -766,282 +762,3 @@ def write_to_csv( # "'sheet_name' not defined and object has no valid " # "_default_excel_sheet_name attribute" # ) - - -@dataclasses.dataclass -class GSolveParameters: - """Base class to store parameters related to GSolveTable derived classes.""" - - def _param_str(self) -> str: - # Return a string representation of the parameters - return repr(self).partition("(")[2].rpartition(")")[0] - - def __copy__(self) -> Self: - # Ensure all copies are deep copies. - return deepcopy(self) - - def copy(self) -> Self: - """Return a deep copy of object.""" # ruff: ignore[docstring-missing-returns] - return copy.copy(self) - - def to_dict(self) -> dict: - """Return parameters as a dict.""" # ruff: ignore[docstring-missing-returns] - return dataclasses.asdict(self) - - def to_series( - self, - series_name: str | None = None, - index_name: str | None = None, - index_prefix: str | None = None, - ) -> pd.Series: - """Return parameters as a Series with parameter names as the index. - - Parameters - ---------- - series_name : str | None, default is None - The series data field name. - index_name : str | None, default is None - The series index name. - index_prefix : str | None, optional - Create a multiindex where level 0 is 'index_prefix` and level 1 are - the parameter names. - - Returns - ------- - Series - - """ - ds = pd.Series(data=self.to_dict(), name=series_name).rename_axis(index_name) - if index_prefix: - ds.index = ds.index = pd.MultiIndex.from_arrays( - arrays=([index_prefix] * ds.shape[0], ds.index), - ) - if index_name is not None: - ds = ds.rename_axis(["", index_name]) - - return ds - - @classmethod - def from_series( - cls, - ds: pd.Series, - skip_missing: bool = False, - skip_unknown_parameters: bool = False, - ) -> Self: - """Generate a GsolveParameters object from a pandas.Series. - - Parameters - ---------- - ds : pd.Series - The input Series is parsed in a dict-like manner with indices as parameter - names and series data as values. - skip_missing: bool, default False: - How to handle cases where ``ds`` does not provide values for all parameters. - If False, raise a TypeError exception. If True and the missing parameters - have default values, create the object with default values. Parameters - without a default value must always be defined in the input series. - skip_unknown_parameters : bool, default False - If False, raise a TypeError if ``ds`` contains indices that do not match - known parameters. If True, silently ignore unknown parameters - - Returns - ------- - GSolveParameters - - """ - ds = ds.copy() - if ds.index.nlevels > 1: - msg = "MultiIndex series not supported." - raise ValueError(msg) - - args: dict[str, Any] = { - str(k): v for k, v in ds.items() if k in cls.__dataclass_fields__ - } - - missing_args = [k for k in cls.__dataclass_fields__ if k not in args] - if not skip_missing and missing_args: - msg = f"skip_missing=False: missing required parameters: {missing_args}" - raise TypeError(msg) - - extra_args = [k for k in ds.index if k not in cls.__dataclass_fields__] - if extra_args and not skip_unknown_parameters: - msg = ( - f"series contains unknown parameters: {ds.index[extra_args].to_list()}" - ) - raise TypeError(msg) - - return cls(**args) - - @classmethod - def default_values(cls) -> dict: - """Return dict of default parameter values.""" # ruff: ignore[docstring-missing-returns] - return { - k: cls.__dataclass_fields__[k].default for k in cls.__dataclass_fields__ - } - - def non_default_values(self) -> dict: - """Return dict of non-default parameter values.""" # ruff: ignore[docstring-missing-returns] - defaults = self.default_values() - return {k: v for k, v in self.to_dict().items() if defaults.get(k, None) != v} - - def to_excel( - self, - fname: FilePath, - sheet_name: str | None = None, - *, - if_workbook_exists: IfWorkbookExists = "error", - if_sheet_exists: IfSheetExists = "error", - parameter_name_label: str = "parameter", - parameter_value_label: str = "value", - **kwargs, - ) -> None: - """Write parameters to an Excel worksheet. - - Parameters - ---------- - fname : str or PathLike - The path to the output Excel file. - sheet_name : str - The name of the excel worksheet to write terrain corrections. - if_workbook_exists : {'error', 'append', 'replace'}, default 'error' - Action to take if the workbook already exists. Options are: - 'error', 'append', or 'replace'. - if_sheet_exists : {'error', 'replace', 'new'}, default 'error' - Action to take if the sheet already exists. Options are: - 'error', 'replace', or 'new'. - parameters_label : str, default is 'parameter' - Set the header label for parameter names column in output worksheet. - values_label : str, default is 'value' - Set the header label for parameter names column in output worksheet. - kwargs : dict - Additional keyword arguments passed to ``pandas.DataFrame.to_excel``. - - See Also - -------- - write_excel_worksheet : Function to write a DataFrame to an Excel worksheet - with options for handling existing workbooks and sheets. - pandas.Dataframe.to_excel - - """ - params_ds = self.to_series( - index_name=parameter_name_label, series_name=parameter_value_label - ) - if sheet_name is None: - sheet_name = getattr(self, "_default_excel_sheet_name", None) - if sheet_name is None: - msg = ( - "sheet_name is None and object has no " - "_default_excel_sheet_name attribute." - ) - raise ValueError(msg) - - write_excel_worksheet( - df=prepare_writable_df(params_ds.to_frame(), normalize_column_names=True), - excel_file=fname, - sheet_name=sheet_name, - if_workbook_exists=if_workbook_exists, - if_sheet_exists=if_sheet_exists, - **kwargs, - ) - - # Todo: remove this method - def summary( - self, include_name: bool = True, as_list: bool = True - ) -> list[str] | str: - """ - Return parameters as strings in the form 'param: value'. - - Parameters - ---------- - include_name : bool, default True - Include the class name in the output. - as_list : bool, default True - Return the output as a list of strings. If False, return as a single string - with each parameter on a new line. - - Returns - ------- - list[str] | str - The parameters as a string or list of strings. - """ - txt = [] - if include_name: - txt.append(f"{type(self).__name__}") - txt.extend([f"{k}: {v}" for k, v in self.to_dict().items()]) - if as_list: - return txt - return "\n".join(txt) - - -def _concat_gsolvetable_dataframes_with_fill( - df1: pd.DataFrame, - df2: pd.DataFrame, - fill_str: str | None = "", - fill_bool: bool | None = None, - known_fields: dict[str, Any] | None = None, - **kwargs, -) -> pd.DataFrame: - - if kwargs.get("axis", 0) != 0: - msg = ( - f"incompatible kwarg axis={kwargs['axis']}, " - "function operates in vstack (axis=0) mode only." - ) - raise ValueError(msg) - kwargs["axis"] = 0 - - use_known_fields = False - if known_fields is not None: - use_known_fields = True - if not all(hasattr(f, "default") for f in known_fields.values()): - msg = "if specified, known_fields must be a dict of DataFieldSpecification objects" - raise TypeError(msg) - - do_str_fill = fill_str is not None - if do_str_fill: - fill_str = str(fill_str) - - do_bool_fill = fill_bool is not None - if do_bool_fill: - fill_bool = bool(fill_bool) - - combined_df = pd.concat([df1, df2], **kwargs) - if not use_known_fields and not do_str_fill and not do_bool_fill: - return combined_df - - in_df1_only = [c for c in df1.columns if c not in df2.columns] - idx = df2.index - for c in in_df1_only: - if ( - use_known_fields - and c in known_fields - and known_fields[c].default is not None - ): - combined_df.loc[idx, c] = combined_df.loc[idx, c].fillna( - known_fields[c].default - ) - continue - if do_str_fill and is_string_dtype(df1[c]): - combined_df.loc[idx, c] = combined_df.loc[idx, c].fillna(fill_str) - elif do_bool_fill and is_bool_dtype(df1[c]): - combined_df.loc[idx, c] = combined_df.loc[idx, c].fillna(fill_bool) - - in_df2_only = [c for c in df2.columns if c not in df1.columns] - idx = df1.index - for c in in_df2_only: - if ( - use_known_fields - and c in known_fields - and known_fields[c].default is not None - ): - combined_df.loc[idx, c] = combined_df.loc[idx, c].fillna( - known_fields[c].default - ) - continue - if do_str_fill and is_string_dtype(df2[c]): - combined_df.loc[idx, c] = combined_df.loc[idx, c].fillna(fill_str) - continue - if do_bool_fill and is_bool_dtype(df2[c]): - combined_df.loc[idx, c] = combined_df.loc[idx, c].fillna(fill_bool) - - return combined_df diff --git a/src/gsolve/core/excel_io.py b/src/gsolve/core/excel_io.py index 5c31a80..4c146be 100644 --- a/src/gsolve/core/excel_io.py +++ b/src/gsolve/core/excel_io.py @@ -218,14 +218,14 @@ def write_excel_worksheet( pandas.ExcelWriter """ - if if_workbook_exists not in get_args(IfWorkbookExists): + if not is_in_literal(if_workbook_exists, IfWorkbookExists): msg = ( f"invalid value for {if_workbook_exists=}, must be one of " f"{get_args(IfWorkbookExists)}" ) raise ValueError(msg) - if if_sheet_exists not in get_args(IfSheetExists): + if not is_in_literal(if_sheet_exists, IfSheetExists): msg = ( f"invalid value for {if_sheet_exists=}, must be one of " f"{get_args(IfSheetExists)}" diff --git a/src/gsolve/core/utils.py b/src/gsolve/core/utils.py index 95f47cb..f2b5677 100644 --- a/src/gsolve/core/utils.py +++ b/src/gsolve/core/utils.py @@ -183,16 +183,14 @@ def to_naive_utc_datetime( @overload -def to_naive_utc_datetime( - t: pd.Series, allow_nat: bool, **kwargs: bool | str | DatetimeScalar -) -> pd.Series: ... +def to_naive_utc_datetime(t: pd.Series, allow_nat: bool, **kwargs) -> pd.Series: ... @overload def to_naive_utc_datetime( t: list | tuple | NDArray | pd.Index | pd.DatetimeIndex, allow_nat: bool, - **kwargs: bool | str | DatetimeScalar, + **kwargs, ) -> pd.DatetimeIndex: ... @@ -287,7 +285,7 @@ def to_1d_ndarray( expected_size: int | None = None, extend_len_1_array: bool = False, dtype: DTypeLike | None = None, -) -> NDArray: +) -> NDArray[np.float64]: a = np.atleast_1d(a) if a.ndim > 1: a = np.squeeze(a) @@ -1095,7 +1093,7 @@ def dms2rad( return np.deg2rad(deg) -def _convert_single_timestamp_arg( +def convert_single_timestamp_arg( t: DatetimeScalar, allow_nat: bool = False, err_prefix: str | None = None, **kwargs ) -> pd.Timestamp: diff --git a/src/gsolve/core/xr_accessor.py b/src/gsolve/core/xr_accessor.py index 3dafa11..410e997 100644 --- a/src/gsolve/core/xr_accessor.py +++ b/src/gsolve/core/xr_accessor.py @@ -254,8 +254,8 @@ def generate_bathymetry_density( Generate a bathymetry density grid from a boolean mask grid or DEM. The output density DataArray can be used in calculating bathymetric terrain - corrections. Bathymety cells are assigned a density of - terrain_density - water_density. Density in topography cells is set to 0.0. + corrections. Bathymetry cells are assigned a density of + ``terrain_density - water_density``. Density in topography cells is set to 0.0. Parameters ---------- diff --git a/src/gsolve/meter_conversion.py b/src/gsolve/meter_conversion.py index c974168..a77a184 100644 --- a/src/gsolve/meter_conversion.py +++ b/src/gsolve/meter_conversion.py @@ -238,7 +238,7 @@ def set_datetime_range( msg = f"Error setting starttime: {e}" raise ValueError(msg) from None else: - msg = f"invalid starttime type {type(starttime)}. Should be datetimelike or None." + msg = f"invalid starttime type {type(starttime)}. Should be datetime-like or None." raise TypeError(msg) if endtime is pd.NaT or endtime is None: @@ -250,9 +250,7 @@ def set_datetime_range( msg = f"Error setting endtime: {e}" raise ValueError(msg) from None else: - msg = ( - f"invalid endtime type {type(endtime)}. Should be datetimelike or None." - ) + msg = f"invalid endtime type {type(endtime)}. Should be datetime-like or None." raise TypeError(msg) if st is not None and et is not None and st >= et: diff --git a/src/gsolve/reductions/corrections.py b/src/gsolve/reductions/corrections.py index 02ae9d2..5c9c98f 100644 --- a/src/gsolve/reductions/corrections.py +++ b/src/gsolve/reductions/corrections.py @@ -21,7 +21,7 @@ from collections.abc import Sequence from dataclasses import dataclass from types import MappingProxyType -from typing import Literal +from typing import ClassVar, Literal, cast import boule import numpy as np @@ -601,36 +601,41 @@ class GravityCorrections(GSolveTable): Parameters used to compute the gravity corrections. """ - _known_fields: MappingProxyType[str, DataFieldSpecification] = MappingProxyType( - { - "site_id": DataFieldSpecification("site_id", str, required=True), - "longitude": DataFieldSpecification("longitude", float, required=False), - "latitude": DataFieldSpecification("latitude", float, required=False), - "height_ellipsoidal": DataFieldSpecification( - "height_ellipsoidal", float, required=False, legacy_name="height" - ), - "normal_gravity_at_stn_elevation": DataFieldSpecification( - "normal_gravity_at_stn_elevation", float, required=False, default=np.nan - ), - "normal_gravity_at_ellipsoid": DataFieldSpecification( - "normal_gravity_at_ellipsoid", float, required=False, default=np.nan - ), - "free_air_correction": DataFieldSpecification( - "free_air_correction", float, required=False, default=np.nan - ), - "bouguer_slab_correction": DataFieldSpecification( - "bouguer_slab_correction", float, required=False, default=np.nan - ), - "bouguer_slab_curvature_corrected": DataFieldSpecification( - "bouguer_slab_curvature_corrected", - float, - required=False, - default=np.nan, - ), - "atmospheric_correction": DataFieldSpecification( - "atmospheric_correction", float, required=False, default=np.nan - ), - } + _known_fields: ClassVar[MappingProxyType[str, DataFieldSpecification]] = ( + MappingProxyType( + { + "site_id": DataFieldSpecification("site_id", str, required=True), + "longitude": DataFieldSpecification("longitude", float, required=False), + "latitude": DataFieldSpecification("latitude", float, required=False), + "height_ellipsoidal": DataFieldSpecification( + "height_ellipsoidal", float, required=False, legacy_name="height" + ), + "normal_gravity_at_stn_elevation": DataFieldSpecification( + "normal_gravity_at_stn_elevation", + float, + required=False, + default=np.nan, + ), + "normal_gravity_at_ellipsoid": DataFieldSpecification( + "normal_gravity_at_ellipsoid", float, required=False, default=np.nan + ), + "free_air_correction": DataFieldSpecification( + "free_air_correction", float, required=False, default=np.nan + ), + "bouguer_slab_correction": DataFieldSpecification( + "bouguer_slab_correction", float, required=False, default=np.nan + ), + "bouguer_slab_curvature_corrected": DataFieldSpecification( + "bouguer_slab_curvature_corrected", + float, + required=False, + default=np.nan, + ), + "atmospheric_correction": DataFieldSpecification( + "atmospheric_correction", float, required=False, default=np.nan + ), + } + ) ) data: pd.DataFrame diff --git a/src/gsolve/scintrex.py b/src/gsolve/scintrex.py index f055a34..7f2b2c1 100644 --- a/src/gsolve/scintrex.py +++ b/src/gsolve/scintrex.py @@ -322,9 +322,9 @@ def _set_data(self, data: pd.DataFrame, on_error: str = "raise") -> None: df.insert(i_date_col, "datetime", dt) # ty:ignore[invalid-argument-type] df = df.drop(columns=["date", "time"]) - corr_flag_colname = "corrections[drift-temp-na-tide-tilt]" - if corr_flag_colname in df.columns: - flag_labels = corr_flag_colname.rstrip("]").rpartition("[")[-1].split("-") + corr_flag_col_name = "corrections[drift-temp-na-tide-tilt]" + if corr_flag_col_name in df.columns: + flag_labels = corr_flag_col_name.rstrip("]").rpartition("[")[-1].split("-") flag_labels = [f"correction_{f}" for f in flag_labels] flags = df.pop(corr_flag_col_name).str.split("").str[1:-1].to_list() @@ -525,8 +525,8 @@ def set_loop( ) raise ValueError(msg) if dates[-1] < self.data["datetime"].max(): - tmax = self.data["datetime"].max() + pd.Timedelta(seconds=1) - dates = pd.DatetimeIndex([*dates.to_list(), tmax]) + t_max = self.data["datetime"].max() + pd.Timedelta(seconds=1) + dates = pd.DatetimeIndex([*dates.to_list(), t_max]) loop_intervals = generate_loop_intervals(dates) loop_namer = pd.Series(loop_ids, index=loop_intervals) diff --git a/src/gsolve/sites.py b/src/gsolve/sites.py index bb7ef42..2dc5711 100644 --- a/src/gsolve/sites.py +++ b/src/gsolve/sites.py @@ -594,7 +594,7 @@ def sample_elevation( """ if is_filepath_like(dem): dem = load_dem(dem) - elif isinstance(dem, DatasetOrArray): + elif isinstance(dem, DatasetOrArray.__value__): dem = prepare_dem(dem) else: msg = "dem must be file path or an xarray Dataset/DataArray" diff --git a/src/gsolve/tide/earth_tide.py b/src/gsolve/tide/earth_tide.py index 4489baf..c111644 100644 --- a/src/gsolve/tide/earth_tide.py +++ b/src/gsolve/tide/earth_tide.py @@ -20,7 +20,7 @@ import dataclasses import warnings -from typing import Any, Literal, Protocol, Self, overload, runtime_checkable +from typing import Any, Literal, Protocol, Self, cast, overload, runtime_checkable import numpy as np import pandas as pd @@ -40,7 +40,7 @@ ) from gsolve.core.utils import ( GSolveDataWarning, - _convert_single_timestamp_arg, + convert_single_timestamp_arg, dms2rad, to_1d_ndarray, to_naive_utc_datetime, @@ -73,7 +73,7 @@ def identifier(self, **kwargs) -> str: ... # ruff: ignore[undocumented-public-m def gravimetric_factor( - k2: float = 0.2980, h2: float = 0.6032 + k2: float | FloatArray = 0.2980, h2: float | FloatArray = 0.6032 ) -> np.float64 | NDArray[np.float64]: """Compute gravimetric factor from Love numbers ``k2`` and ``h2``. @@ -96,7 +96,7 @@ def gravimetric_factor( .. [1] Agnew, D. C. (2007). 3.06 Earth Tides. In Treatise on Geophysics (pp. 163-195). Elsevier. https://doi.org/10.1016/B978-044452748-6.00056-0 """ - h2 = to_1d_ndarray(h2).astype(float) + h2 = to_1d_ndarray(h2, dtype=float).astype(float) k2 = to_1d_ndarray(k2, expected_size=h2.size).astype(float) gfactor = 1 + h2 - 1.5 * k2 return gfactor[0] if gfactor.size == 1 else gfactor @@ -135,7 +135,8 @@ class LongmanConstants: c1: float = 1.495983e13 # Mean distance between centers earth-sun (cm) e: float = 0.054900489 # Eccentricity of the moon's orbit i: float = round( - deg2rad(5.145), ndigits=9 + deg2rad(5.145, dtype=float), + ndigits=9, ) # = 0.08979719 Inclination of moon's orbit to the ecliptic m: float = 0.074804 # Ratio of mean motion of the sun to that of the moon mu: float = 6.67428e-08 # Newton's gravitational constant, 6.670e-8 in orig. @@ -474,14 +475,10 @@ def time_series( msg = f"error parsing step: {e}" raise ValueError(msg) from e - if not isinstance(step, pd.Timedelta): - msg = "step must be a valid timedelta or timedelta string." - raise TypeError(msg) - - t0 = _convert_single_timestamp_arg( + t0 = convert_single_timestamp_arg( starttime, allow_nat=False, err_prefix="error parsing starttime" ) - t1 = _convert_single_timestamp_arg( + t1 = convert_single_timestamp_arg( endtime, allow_nat=False, err_prefix="error parsing endtime" ) @@ -494,7 +491,7 @@ def time_series( lon_arr = np.full(len(t_idx), lon) elev_array = np.full(len(t_idx), elev) if method == "correction": - tseries = pd.Series( + time_series = pd.Series( data=self.tidal_correction( site_id=None, lat=lat_arr, @@ -507,7 +504,7 @@ def time_series( ) elif method == "acceleration": a, b = self.gravity_accelerations(lat_arr, lon_arr, elev_array, t_idx) - tseries = pd.Series( + time_series = pd.Series( data=a + b, index=t_idx, name=method, @@ -1118,8 +1115,9 @@ def time_series( DataFrame DataFrame containing the tidal corrections. """ - unit = unit.lower() - if unit not in {"mgal", "ugal", "nm/s^2"}: + if unit in {"mgal", "ugal", "nm/s^2"}: + unit = cast(Literal["mgal", "ugal", "nm/s^2"], unit.lower()) + else: msg = f"invalid unit value '{unit}'" raise ValueError(msg) @@ -1257,7 +1255,7 @@ def tidal_correction( site_id = [site_id] * lat.size site_id = to_1d_ndarray(site_id, expected_size=lat.size, dtype=str) - corrs = np.full_like(lat, np.nan) + corrs = np.full_like(lat, np.nan, dtype=np.float64) for site in np.unique(site_id): site_mask = site_id == site diff --git a/src/gsolve/tide/ocean_load.py b/src/gsolve/tide/ocean_load.py index 1854b25..94e25fb 100644 --- a/src/gsolve/tide/ocean_load.py +++ b/src/gsolve/tide/ocean_load.py @@ -136,7 +136,7 @@ def __init__( ) -> None: if isinstance(site_id, str): site_id = np.array([site_id] * len(date_time)) - site_id = np.atleast_1d(site_id).astype(str) + site_id = to_1d_ndarray(site_id, dtype=str) if site_id.ndim != 1: msg = "site_id argument must be 1-dimensional." raise ValueError(msg) @@ -637,7 +637,6 @@ def generate_qtp_input( # ruff: ignore[too-many-positional-arguments] if not (site_id.size == datetimes.size == lat.size == lon.size == elevation.size): msg = "site_id, datetimes, latitude, longitude, and elevation arguments must all have the same shape." raise ValueError(msg) - raise ValueError(msg) # initial data frame with station IDs and datetimes qtp_df = pd.DataFrame( From 70789f109b21336e6ff9d811917591cfec5db8cc Mon Sep 17 00:00:00 2001 From: Adrian Benson Date: Thu, 24 Sep 2026 13:24:23 +1200 Subject: [PATCH 18/36] strip out TypeAlias hints, replace with type = --- src/gsolve/scintrex.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/gsolve/scintrex.py b/src/gsolve/scintrex.py index 7f2b2c1..29bbcee 100644 --- a/src/gsolve/scintrex.py +++ b/src/gsolve/scintrex.py @@ -21,7 +21,7 @@ import warnings from collections.abc import Callable, Mapping, Sequence from types import MappingProxyType -from typing import Literal, Self, TypeAlias +from typing import Literal, Self import numpy as np import numpy.typing as npt @@ -46,9 +46,9 @@ __all__ = ["CG6Data", "ScintrexData"] -_ScintrexMetadataDataTypes: TypeAlias = str | float | int | bool | pd.Timestamp +type _ScintrexMetadataDataTypes = str | float | int | bool | pd.Timestamp -type _SCINTREX_ON_ERROR_OPTIONS = Literal["raise", "warn", "ignore"] +type _ScintrexOnErrorOptions = Literal["raise", "warn", "ignore"] class ScintrexData(abc.ABC): @@ -244,7 +244,7 @@ def __init__( metadata: dict[str, _ScintrexMetadataDataTypes], metadata_units: dict[str, str] | None = None, loop_from_line: bool = False, - on_error: _SCINTREX_ON_ERROR_OPTIONS = "warn", + on_error: _ScintrexOnErrorOptions = "warn", ) -> None: super().__init__(data, metadata, metadata_units, on_error) @@ -275,7 +275,7 @@ def _set_metadata( def _set_data(self, data: pd.DataFrame, on_error: str = "raise") -> None: """Set data attribute.""" - if not is_in_literal(on_error, _SCINTREX_ON_ERROR_OPTIONS): + if not is_in_literal(on_error, _ScintrexOnErrorOptions): msg = f"invalid on_error arg {on_error}" raise ValueError(msg) From a0746974cd9b2ea64affc6690a1f9ee9cff31ebc Mon Sep 17 00:00:00 2001 From: Adrian Benson Date: Fri, 25 Sep 2026 00:49:05 +1200 Subject: [PATCH 19/36] Add numpy and matplotlib as direct dependencies. Required because they are explicitly imported. --- pyproject.toml | 89 ++++++++++++++++++++++++++------------------------ 1 file changed, 47 insertions(+), 42 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 23a3f77..0d4ada5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -122,31 +122,30 @@ line-length = 88 select = ["ANN", "D", "DOC", "PD", "NPY"] preview = true extend-select = [ - "ARG", # flake8-unused-arguments - "B", # flake8-bugbear - "C4", # flake8-comprehensions - "D", # pydocstyle - "EM", # flake8-errmsg - "EXE", # flake8-executable - "FURB", # refurb - "G", # flake8-logging-format - "I", # isort - "ICN", # flake8-import-conventions - "NPY", # NumPy specific rules - "PD", # pandas-vet - "PGH", # pygrep-hooks - "PIE", # flake8-pie - "PL", # pylint - "PT", # flake8-pytest-style - "PTH", # flake8-use-pathlib - "PYI", # flake8-pyi - "RET", # flake8-return - "RUF", # Ruff-specific - "SIM", # flake8-simplify - "T20", # flake8-print - "UP", # pyupgrade - "YTT", # flake8-2020 - # "E501", + "ARG", # flake8-unused-arguments + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "D", # pydocstyle + "EM", # flake8-errmsg + "EXE", # flake8-executable + "FURB", # refurb + "G", # flake8-logging-format + "I", # isort + "ICN", # flake8-import-conventions + "NPY", # NumPy specific rules + "PD", # pandas-vet + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PL", # pylint + "PT", # flake8-pytest-style + "PTH", # flake8-use-pathlib + "PYI", # flake8-pyi + "RET", # flake8-return + "RUF", # Ruff-specific + "SIM", # flake8-simplify + "T20", # flake8-print + "UP", # pyupgrade + "YTT", # flake8-2020 "too-many-positional-arguments", # need to make keyword args using * ] ignore = [ @@ -191,6 +190,29 @@ ignore-overlong-task-comments = true [tool.ruff.lint.flake8-annotations] allow-star-arg-any = true +[tool.ty.src] +include = ["gsolve/", "examples/scripts/"] + +[tool.ty.rules] +# Strict type safety defaults with mild allowances for edge cases +# all = "error" +possibly-unresolved-reference = "ignore" +division-by-zero = "ignore" + +[tool.numpydoc_validation] +checks = ["all", "EX01", "SA01"] + +[tool.basedpyright] +exclude = [ + "**/node_modules", + "**/__pycache__", + "**/.venv", + "**/env", + "**/build", + "**/dist", +] + + [tool.burocrata] notice = ''' # gSolve - gravity processing software. @@ -210,20 +232,3 @@ notice = ''' # SPDX-License-Identifier: GPLv3 ''' exclude = ["__init__.py"] - - -[tool.ty.src] -include = ["gsolve/", "examples/scripts/"] - -[tool.numpydoc_validation] -checks = ["all", "EX01", "SA01"] - -[tool.basedpyright] -exclude = [ - "**/node_modules", - "**/__pycache__", - "**/.venv", - "**/env", - "**/build", - "**/dist", -] From e5b3047675e443cf7e44d6abb31c19e3b14f430d Mon Sep 17 00:00:00 2001 From: Adrian Benson Date: Fri, 25 Sep 2026 00:50:22 +1200 Subject: [PATCH 20/36] Fix type errors --- examples/scripts/okataina_survey.py | 2 +- src/gsolve/core/utils.py | 23 +++++++------- src/gsolve/gsolve_outputs.py | 26 +++++---------- src/gsolve/observations.py | 19 ++++++----- src/gsolve/reductions/anomalies.py | 1 + src/gsolve/reductions/corrections.py | 1 + src/gsolve/reductions/terrain_corrections.py | 33 ++++++++++++++------ src/gsolve/reports.py | 7 +++-- src/gsolve/scintrex.py | 3 +- src/gsolve/sites.py | 7 ++--- src/gsolve/tide/ocean_load.py | 19 ++++++----- tests/test_corrections.py | 4 +-- tests/test_data_structures.py | 16 +++++----- tests/test_observations.py | 2 +- tests/test_rongotai_survey.py | 2 +- 15 files changed, 90 insertions(+), 75 deletions(-) diff --git a/examples/scripts/okataina_survey.py b/examples/scripts/okataina_survey.py index 9abe2c0..588ffc1 100644 --- a/examples/scripts/okataina_survey.py +++ b/examples/scripts/okataina_survey.py @@ -18,7 +18,7 @@ import pathlib try: - import contextily as cx # type:ignore[unresolved-import] # ty: ignore[unresolved-import] + import contextily as cx # ty:ignore[unresolved-import] except ImportError: has_contextily = False cx = None diff --git a/src/gsolve/core/utils.py b/src/gsolve/core/utils.py index f2b5677..019f242 100644 --- a/src/gsolve/core/utils.py +++ b/src/gsolve/core/utils.py @@ -183,14 +183,16 @@ def to_naive_utc_datetime( @overload -def to_naive_utc_datetime(t: pd.Series, allow_nat: bool, **kwargs) -> pd.Series: ... +def to_naive_utc_datetime( + t: pd.Series, allow_nat: bool, **kwargs: bool | str | DatetimeScalar +) -> pd.Series: ... @overload def to_naive_utc_datetime( t: list | tuple | NDArray | pd.Index | pd.DatetimeIndex, allow_nat: bool, - **kwargs, + **kwargs: bool | str | DatetimeScalar, ) -> pd.DatetimeIndex: ... @@ -718,10 +720,8 @@ def expand_datetime_column( raise ValueError(msg) if prefix is None or not prefix: - if len(cols_to_split) > 1: # ruff: ignore[if-else-block-instead-of-if-exp] - prefixes = [f"{n}_" for n in cols_to_split] - else: - prefixes = [""] + prefixes = [f"{n}_" for n in cols_to_split] if len(cols_to_split) > 1 else [""] + else: prefixes = list(prefix) if is_list_like(prefix) else [prefix] if len(prefixes) != len(cols_to_split): @@ -951,15 +951,16 @@ def identify_loop_blocks( if not dt.is_monotonic_increasing: msg = "datetimes must be sorted in increasing order." raise ValueError(msg) - # if isinstance(_datetimes, pd.DatetimeIndex): - # _datetimes = _datetimes.to_series() + gap = pd.to_timedelta(gap) gaps = dt.diff().gt(gap) one_sec = pd.Timedelta("1s") - gap_bounds: list[pd.Timestamp] = ( - [dt.iloc[0] - one_sec] + dt.loc[gaps].to_list() + [dt.iloc[-1] + one_sec] # ruff: ignore[collection-literal-concatenation] - ) + gap_bounds: list[pd.Timestamp] = [ + dt.iloc[0] - one_sec, + *dt.loc[gaps].to_list(), + dt.iloc[-1] + one_sec, + ] if as_intervals: return pd.IntervalIndex.from_tuples( list(itertools.pairwise(gap_bounds)), closed="left" diff --git a/src/gsolve/gsolve_outputs.py b/src/gsolve/gsolve_outputs.py index a15716a..02ef1cd 100644 --- a/src/gsolve/gsolve_outputs.py +++ b/src/gsolve/gsolve_outputs.py @@ -132,18 +132,14 @@ class GSolveResults: The final 'absolute_gravity' solution for each site after adjustment, with solution statistics. - Parameters - ---------- - method : {1, 2, 3} - The gsolve algorithm used. - use_loops: bool - If loops were used in the solution. - calculate_calibration_factor : bool - If solution solved for gravity meter calibration factor. - percentile_clipping: float - The percentile clip applied. + """ - """ # ruff: ignore[incorrect-section-order] + obs_solution: pd.DataFrame + site_solution: pd.DataFrame + loop_solution: pd.DataFrame + observations_input: pd.DataFrame + reference_sites_input: pd.DataFrame + params: GSolveSolutionParameters def __init__( self, @@ -159,12 +155,6 @@ def __init__( calculate_calibration_factor=calculate_calibration_factor, ) - self.obs_solution: pd.DataFrame - self.site_solution: pd.DataFrame - self.loop_solution: pd.DataFrame - self.observations_input: pd.DataFrame - self.reference_sites_input: pd.DataFrame - def set_inputs(self, obs: pd.DataFrame, ref_sites: pd.DataFrame) -> None: """Add input data used in the gsolve run.""" self.observations_input = obs.copy() @@ -398,7 +388,7 @@ def plot_residual_drift( x_col: str = "timedelta" y_col: str = "residual" - drift = float(self.loop_solution.at[loop, "drift"]) # type: ignore[bad-argument-type] # ruff: ignore[pandas-use-of-dot-at] + drift = float(self.loop_solution.loc[loop, "drift"]) m_loop = self.obs_solution["loop"].eq(loop) m_active = self.obs_solution["active"].eq(True) diff --git a/src/gsolve/observations.py b/src/gsolve/observations.py index 6ac47b1..b6c3c1d 100644 --- a/src/gsolve/observations.py +++ b/src/gsolve/observations.py @@ -819,10 +819,6 @@ def set_calibration_factor( msg = "Multiple gravity meters found in data, must specify ``meter_id``" raise ValueError(msg) - if self.data["meter_id"].nunique() > 1 and meter_id is None: # ruff: ignore[pandas-nunique-constant-series-check] - msg = "Multiple gravity meters found in data, must specify ``meter_id``" - raise ValueError(msg) - if meter_id is None: self.set_column(c_label, float(calibration_factor)) else: @@ -983,9 +979,7 @@ def _activate_deactivate( ------ ValueError If any specified ``obs_id``, ``site_id`` or ``loop`` values are not found in the data. - TypeError - If any input is not a string or iterable of strings. - """ # ruff: ignore[docstring-extraneous-exception] + """ def _parse_inputs(o: str | Iterable[str] | None) -> list[str]: if o is None: @@ -1037,7 +1031,12 @@ def _get_writable_df( include_unknown_fields: bool | Sequence[str] = True, active_only: bool = False, ) -> pd.DataFrame: - """Return a DataFrame suitable for writing to an excel or csv file.""" # ruff: ignore[docstring-missing-returns] + """Return a DataFrame suitable for writing to an excel or csv file. + + Returns + ------- + Dataframe + """ cols = [c for c in self.known_fields() if c in self.data.columns] if include_unknown_fields: if include_unknown_fields is True: @@ -1484,8 +1483,8 @@ def check_data(self, warn: bool = True) -> bool: if "loop" in self.data.columns and "meter_id" in self.data.columns: for l in self.loop_ids: - m = self.data["loop"].eq(l) - if self.data.loc[m, "meter_id"].nunique() > 1: # ruff: ignore[pandas-nunique-constant-series-check] + m = self.data["loop", self.data["loop"].eq(l)].to_numpy() + if not (m.shape[0] == 0 or (m[0] == m).all()): warner(f"Multiple gravity meters found in loop '{l}'") warner.final_msg() diff --git a/src/gsolve/reductions/anomalies.py b/src/gsolve/reductions/anomalies.py index d3fb0f0..404cc35 100644 --- a/src/gsolve/reductions/anomalies.py +++ b/src/gsolve/reductions/anomalies.py @@ -392,6 +392,7 @@ class GravityAnomalies(GSolveTable): ), } ) + _default_excel_sheet_name: str = "gravity_anomalies" def __init__( self, diff --git a/src/gsolve/reductions/corrections.py b/src/gsolve/reductions/corrections.py index 5c9c98f..72f2dc6 100644 --- a/src/gsolve/reductions/corrections.py +++ b/src/gsolve/reductions/corrections.py @@ -637,6 +637,7 @@ class GravityCorrections(GSolveTable): } ) ) + _default_excel_sheet_name: str = "gravity_corrections" data: pd.DataFrame params: GravityCorrectionParameters diff --git a/src/gsolve/reductions/terrain_corrections.py b/src/gsolve/reductions/terrain_corrections.py index 472a83c..58fd84a 100644 --- a/src/gsolve/reductions/terrain_corrections.py +++ b/src/gsolve/reductions/terrain_corrections.py @@ -568,7 +568,9 @@ def warn_(m: str) -> None: "0.0 <= min_dist < max_dist: " f"got min_dist={self.min_dist}, max_dist={self.max_dist}" ) - raise ValueError(msg) if throw_error else warn_(msg) + if throw_error: + raise ValueError(msg) + warn_(msg) # check distance msk type is valid if not is_in_literal(self.distance_mask_type, TCorrDistanceMaskType): @@ -576,7 +578,9 @@ def warn_(m: str) -> None: f"invalid 'distance_mask_type': {self.distance_mask_type}. " f"Expected one of: {get_args(TCorrDistanceMaskType.__value__)}" ) - raise ValueError(msg) if throw_error else warn_(msg) + if throw_error: + raise ValueError(msg) + warn_(msg) # check dem_source if not _is_dataarray(self.dem_source) and not is_filepath_like(self.dem_source): @@ -584,22 +588,31 @@ def warn_(m: str) -> None: f"invalid dem_source: must be an xarray.DataArray or file path" f", not {type(self.dem_source).__name__}" ) - raise TypeError(msg) if throw_error else warn_(msg) + if throw_error: + raise ValueError(msg) + warn_(msg) if _is_dataarray(self.dem_source): if not self.dem_source.tcorr.is_valid_dem: msg = "invalid dem_source DataArray: must be a 2D array of floats" - raise ValueError(msg) if throw_error else warn_(msg) + if throw_error: + raise ValueError(msg) + warn_(msg) + elif is_filepath_like(self.dem_source): if not self.dem_source: msg = "invalid dem_source file-path like" - raise ValueError(msg) if throw_error else warn_(msg) + if throw_error: + raise ValueError(msg) + warn_(msg) else: msg = ( f"invalid dem_source: must be an xarray.DataArray or file-path like" f", not {type(self.dem_source).__name__}" ) - raise TypeError(msg) if throw_error else warn_(msg) + if throw_error: + raise ValueError(msg) + warn_(msg) # check density_dataset_source if _is_dataarray(self.density_dataset_source): @@ -608,17 +621,19 @@ def warn_(m: str) -> None: "invalid density_dataset_source DataArray: " "must be a 2D array of floats" ) - raise ValueError(msg) if throw_error else warn_(msg) + if throw_error: + raise ValueError(msg) + warn_(msg) + elif is_filepath_like(self.density_dataset_source): if not self.density_dataset_source: msg = "" elif self.density_dataset_source is not None: msg = ( - "invalid density_dataset_source type: should be a file-path like, " + "invalid density_dataset_source: expected file-path like, " f"DataArray or None, not {type(self.density_dataset_source).__name__}" ) raise TypeError(msg) - # if throw_error else warn_(msg) def to_series( self, diff --git a/src/gsolve/reports.py b/src/gsolve/reports.py index 2c42a2e..af99a6a 100644 --- a/src/gsolve/reports.py +++ b/src/gsolve/reports.py @@ -16,7 +16,8 @@ # Copyright (c) 2025 Earth Sciences New Zealand. -from copy import deepcopy + +import copy from pathlib import Path from typing import Any, Self @@ -126,11 +127,11 @@ def __init__( def copy(self) -> Self: """Return a deep copy.""" # ruff: ignore[docstring-missing-returns] - return self.__copy__() # ruff: ignore[unnecessary-dunder-call] + return copy.copy(self) def __copy__(self) -> Self: """Return a deep copy.""" # ruff: ignore[docstring-missing-returns] - return deepcopy(self) + return copy.deepcopy(self) def _set_params( self, diff --git a/src/gsolve/scintrex.py b/src/gsolve/scintrex.py index 29bbcee..333ec32 100644 --- a/src/gsolve/scintrex.py +++ b/src/gsolve/scintrex.py @@ -17,6 +17,7 @@ # Copyright (c) 2025 Earth Sciences New Zealand. import abc +import copy import pathlib import warnings from collections.abc import Callable, Mapping, Sequence @@ -124,7 +125,7 @@ def copy(self) -> Self: ------- ScintrexData """ - return self.__copy__() # ruff: ignore[unnecessary-dunder-call] + return copy(self) class CG6Data(ScintrexData): diff --git a/src/gsolve/sites.py b/src/gsolve/sites.py index 2dc5711..4481d3b 100644 --- a/src/gsolve/sites.py +++ b/src/gsolve/sites.py @@ -603,7 +603,7 @@ def sample_elevation( z = ( dem.interp( {dem.dims[0]: self.data[ycol], dem.dims[1]: self.data[xcol]}, - method=method, # type: ignore[invalid-argument-type, ty:invalid-argument-type] + method=method, ) .to_numpy() .diagonal() @@ -767,9 +767,8 @@ def __init__( raise ValueError(msg) # catch empty site_id - if (m := idx.isna() | (idx == "")).any(): # ruff: ignore[compare-to-empty-string] - empty = pd.Series(m) - empty = empty.loc[m.tolist()].index.to_list() + if (m := idx.isna() | idx.eq("")).any(): + empty = pd.Series(m).loc[m.tolist()].index.to_list() msg = ( "creating ReferenceGravity object: " f"site_id field contains empty values at rows: {empty}" diff --git a/src/gsolve/tide/ocean_load.py b/src/gsolve/tide/ocean_load.py index 94e25fb..947f4c5 100644 --- a/src/gsolve/tide/ocean_load.py +++ b/src/gsolve/tide/ocean_load.py @@ -18,7 +18,6 @@ """Methods and classes for reading and applying ocean load corrections to gravity data.""" -import pathlib import warnings from pathlib import Path from typing import Any, Literal, Protocol, runtime_checkable @@ -459,7 +458,7 @@ def qtp_to_corrector( if corr_type == "auto": # determine file type by reading first line - with pathlib.Path(file_path).open("r", encoding="iso-8859-1") as f: + with Path(file_path).open("r", encoding="iso-8859-1") as f: first_line = f.readline() if first_line.strip().startswith("Year DOY Time"): corr_type = "timeseries" @@ -506,7 +505,7 @@ def read_qtp_timeseries(file_path: FilePath) -> pd.DataFrame: if not all(col in df.columns for col in expected_columns): msg = f"Format error reading '{file_path}': expected columns {expected_columns} not found, not QTP timeseries format?" raise ValueError(msg) - if df.isna().any(axis=None): + if bool(df.isna().any(axis=None)): msg = f"Missing values detected while reading '{file_path}': not QTP timeseries format?" raise ValueError(msg) @@ -649,7 +648,13 @@ def generate_qtp_input( # ruff: ignore[too-many-positional-arguments] }, ) - if qtp_df["Elevation"].isna().any(): + # wrap to [-180, 180] + def _wrap_180(x: float) -> float: + return (x + 180.0) % 360 - 180.0 + + qtp_df["Longitude"] = qtp_df["Longitude"].apply(_wrap_180) + + if any(qtp_df["Elevation"].isna()): msg = "Some site elevations are missing and no 'fill_elevation' was specified." raise ValueError(msg) @@ -717,7 +722,7 @@ def _get_model_parameters(self, f: FilePath) -> None: "ocean_tide_model": "", "center_mass_correction": False, } - with pathlib.Path(f).open() as fh: # ruff: ignore[unspecified-encoding] + with Path(f).open() as fh: # ruff: ignore[unspecified-encoding] model_txt = [l.strip() for l in fh if l.startswith("$$")] for l in model_txt: if l.startswith("$$ Greens function:"): @@ -726,7 +731,7 @@ def _get_model_parameters(self, f: FilePath) -> None: metadata["ocean_tide_model"] = l.split(":", 1)[1].strip() elif l.startswith("$$ CMC"): v = l.split(":", 1)[1].strip().split()[0] - metadata["center_mass_correction"] = v != "NO" + metadata["center_mass_correction"] = v.upper() != "NO" elif l.startswith("$$ END HEADER:"): break self.metadata.update(metadata) @@ -753,7 +758,7 @@ def ocean_load_correction( len( bad_site_ids := [str(s) for s in uniq_site_id if s not in self.stations] ) - != 0 + == 0 ): msg = ( f"site_id(s) {bad_site_ids} not found in station loading model. " diff --git a/tests/test_corrections.py b/tests/test_corrections.py index 8b0b4bc..c8c22ba 100644 --- a/tests/test_corrections.py +++ b/tests/test_corrections.py @@ -320,8 +320,8 @@ class TestGravityCorrectionParameters: def test_defaults(self): p = GravityCorrectionParameters() assert p.ellipsoid == "GRS80" - assert p.density_crust == 2670.0 # ruff: ignore[float-equality-comparison] - assert p.density_water == 1030.0 # ruff: ignore[float-equality-comparison] + assert np.isclose(p.density_crust, 2670.0) + assert np.isclose(p.density_water, 1030.0) assert p.use_curvature_corrected is True assert p.use_atmospheric_correction is True diff --git a/tests/test_data_structures.py b/tests/test_data_structures.py index 592c800..9d0839f 100644 --- a/tests/test_data_structures.py +++ b/tests/test_data_structures.py @@ -32,12 +32,14 @@ @pytest.fixture def gsolve_table_subclass(): class TestClass(data.GSolveTable): - _known_fields = { # ruff: ignore[mutable-class-default] - "a": data.DataFieldSpecification("a", str, "", True), - "b": data.DataFieldSpecification("b", float, 0.0, True), - "c": data.DataFieldSpecification("c", "datetime", pd.NaT, False), - "d": data.DataFieldSpecification("d", bool, False, False), - } + _known_fields = MappingProxyType( + { + "a": data.DataFieldSpecification("a", str, "", True), + "b": data.DataFieldSpecification("b", float, 0.0, True), + "c": data.DataFieldSpecification("c", "datetime", pd.NaT, False), + "d": data.DataFieldSpecification("d", bool, False, False), + } + ) _index_field = "a" return TestClass @@ -57,7 +59,7 @@ def test_data_field_specification(self): fs = data.DataFieldSpecification("longitude", float, 0.0) assert fs.name == "longitude" assert fs.dtype == float - assert fs.default == 0.0 # ruff: ignore[float-equality-comparison] + assert np.isclose(fs.default, 0.0) assert fs.required is False # def test_data_field_specification_convert(self): diff --git a/tests/test_observations.py b/tests/test_observations.py index 7bb8775..5eb652f 100644 --- a/tests/test_observations.py +++ b/tests/test_observations.py @@ -263,7 +263,7 @@ def test_gravity_observations_tdelta(self) -> None: for loop in obj3.loop_ids: m = obj3.data["loop"].eq(loop).to_list() - assert obj3.data[m]["loop_tdelta"].min() == 0.0 # ruff: ignore[float-equality-comparison] + assert np.isclose(obj3.data[m]["loop_tdelta"].min(), 0.0) def test_gravity_observations_timedelta_unit( self, diff --git a/tests/test_rongotai_survey.py b/tests/test_rongotai_survey.py index 3dc17b1..a42b958 100644 --- a/tests/test_rongotai_survey.py +++ b/tests/test_rongotai_survey.py @@ -53,7 +53,7 @@ def test_rongotai_network_adjustment(shared_datadir: pathlib.Path) -> None: for method in [1, 2, 3]: for percentile_clipping in [95, 100]: # reference site solution file - suffix = "_ci%1i" % percentile_clipping # ruff: ignore[printf-string-formatting] + suffix = f"_ci{percentile_clipping:1d}" site_solution_file = data_path / ( f"RIG_G106_site_solution_method{method}{suffix}.csv" ) From 999801538fc818f2feea9e8032eb0dbe95f9190f Mon Sep 17 00:00:00 2001 From: Adrian Benson Date: Fri, 25 Sep 2026 01:02:04 +1200 Subject: [PATCH 21/36] Spell checked docs. --- project-words.txt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/project-words.txt b/project-words.txt index 42e7fcc..a006a95 100644 --- a/project-words.txt +++ b/project-words.txt @@ -1,5 +1,6 @@ addopts afactor +airgap Aleksandr alisonk amsmath @@ -13,12 +14,14 @@ argvalues asanyarray atleast autoapi +automodule autosummary auxfuncs Bartels basedpyright bathy bathymetric +boldsymbol Bouguer Bullard burocrata @@ -27,17 +30,21 @@ copybutton corrgrav corrs craigm +Cubbine cumcount +currentmodule deduplicator detided Docstrings driftcorr +DSIR earthtide ecdf elevgps elevuser Elsevier emsg +EPSG errmsg Eterna ETGTAB @@ -53,6 +60,7 @@ Frese FURB gcal gcorr +GDAL gfactor Gotze Götze @@ -103,7 +111,9 @@ milligals minversion Moritz multistation +ndarray ndigits +NIWA nlevels nparam numpydoc @@ -158,6 +168,8 @@ TIDALPARAM tidalpoten tidecorr tiltcorr +toctree +Tontini tstamp tstamps typehints @@ -167,6 +179,7 @@ uncorr undoc UNSO viewcode +vmatrix wavegroup Wellenhof Wenzel From b81184540aa70f3ae93384c8e68b219a1890e1c7 Mon Sep 17 00:00:00 2001 From: Adrian Benson Date: Fri, 25 Sep 2026 02:16:47 +1200 Subject: [PATCH 22/36] Change ruff rules to default, and make final changes. --- examples/scripts/okataina_survey.py | 2 +- pyproject.toml | 2 +- src/gsolve/__main__.py | 2 +- src/gsolve/core/_typing.py | 4 ++-- src/gsolve/core/data.py | 22 +++++++++---------- src/gsolve/core/utils.py | 14 +++++------- src/gsolve/observations.py | 15 +++++-------- src/gsolve/reductions/corrections.py | 4 ++-- src/gsolve/reductions/terrain_corrections.py | 13 +++++------ src/gsolve/reports.py | 12 +++++----- src/gsolve/scintrex.py | 12 +++++----- src/gsolve/sites.py | 8 +++---- src/gsolve/tide/earth_tide.py | 14 ++++++------ src/gsolve/tide/ocean_load.py | 8 +++---- tests/test_anomalies.py | 3 --- tests/test_terrain_corrections_consistency.py | 2 -- tests/test_tide/test_ocean_load.py | 1 - 17 files changed, 61 insertions(+), 77 deletions(-) diff --git a/examples/scripts/okataina_survey.py b/examples/scripts/okataina_survey.py index 588ffc1..d8f3097 100644 --- a/examples/scripts/okataina_survey.py +++ b/examples/scripts/okataina_survey.py @@ -122,7 +122,7 @@ qtp_output_file = ocean_load_path / "okataina_qtp_input_Modified.csv" if not qtp_output_file.exists(): - msg = f"You didn't run QuickTide Pro yet did you?" + msg = "You didn't run QuickTide Pro yet did you?" raise FileNotFoundError(msg) qtp_ocean_load_corrector = qtp_to_corrector( diff --git a/pyproject.toml b/pyproject.toml index 0d4ada5..2e2682f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -119,7 +119,7 @@ line-length = 88 # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. # Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or # McCabe complexity (`C901`) by default. -select = ["ANN", "D", "DOC", "PD", "NPY"] +# select = ["ANN", "D", "DOC", "PD", "NPY"] preview = true extend-select = [ "ARG", # flake8-unused-arguments diff --git a/src/gsolve/__main__.py b/src/gsolve/__main__.py index d278510..724be72 100644 --- a/src/gsolve/__main__.py +++ b/src/gsolve/__main__.py @@ -182,7 +182,7 @@ def main() -> None: # ruff: ignore[undocumented-public-function] try: args = parse_args() processing(args) - except Exception as e: + except Exception as e: # ruff: ignore[blind-except] sys.stderr.write(e + "\n") diff --git a/src/gsolve/core/_typing.py b/src/gsolve/core/_typing.py index 364525c..52fdda3 100644 --- a/src/gsolve/core/_typing.py +++ b/src/gsolve/core/_typing.py @@ -22,13 +22,13 @@ import datetime from collections.abc import Callable, Hashable, Mapping, Sequence from os import PathLike -from typing import Any, Literal, Protocol, TypeAlias, runtime_checkable +from typing import Any, Literal, Protocol, runtime_checkable import numpy as np import pandas as pd import xarray as xr from numpy.typing import ArrayLike, NDArray -from pandas import DataFrame, DatetimeIndex, Index, Series +from pandas import DatetimeIndex, Index, Series # from pandas.api.typing.aliases import TimedeltaConvertibleTypes diff --git a/src/gsolve/core/data.py b/src/gsolve/core/data.py index 8796645..5f17407 100644 --- a/src/gsolve/core/data.py +++ b/src/gsolve/core/data.py @@ -119,11 +119,11 @@ def __copy__(self) -> Self: return deepcopy(self) def copy(self) -> Self: - """Return a deep copy of object.""" # ruff: ignore[docstring-missing-returns] + """Return a deep copy of object.""" return copy.copy(self) def to_dict(self) -> dict: - """Return parameters as a dict.""" # ruff: ignore[docstring-missing-returns] + """Return parameters as a dict.""" return dataclasses.asdict(self) def to_series( @@ -212,13 +212,13 @@ def from_series( @classmethod def default_values(cls) -> dict: - """Return dict of default parameter values.""" # ruff: ignore[docstring-missing-returns] + """Return dict of default parameter values.""" return { k: cls.__dataclass_fields__[k].default for k in cls.__dataclass_fields__ } def non_default_values(self) -> dict: - """Return dict of non-default parameter values.""" # ruff: ignore[docstring-missing-returns] + """Return dict of non-default parameter values.""" defaults = self.default_values() return {k: v for k, v in self.to_dict().items() if defaults.get(k, None) != v} @@ -425,22 +425,22 @@ def __len__(self) -> int: return len(self.data) if self else 0 def __copy__(self) -> Self: - """Ensure all copies are deep copies.""" # ruff: ignore[docstring-missing-returns] + """Ensure all copies are deep copies.""" return deepcopy(self) def copy(self) -> Self: - """Return a deep copy of object.""" # ruff: ignore[docstring-missing-returns] + """Return a deep copy of object.""" return deepcopy(self) @classmethod def known_fields(cls) -> list[str]: - """Return a list of known fields in the object.""" # ruff: ignore[docstring-missing-returns] + """Return a list of known fields in the object.""" fields = [str(k) for k in getattr(cls, "_known_fields", {})] return fields @classmethod def required_fields(cls) -> list[str]: - """Return a list of required fields in the object.""" # ruff: ignore[docstring-missing-returns] + """Return a list of required fields in the object.""" if cls.known_fields(): return [k for k, v in cls._known_fields.items() if v.required] return [] @@ -448,8 +448,8 @@ def required_fields(cls) -> list[str]: def set_column( self, label: str, - data: Any | None = None, # ruff: ignore[any-type] - default: Any | None = None, # ruff: ignore[any-type] + data: Any | None = None, + default: Any | None = None, dtype: str | type | None = None, ) -> None: """ @@ -501,7 +501,7 @@ def set_column( self.data[label] = pd.Series(data=data_, index=self.data.index, dtype=dtype) def _data_ok(self, warn: bool = True) -> bool: - """Test whether data are complete according to specifications in ``obj._known_fields``.""" # ruff: ignore[docstring-missing-returns] + """Test whether data are complete according to specifications in ``obj._known_fields``.""" rval = True for f in self.required_fields(): if f not in self.data.columns: diff --git a/src/gsolve/core/utils.py b/src/gsolve/core/utils.py index 019f242..2eca845 100644 --- a/src/gsolve/core/utils.py +++ b/src/gsolve/core/utils.py @@ -29,11 +29,9 @@ import pandas as pd from numpy.typing import ArrayLike, DTypeLike, NDArray from pandas.api.types import ( - is_bool_dtype, is_datetime64_any_dtype, is_dict_like, is_list_like, - is_string_dtype, ) from pandas.api.typing import NaTType, NAType @@ -73,7 +71,7 @@ ] -def is_filepath_like(obj: Any) -> bool: # ruff: ignore[any-type] +def is_filepath_like(obj: Any) -> bool: """Test if object type is compatible with ``gsolve.core._typing.FilePath``. Returns @@ -83,7 +81,7 @@ def is_filepath_like(obj: Any) -> bool: # ruff: ignore[any-type] return isinstance(obj, FilePath.__value__) -def is_in_literal(value: Any, literal_type: TypeAliasType) -> bool: # ruff: ignore[any-type] +def is_in_literal(value: Any, literal_type: TypeAliasType) -> bool: """Test if value is in a Literal type. Parameters @@ -112,7 +110,7 @@ def is_in_literal(value: Any, literal_type: TypeAliasType) -> bool: # ruff: ign raise TypeError(msg) -def is_datetime_array(v: Any) -> bool: # ruff: ignore[any-type] +def is_datetime_array(v: Any) -> bool: """Test if the input is a datetime-like array. Returns @@ -122,7 +120,7 @@ def is_datetime_array(v: Any) -> bool: # ruff: ignore[any-type] return isinstance(v, DatetimeArray.__value__) -def is_points3d_like(v: Any) -> bool: # ruff: ignore[any-type] +def is_points3d_like(v: Any) -> bool: """Test if value is compatible with Points3D type. Note that is not possible to test the data type of contained arrays. @@ -292,7 +290,7 @@ def to_1d_ndarray( if a.ndim > 1: a = np.squeeze(a) if a.ndim != 1: - msg = f"input not convertible to 1d array" + msg = "input not convertible to 1d array" raise ValueError(msg) if extend_len_1_array and a.size == 1: @@ -714,7 +712,7 @@ def expand_datetime_column( cols_to_split = [str(c) for c in cols_to_split if str(c) in candidate_columns] if not cols_to_split: msg = ( - f"the specified column_name(s) are either missing or or are " + "the specified column_name(s) are either missing or or are " "not datetime-like columns" ) raise ValueError(msg) diff --git a/src/gsolve/observations.py b/src/gsolve/observations.py index b6c3c1d..67e700b 100644 --- a/src/gsolve/observations.py +++ b/src/gsolve/observations.py @@ -97,7 +97,7 @@ class GravityObservationsParameters(GSolveParameters): earthtide_correction_method: str = "" ocean_load_correction_method: str = "" - def __setattr__(self, name: str, value: Any) -> None: # ruff: ignore[any-type] + def __setattr__(self, name: str, value: Any) -> None: if name == "timedelta_unit": value = pd.Timedelta(value) elif name == "fixed_time_datum": @@ -777,7 +777,7 @@ def apply_ocean_load_correction( If ``corrector`` does not implement the ``OceanLoadCorrectionProvider`` protocol. """ if not isinstance(corrector, OceanLoadCorrectionProvider): - msg = f"ocean_load_corrector must implement OceanLoadCorrectionProvider protocol" + msg = "ocean_load_corrector must implement OceanLoadCorrectionProvider protocol" raise TypeError(msg) corrections = corrector.ocean_load_correction( @@ -1224,10 +1224,6 @@ def plot_observed_data( return fig, ax def _make_network(self, sites: GravitySites) -> pd.DataFrame: - df = self.data.assign( - group=self.data["site_id"].ne(self.data["site_id"].shift()).cumsum() - ) - station_order = ( self.data.assign( group=self.data["site_id"].ne(self.data["site_id"].shift()).cumsum() @@ -1359,7 +1355,7 @@ def plot_site_visits(self, loop: str) -> None: ) def loop_summary(self) -> pd.DataFrame: - """Return a summary of the observations by loop.""" # ruff: ignore[docstring-missing-returns] + """Return a summary of the observations by loop.""" from gsolve.core._summary_functions import ( # ruff: ignore[import-outside-top-level] duration_hr, endtime_utc, @@ -1483,7 +1479,8 @@ def check_data(self, warn: bool = True) -> bool: if "loop" in self.data.columns and "meter_id" in self.data.columns: for l in self.loop_ids: - m = self.data["loop", self.data["loop"].eq(l)].to_numpy() + # m = self.data.loc[self.data["loop"].eq(l)].to_numpy() + m = self.data.loc[self.data["loop"].eq(l), "meter_id"].to_numpy() if not (m.shape[0] == 0 or (m[0] == m).all()): warner(f"Multiple gravity meters found in loop '{l}'") @@ -1650,7 +1647,7 @@ def __copy__(self) -> Self: return type(self)(obs=self.observations.copy(), sites=self.sites.copy()) def copy(self) -> Self: - """Return a deep copy.""" # ruff: ignore[docstring-missing-returns] + """Return a deep copy.""" return copy.copy(self) @classmethod diff --git a/src/gsolve/reductions/corrections.py b/src/gsolve/reductions/corrections.py index 72f2dc6..da731c0 100644 --- a/src/gsolve/reductions/corrections.py +++ b/src/gsolve/reductions/corrections.py @@ -476,7 +476,7 @@ def bouguer_slab_curvature_corrected( msg = ( f"Unknown ellipsoid '{ellipsoid_or_radius}': must be 'WGS84' or 'GRS80'" ) - raise ValueError(msg) + raise ValueError(msg) # ruff: ignore[type-check-without-type-error] elif isinstance(ellipsoid_or_radius, boule.Ellipsoid): Ro = float(ellipsoid_or_radius.mean_radius) @@ -872,7 +872,7 @@ def compute( return GravityCorrections(params=self.params, site_id=idx, **df_dict) def _configured_bouguer_corrections(self) -> Sequence[str]: - """Return bouguer correction method names required for the current parameters.""" # ruff: ignore[docstring-missing-returns] + """Return bouguer correction method names required for the current parameters.""" corrections = ["normal_gravity_at_ellipsoid", "free_air_correction"] if self.params.use_atmospheric_correction: corrections.append("atmospheric_correction") diff --git a/src/gsolve/reductions/terrain_corrections.py b/src/gsolve/reductions/terrain_corrections.py index 58fd84a..f2ef273 100644 --- a/src/gsolve/reductions/terrain_corrections.py +++ b/src/gsolve/reductions/terrain_corrections.py @@ -30,9 +30,7 @@ import numpy as np import numpy.typing as npt import pandas as pd -import tqdm import xarray as xr -from numpy.f2py.auxfuncs import throw_error from tqdm import tqdm as _tqdm from gsolve.core._typing import ( @@ -67,8 +65,8 @@ ] -def _is_dataarray(obj: Any) -> bool: # ruff: ignore[any-type] - """Check if an object is an xarray DataArray.""" # ruff: ignore[docstring-missing-returns] +def _is_dataarray(obj: Any) -> bool: + """Check if an object is an xarray DataArray.""" return isinstance(obj, xr.DataArray) @@ -154,7 +152,7 @@ def calculate_terrain_correction( pts_x = to_1d_ndarray(points[0]).astype(np.float64) pts_y = to_1d_ndarray(points[1], expected_size=pts_x.size).astype(np.float64) pts_z = to_1d_ndarray(points[2], expected_size=pts_x.size).astype(np.float64) - except Exception as e: + except Exception as e: # ruff: ignore[blind-except] msg = f"Points must contain 1d x,y,z arrays of equal size: {e}" raise ValueError(msg) from None @@ -506,8 +504,7 @@ class TerrainCorrectionParameters(GSolveParameters): def __post_init__(self) -> None: self._sanity_check(if_errors="warn") - def __setattr__(self, name: str, value: Any) -> None: # ruff: ignore[any-type] - fieldnames = [] + def __setattr__(self, name: str, value: Any) -> None: if name not in (n.name for n in dataclasses.fields(self)): msg = f"unrecognized field name {name}" raise ValueError(msg) @@ -968,7 +965,7 @@ def compute( ycol=pars.site_northing_field, zcol=pars.site_height_field, ) - except Exception as e: + except Exception as e: # ruff: ignore[blind-except] msg = f"Error extracting site coordinates from GravitySites object: {e}" raise ValueError(msg) from None diff --git a/src/gsolve/reports.py b/src/gsolve/reports.py index af99a6a..9cb470b 100644 --- a/src/gsolve/reports.py +++ b/src/gsolve/reports.py @@ -126,11 +126,11 @@ def __init__( self._set_terrain_correction_data(terrain_corrections=terrain_corrections) def copy(self) -> Self: - """Return a deep copy.""" # ruff: ignore[docstring-missing-returns] + """Return a deep copy.""" return copy.copy(self) def __copy__(self) -> Self: - """Return a deep copy.""" # ruff: ignore[docstring-missing-returns] + """Return a deep copy.""" return copy.deepcopy(self) def _set_params( @@ -203,7 +203,7 @@ def _set_site_data( i_n_obs_used = df.columns.get_loc("n_obs_used") if not isinstance(i_n_obs_used, int): msg = "unexpected non-integer column index" - raise ValueError(msg) + raise TypeError(msg) df.insert(i_n_obs_used, "n_obs_input", n_obs_input) # add a csv string of loops each site is included in @@ -244,7 +244,7 @@ def _set_obs_data( i_included_in_solution = df.columns.get_loc("included_in_solution") if not isinstance(i_included_in_solution, int): msg = "unexpected non-integer column index" - raise ValueError(msg) + raise TypeError(msg) df.insert( loc=i_included_in_solution + 1, column="has_site_solution", @@ -271,7 +271,7 @@ def _set_loop_data( i_n_obs_input = df.columns.get_loc("n_obs_input") if not isinstance(i_n_obs_input, int): msg = "unexpected non-integer column index" - raise ValueError(msg) + raise TypeError(msg) df.insert(i_n_obs_input + 1, "n_obs_used", 0) df.loc[n_obs_used.index, "n_obs_used"] = n_obs_used @@ -415,7 +415,7 @@ def to_excel( # This is a kludge - should create method on parameter objects to # to normalize parameter outputs for writing to excel. - def _format_value(x: Any) -> str | float | int | bool: # ruff: ignore[any-type] + def _format_value(x: Any) -> str | float | int | bool: if isinstance(x, pd.Timedelta): return x.total_seconds() if isinstance(x, Path): diff --git a/src/gsolve/scintrex.py b/src/gsolve/scintrex.py index 333ec32..a13fa78 100644 --- a/src/gsolve/scintrex.py +++ b/src/gsolve/scintrex.py @@ -125,7 +125,7 @@ def copy(self) -> Self: ------- ScintrexData """ - return copy(self) + return copy.copy(self) class CG6Data(ScintrexData): @@ -338,7 +338,7 @@ def _set_data(self, data: pd.DataFrame, on_error: str = "raise") -> None: self.data = df def _strip_corrections(self) -> pd.Series: - """Return corrgrav values with all corrections removed.""" # ruff: ignore[docstring-missing-returns] + """Return corrgrav values with all corrections removed.""" return ( self.data["corrgrav"] - (self.data["driftcorr"] * self.data["correction_drift"]) @@ -736,7 +736,7 @@ def set_drift_correction( if not isinstance(drift_zero_time, pd.Timestamp): msg = "drift_zero_time could not be converted to a valid Timestamp." - raise ValueError(msg) + raise TypeError(msg) drift_corr = ( (self.data["datetime"] - drift_zero_time) @@ -752,7 +752,7 @@ def set_drift_correction( def _slurp_scintrex_text_file(filepath: FilePath) -> list[str]: - """Read a Scintrex text file, fix encoding and return lines as a list.""" # ruff: ignore[docstring-missing-returns] + """Read a Scintrex text file, fix encoding and return lines as a list.""" with pathlib.Path(filepath).open("r", encoding="utf-8-sig") as fh: return [l.strip() for l in fh] @@ -762,7 +762,7 @@ def _split_header_key_val_unit( normalize_key: bool = True, extract_units: bool = True, ) -> tuple[str, str, str]: - """Split headers into key, value and units.""" # ruff: ignore[docstring-missing-returns] + """Split headers into key, value and units.""" header = header.strip("/ ") if not header: return ("", "", "") @@ -814,7 +814,7 @@ def _scintrex_header_type_conversion( def _extract_unit_from_keyword(header: str) -> tuple[str, str]: - """Get header and unit form a header string.""" # ruff: ignore[docstring-missing-returns] + """Get header and unit form a header string.""" if header.endswith(")"): sep = "(" elif header.endswith("]"): diff --git a/src/gsolve/sites.py b/src/gsolve/sites.py index 4481d3b..b7ec692 100644 --- a/src/gsolve/sites.py +++ b/src/gsolve/sites.py @@ -20,7 +20,7 @@ from __future__ import annotations -from collections.abc import Iterable, Mapping, Sequence +from collections.abc import Iterable, Mapping from types import MappingProxyType from typing import Literal, Self @@ -767,11 +767,9 @@ def __init__( raise ValueError(msg) # catch empty site_id - if (m := idx.isna() | idx.eq("")).any(): - empty = pd.Series(m).loc[m.tolist()].index.to_list() + if idx.isna().any() or (idx == "").any(): # ruff: ignore[compare-to-empty-string] msg = ( - "creating ReferenceGravity object: " - f"site_id field contains empty values at rows: {empty}" + "creating ReferenceGravity object: site_id field contains empty values" ) raise ValueError(msg) diff --git a/src/gsolve/tide/earth_tide.py b/src/gsolve/tide/earth_tide.py index c111644..3882729 100644 --- a/src/gsolve/tide/earth_tide.py +++ b/src/gsolve/tide/earth_tide.py @@ -556,14 +556,14 @@ def _decimal_julian_century( dt_ = to_naive_utc_datetime(dt, allow_nat=False, **kwargs) if dt_ is None or isinstance(dt_, NaTType): msg = "dt cannot be NaT or None." - raise ValueError(msg) + raise TypeError(msg) if isinstance(dt_, pd.Timestamp): dt_ = pd.DatetimeIndex([dt_]) elif isinstance(dt_, (pd.DatetimeIndex, pd.Series)): dt_ = pd.DatetimeIndex(dt_) else: msg = "dt could not be resolved to a datetime, DatetimeIndex, Series, or array-like of datetimes." - raise ValueError(msg) + raise TypeError(msg) julian_century_origin = pd.Timestamp("1899-12-31T12:00:00", tz=None) td_seconds = (dt_ - julian_century_origin).total_seconds() @@ -602,7 +602,7 @@ def _decimal_hour_of_day( raise ValueError(msg) if not isinstance(dt, (pd.Timestamp, pd.Series, pd.DatetimeIndex)): msg = "date_time could not be resolved to a datetime, DatetimeIndex, Series, or array-like of datetimes." - raise ValueError(msg) + raise TypeError(msg) if isinstance(dt, pd.Timestamp): dt = pd.DatetimeIndex([dt]) @@ -760,7 +760,7 @@ def sort_and_validate(self, gap_threshold: float | None = None) -> None: if gaps.any(): warnings.warn( message=( - "some frequency intervals separated by " + f"some frequency {gaps.sum()} intervals separated by" f"greater than {gap_threshold} " ), category=UserWarning, @@ -867,7 +867,7 @@ def from_excel(cls, fname: FilePath, sheet_name: str | int = 0) -> Self: df = pd.read_excel(fname, sheet_name=sheet_name) if isinstance(df, dict): msg = "Excel file contains multiple sheets. Please specify sheet_name." - raise ValueError(msg) + raise ValueError(msg) # ruff: ignore[type-check-without-type-error] return cls.from_dataframe(df) @@ -1172,7 +1172,7 @@ def time_series( tides_df = self._pgt.results() if not isinstance(tides_df, pd.DataFrame): msg = "No results returned from pygtide prediction." - raise ValueError(msg) + raise ValueError(msg) # ruff: ignore[type-check-without-type-error] normalized_cols = ["datetime", "signal", "tide", "pole_tide", "lod_tide"] tides_df = tides_df.rename( @@ -1283,7 +1283,7 @@ def tidal_correction( if not isinstance(ts.index, pd.DatetimeIndex): msg = "Unexpected time series index type from pygtide results." - raise ValueError(msg) + raise ValueError(msg) # ruff: ignore[type-check-without-type-error] ts = ts.set_index(ts.index.round(freq="1s")) if not np.isnan(corrs[site_mask]).all(): diff --git a/src/gsolve/tide/ocean_load.py b/src/gsolve/tide/ocean_load.py index 947f4c5..6b0d072 100644 --- a/src/gsolve/tide/ocean_load.py +++ b/src/gsolve/tide/ocean_load.py @@ -152,7 +152,7 @@ def __init__( self.metadata: dict[str, Any] = metadata def identifier(self) -> str: - """Corrector identifier string.""" # ruff: ignore[docstring-missing-returns] + """Corrector identifier string.""" return f"{type(self).__name__}()" def ocean_load_correction( @@ -296,7 +296,7 @@ def __repr__(self) -> str: return f"{cname}({md})" def identifier(self) -> str: - """Corrector identifier string.""" # ruff: ignore[docstring-missing-returns] + """Corrector identifier string.""" return f"{self.__class__.__name__}()" @property @@ -377,7 +377,7 @@ class since it provides corrections for a single station. def _datetimes_to_np_datetime64( dt: DatetimeScalar | DatetimeArray, dtype: str = "datetime64" ) -> np.ndarray: - """Convert datetimes to numpy datetime64 array.""" # ruff: ignore[docstring-missing-returns] + """Convert datetimes to numpy datetime64 array.""" dt = to_naive_utc_datetime(dt, allow_nat=False) if isinstance(dt, pd.Timestamp): return np.array([dt], dtype=dtype) @@ -758,7 +758,7 @@ def ocean_load_correction( len( bad_site_ids := [str(s) for s in uniq_site_id if s not in self.stations] ) - == 0 + != 0 ): msg = ( f"site_id(s) {bad_site_ids} not found in station loading model. " diff --git a/tests/test_anomalies.py b/tests/test_anomalies.py index 8946e1e..0cd79dd 100644 --- a/tests/test_anomalies.py +++ b/tests/test_anomalies.py @@ -18,8 +18,6 @@ import numpy as np import pytest -from scipy.constants import atm -from tornado.routing import AnyMatches from gsolve.reductions.anomalies import ( compute_complete_bouguer_anomaly, @@ -41,7 +39,6 @@ def anomaly_args(): def test_compute_complete_bouguer_anomaly_basic(): - c = anomaly_args() ag = np.array([100.0, 200.0]) ng = np.array([10.0, 20.0]) fac = np.array([1.0, 2.0]) diff --git a/tests/test_terrain_corrections_consistency.py b/tests/test_terrain_corrections_consistency.py index b8fd068..a144555 100644 --- a/tests/test_terrain_corrections_consistency.py +++ b/tests/test_terrain_corrections_consistency.py @@ -22,8 +22,6 @@ TerrainCorrectionData, TerrainCorrectionParameters, TerrainCorrector, - _is_dataarray, - calculate_terrain_correction, ) from gsolve.sites import GravitySites diff --git a/tests/test_tide/test_ocean_load.py b/tests/test_tide/test_ocean_load.py index 8e8ce8c..1fcb5c0 100644 --- a/tests/test_tide/test_ocean_load.py +++ b/tests/test_tide/test_ocean_load.py @@ -17,7 +17,6 @@ from pathlib import Path import numpy as np -import numpy.testing as npt import pandas as pd import pytest From 51e6ad8b4e4d362723a53d3b8634f2f8760cc9bb Mon Sep 17 00:00:00 2001 From: Adrian Benson Date: Fri, 25 Sep 2026 02:23:32 +1200 Subject: [PATCH 23/36] Make terrain correction status messages go to stderr. --- src/gsolve/reductions/terrain_corrections.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/gsolve/reductions/terrain_corrections.py b/src/gsolve/reductions/terrain_corrections.py index f2ef273..e748bb5 100644 --- a/src/gsolve/reductions/terrain_corrections.py +++ b/src/gsolve/reductions/terrain_corrections.py @@ -955,7 +955,7 @@ def compute( for zone in self.zones: pars = self.params[zone].copy() if show_progress: - sys.stderr.write(f"Calculating terrain corrections for zone: {zone}") + sys.stderr.write(f"Calculating terrain corrections for zone: {zone}\n") # get points if necessary if get_xyz_per_zone: @@ -1021,12 +1021,12 @@ def compute( indent = " " if show_progress else "" sys.stderr.write( f"{indent}Warning: zone '{zone}': terrain corrections " - f"not calculated for {n_missing_tc} of {len(x)} sites." + f"not calculated for {n_missing_tc} of {len(x)} sites.\n" ) if not nan_error_description_displayed: nan_error_description_displayed = True sys.stderr.write( - f"{indent} This is probably due to:", + f"{indent} This is probably due to:\n", ) sys.stderr.write( f"{indent} (1) insufficient DEM coverage and/or\n" From caa8421e2b47f2f50b20f2187aaba93b2a047cd7 Mon Sep 17 00:00:00 2001 From: Adrian Benson Date: Fri, 25 Sep 2026 02:51:36 +1200 Subject: [PATCH 24/36] Resolve doc build errors. --- docs/source/conf.py | 2 +- src/gsolve/core/utils.py | 50 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 7dda8a6..f540cf6 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -22,7 +22,7 @@ import sys from pathlib import Path -sys.path.insert(0, Path.resolve("../../src")) +sys.path.insert(0, Path("../../src").resolve()) # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information diff --git a/src/gsolve/core/utils.py b/src/gsolve/core/utils.py index 2eca845..82cb870 100644 --- a/src/gsolve/core/utils.py +++ b/src/gsolve/core/utils.py @@ -67,7 +67,10 @@ "prepare_writable_df", "round_coords", "timestamp_to_columns", + "to_1d_ndarray", + "to_1d_ndarray_or_float", "to_naive_utc_datetime", + "to_points3d", ] @@ -286,6 +289,29 @@ def to_1d_ndarray( extend_len_1_array: bool = False, dtype: DTypeLike | None = None, ) -> NDArray[np.float64]: + """Convert input to a 1D numpy array. + + Replicates the functionality of numpy.atleast_1d, but with additional + checks for expected size and optional extension of length-1 arrays. + + Parameters + ---------- + a : array-like + The input to be converted to a 1D numpy array. + expected_size : int, optional + If specified, the function will raise a ValueError if the resulting + array does not have this size. + extend_len_1_array : bool, default False + If True and the input is a length-1 array, it will be extended to + the specified expected_size. + dtype : data-type, optional + If specified, the resulting array will be cast to this data type. + + Returns + ------- + numpy.ndarray + A 1D numpy array with the specified properties. + """ a = np.atleast_1d(a) if a.ndim > 1: a = np.squeeze(a) @@ -309,8 +335,28 @@ def to_1d_ndarray( return a -def to_1d_ndarray_or_float(a: ArrayLike) -> NDArray[np.float64] | np.float64: - a = to_1d_ndarray(a).astype(np.float64) +def to_1d_ndarray_or_float( + a: ArrayLike, dtype: DTypeLike = np.float64 +) -> NDArray[np.float64] | np.float64: + """Convert input to a 1D numpy array or a float. + + If the input is a length-1 array, it will be converted to a float. + + Parameters + ---------- + a : array-like + The input to be converted to a 1D numpy array or a float. + dtype : data-type, optional + If specified, the resulting array will be cast to this data type. + If None, the default data type is np.float64. + + Returns + ------- + numpy.ndarray or float + A 1D numpy array or a float, depending on the size of the input. + + """ + a = to_1d_ndarray(a, dtype=dtype) return a[0] if a.size == 1 else a From 4b9f0c3cfea685b9cf0bf49837823e0a0f728861 Mon Sep 17 00:00:00 2001 From: Adrian Benson Date: Fri, 25 Sep 2026 11:27:08 +1200 Subject: [PATCH 25/36] Adress review comments --- .../TeMaari_ocean_loading_pyhardisp.py | 2 +- examples/scripts/okataina_survey.py | 4 +-- src/gsolve/core/data.py | 3 +- src/gsolve/core/utils.py | 2 +- src/gsolve/reductions/corrections.py | 33 +++++++++---------- src/gsolve/tide/earth_tide.py | 11 +++---- 6 files changed, 25 insertions(+), 30 deletions(-) diff --git a/examples/scripts/TeMaari_ocean_loading_pyhardisp.py b/examples/scripts/TeMaari_ocean_loading_pyhardisp.py index 5e47124..7434ade 100644 --- a/examples/scripts/TeMaari_ocean_loading_pyhardisp.py +++ b/examples/scripts/TeMaari_ocean_loading_pyhardisp.py @@ -98,7 +98,7 @@ """ Run the network adjustment. Here we use solve method "2", see documentation. We process each loop individually -and apply a 99 percentile cutoff filter to the residuals.""" +and apply a 95 percentile cutoff filter to the residuals.""" results = survey.solve_lstsq(method=2, use_loops=True, percentile_clipping=95) # results.site_solution contains the adjusted gravity per station diff --git a/examples/scripts/okataina_survey.py b/examples/scripts/okataina_survey.py index d8f3097..bb18e48 100644 --- a/examples/scripts/okataina_survey.py +++ b/examples/scripts/okataina_survey.py @@ -18,7 +18,7 @@ import pathlib try: - import contextily as cx # ty:ignore[unresolved-import] + import contextily as cx # type:ignore[unresolved-import] # ty: ignore[unresolved-import] except ImportError: has_contextily = False cx = None @@ -152,7 +152,7 @@ """ Run the network adjustment. Here we use solve method "2", see documentation. We process each loop individually -and apply a 99 percentile cutoff filter to the residuals.""" +and apply a 95 percentile cutoff filter to the residuals.""" results = survey.solve_lstsq(method=2, use_loops=True, percentile_clipping=95) # results.site_solution contains the adjusted gravity per station diff --git a/src/gsolve/core/data.py b/src/gsolve/core/data.py index 5f17407..5784699 100644 --- a/src/gsolve/core/data.py +++ b/src/gsolve/core/data.py @@ -27,6 +27,7 @@ from types import MappingProxyType from typing import Any, ClassVar, Protocol, Self +import numpy as np import pandas as pd from pandas.api.types import is_bool_dtype, is_string_dtype @@ -96,7 +97,7 @@ class DataFieldSpecification: ), DataFieldSpecification("active", bool, default=True, required=False), DataFieldSpecification("latitude", float, required=False), - DataFieldSpecification("longitude", float, required=False), + DataFieldSpecification("latitude", float, required=False), DataFieldSpecification( "height_ellipsoidal", float, legacy_name="height", default=np.nan ), diff --git a/src/gsolve/core/utils.py b/src/gsolve/core/utils.py index 82cb870..f7898c5 100644 --- a/src/gsolve/core/utils.py +++ b/src/gsolve/core/utils.py @@ -288,7 +288,7 @@ def to_1d_ndarray( expected_size: int | None = None, extend_len_1_array: bool = False, dtype: DTypeLike | None = None, -) -> NDArray[np.float64]: +) -> np.ndarray[tuple[int], np.dtype[Any]]: """Convert input to a 1D numpy array. Replicates the functionality of numpy.atleast_1d, but with additional diff --git a/src/gsolve/reductions/corrections.py b/src/gsolve/reductions/corrections.py index da731c0..c78e8d6 100644 --- a/src/gsolve/reductions/corrections.py +++ b/src/gsolve/reductions/corrections.py @@ -604,36 +604,33 @@ class GravityCorrections(GSolveTable): _known_fields: ClassVar[MappingProxyType[str, DataFieldSpecification]] = ( MappingProxyType( { - "site_id": DataFieldSpecification("site_id", str, required=True), - "longitude": DataFieldSpecification("longitude", float, required=False), - "latitude": DataFieldSpecification("latitude", float, required=False), - "height_ellipsoidal": DataFieldSpecification( - "height_ellipsoidal", float, required=False, legacy_name="height" - ), + COMMON_FIELDS["site_id"].name: COMMON_FIELDS["site_id"], + COMMON_FIELDS["latitude"].name: COMMON_FIELDS["timestamp"], + COMMON_FIELDS["longitude"].name: COMMON_FIELDS["longitude"], + COMMON_FIELDS["height_ellipsoidal"].name: COMMON_FIELDS[ + "height_ellipsoidal" + ], "normal_gravity_at_stn_elevation": DataFieldSpecification( - "normal_gravity_at_stn_elevation", - float, - required=False, - default=np.nan, + "normal_gravity_at_stn_elevation", float, default=np.nan ), "normal_gravity_at_ellipsoid": DataFieldSpecification( - "normal_gravity_at_ellipsoid", float, required=False, default=np.nan + "normal_gravity_at_ellipsoid", float, default=np.nan ), "free_air_correction": DataFieldSpecification( - "free_air_correction", float, required=False, default=np.nan + "free_air_correction", float, default=np.nan ), "bouguer_slab_correction": DataFieldSpecification( - "bouguer_slab_correction", float, required=False, default=np.nan + "bouguer_slab_correction", float, default=np.nan ), "bouguer_slab_curvature_corrected": DataFieldSpecification( - "bouguer_slab_curvature_corrected", - float, - required=False, - default=np.nan, + "bouguer_slab_curvature_corrected", float, default=np.nan ), "atmospheric_correction": DataFieldSpecification( - "atmospheric_correction", float, required=False, default=np.nan + "atmospheric_correction", float, default=np.nan ), + COMMON_FIELDS["absolute_gravity"].name: COMMON_FIELDS[ + "absolute_gravity" + ], } ) ) diff --git a/src/gsolve/tide/earth_tide.py b/src/gsolve/tide/earth_tide.py index 3882729..a15f4ce 100644 --- a/src/gsolve/tide/earth_tide.py +++ b/src/gsolve/tide/earth_tide.py @@ -134,16 +134,13 @@ class LongmanConstants: c: float = 3.84399e10 # Mean distance between the centers earth-moon (cm) c1: float = 1.495983e13 # Mean distance between centers earth-sun (cm) e: float = 0.054900489 # Eccentricity of the moon's orbit - i: float = round( - deg2rad(5.145, dtype=float), - ndigits=9, - ) # = 0.08979719 Inclination of moon's orbit to the ecliptic + i: float = round(deg2rad(5.145, dtype=np.float64), ndigits=9) + # = 0.08979719 Inclination of moon's orbit to the ecliptic m: float = 0.074804 # Ratio of mean motion of the sun to that of the moon mu: float = 6.67428e-08 # Newton's gravitational constant, 6.670e-8 in orig. M: float = 7.3477e25 # Mass of the moon in grams - omega: float = round( - deg2rad(23.452), ndigits=9 - ) # = 0.409315 Incl. of Earth's equator to ecliptic + omega: float = round(deg2rad(23.452, dtype=np.float64), ndigits=9) + # = 0.409315 Incl. of Earth's equator to ecliptic S: float = 1.98840987e33 # Mass of the sun in grams # https://aa.usno.navy.mil/downloads/publications/Constants_2021.pdf From 7b048602995a0da253781d8dca16514e202c0e3c Mon Sep 17 00:00:00 2001 From: Adrian Benson Date: Fri, 25 Sep 2026 11:31:09 +1200 Subject: [PATCH 26/36] doh! --- src/gsolve/reductions/corrections.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gsolve/reductions/corrections.py b/src/gsolve/reductions/corrections.py index c78e8d6..a34702d 100644 --- a/src/gsolve/reductions/corrections.py +++ b/src/gsolve/reductions/corrections.py @@ -605,7 +605,7 @@ class GravityCorrections(GSolveTable): MappingProxyType( { COMMON_FIELDS["site_id"].name: COMMON_FIELDS["site_id"], - COMMON_FIELDS["latitude"].name: COMMON_FIELDS["timestamp"], + COMMON_FIELDS["latitude"].name: COMMON_FIELDS["latitude"], COMMON_FIELDS["longitude"].name: COMMON_FIELDS["longitude"], COMMON_FIELDS["height_ellipsoidal"].name: COMMON_FIELDS[ "height_ellipsoidal" From edaa439e8bbbdb29274f7faea91944fd359ea10e Mon Sep 17 00:00:00 2001 From: Adrian Benson Date: Fri, 25 Sep 2026 11:33:03 +1200 Subject: [PATCH 27/36] double doh! --- src/gsolve/core/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gsolve/core/data.py b/src/gsolve/core/data.py index 5784699..6a8ef39 100644 --- a/src/gsolve/core/data.py +++ b/src/gsolve/core/data.py @@ -97,7 +97,7 @@ class DataFieldSpecification: ), DataFieldSpecification("active", bool, default=True, required=False), DataFieldSpecification("latitude", float, required=False), - DataFieldSpecification("latitude", float, required=False), + DataFieldSpecification("longitude", float, required=False), DataFieldSpecification( "height_ellipsoidal", float, legacy_name="height", default=np.nan ), From b98852efa3b3a213b47917397cfd2acf5122d31e Mon Sep 17 00:00:00 2001 From: Adrian Benson Date: Mon, 28 Sep 2026 14:27:01 +1300 Subject: [PATCH 28/36] White space and spelling --- AUTHORS.md | 2 +- CHANGELOG.md | 3 +- README.md | 24 +- .../quicktide/okataina_qtp_input.csv | 1226 ++++++++--------- project-words.txt | 1 + pyproject.toml | 1 + uv.lock | 358 +++++ 7 files changed, 987 insertions(+), 628 deletions(-) diff --git a/AUTHORS.md b/AUTHORS.md index 7a85e03..ed93389 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -7,4 +7,4 @@ order by last name) and are considered "The gSolve Developers": * Alison Kirkby - Earth Sciences New Zealand, New Zealand * Craig Miller - Earth Sciences New Zealand, New Zealand * Vaughan Stagpoole - Earth Sciences New Zealand, New Zealand -* Aleksandr Spesivtsev - Earth Sciences New Zealand, New Zealand \ No newline at end of file +* Aleksandr Spesivtsev - Earth Sciences New Zealand, New Zealand diff --git a/CHANGELOG.md b/CHANGELOG.md index 527cb43..8e2bca4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,6 @@ New features: * Added GPLv3 licence * PyGTide added for ETERNA tide correction -Maintenance: +Maintenance: * Bug fix for terrain corrections - diff --git a/README.md b/README.md index 78fd941..b77f2e8 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,15 @@ ![gSolve logo](docs/source/_static/gsolve_logo.png) - + [![codecov](https://codecov.io/gh/GNS-Science/gsolve/branch/main/graph/badge.svg)](https://codecov.io/gh/GNS-Science/gsolve) [![GitHub License](https://img.shields.io/github/license/GNS-Science/gsolve)](https://github.com/GNS-Science/gsolve/blob/main/LICENSE) ![Publish to PyPI](https://github.com/GNS-Science/gsolve/actions/workflows/publish.yml/badge.svg) [![Pypi version](https://img.shields.io/pypi/v/gsolve)](https://pypi.org/project/gsolve/) - - # gSolve -gSolve, a Python computer library by Earth Sciences New Zealand (formerly GNS Science) to transform relative gravity survey measurements to absolute gravity values and gravity anomalies and disturbances. +gSolve, a Python computer library by Earth Sciences New Zealand (formerly GNS Science) to transform relative gravity survey measurements to absolute gravity values and gravity anomalies and disturbances. -It is suitable for time varying gravity as well as Bouguer gravity. +It is suitable for time varying gravity as well as Bouguer gravity. This version is a substantial re-write of the previous python version to remove the limitation of a graphical user interface and to update to python 3 with modern software management. @@ -30,8 +28,10 @@ Process gravity data for time change microgravity and Bouguer surveys. * option to correct for ocean loading using [pyhardisp](https://github.com/craigmillernz/pyhardisp). * correct for drift across loops or whole survey. * network adjustment with three different network adjustment algorithms depending on user requirements. -# calibrate meters to absolute values. -* residuals can be filtered using a percentile cut filter. + +# calibrate meters to absolute values + +* residuals can be filtered using a percentile cut filter. ## Corrections @@ -42,7 +42,7 @@ Process gravity data for time change microgravity and Bouguer surveys. ## Plotting * plot raw observations -* residual cumuluative probability density functions, CDF. +* residual cumulative probability density functions, CDF. * drift curve * network map @@ -65,14 +65,14 @@ Full documentation is available here. [gSolve](https://gns-science.github.io/gso # Authors and acknowledgment -gSolve builds on many previous authors. +gSolve builds on many previous authors. The current author team is Adrian Benson, Alison Kirkby, Craig Miller, Aleksandr Spesivtsev, Vaughan Stagpoole. This version supersedes previous Gsolve versions e.g. -McCubbine, J., Tontini, F. C., Stagpoole, V., Smith, E., & O’Brien, G. (2018). Gsolve, a Python computer program with a graphical user interface to transform relative gravity survey measurements to absolute gravity values and gravity anomalies. SoftwareX, 7, 129–137. +McCubbine, J., Tontini, F. C., Stagpoole, V., Smith, E., & O’Brien, G. (2018). Gsolve, a Python computer program with a graphical user interface to transform relative gravity survey measurements to absolute gravity values and gravity anomalies. SoftwareX, 7, 129–137. -# How to cite gSolve +# How to cite gSolve Link to JOSS paper here when it is ready. @@ -82,4 +82,4 @@ Python 3.12+ # License -Licenced with GPLv3. +Licensed with GPLv3. diff --git a/examples/ocean_load/quicktide/okataina_qtp_input.csv b/examples/ocean_load/quicktide/okataina_qtp_input.csv index 60ccaba..cb9cade 100644 --- a/examples/ocean_load/quicktide/okataina_qtp_input.csv +++ b/examples/ocean_load/quicktide/okataina_qtp_input.csv @@ -1,613 +1,613 @@ -ACNN,03/16/2020 21:08,-38.512209,176.331926,297.958 -ACNN,03/16/2020 21:12,-38.512209,176.331926,297.958 -ACNN,03/16/2020 21:13,-38.512209,176.331926,297.958 -WP93,03/16/2020 22:29,-38.265116,176.564573,467.265 -WP93,03/16/2020 22:30,-38.265116,176.564573,467.265 -WP146,03/16/2020 22:44,-38.268071,176.572404,446.828 -WP146,03/16/2020 22:45,-38.268071,176.572404,446.828 -WP140,03/16/2020 22:56,-38.269557,176.577605,434.051 -WP140,03/16/2020 22:59,-38.269557,176.577605,434.051 -WP140,03/16/2020 23:00,-38.269557,176.577605,434.051 -WP143,03/16/2020 23:24,-38.275345,176.584047,479.006 -WP143,03/16/2020 23:25,-38.275345,176.584047,479.006 -WP143,03/16/2020 23:26,-38.275345,176.584047,479.006 -WP144,03/16/2020 23:55,-38.274621,176.568323,448.606 -WP144,03/16/2020 23:57,-38.274621,176.568323,448.606 -WP144,03/16/2020 23:58,-38.274621,176.568323,448.606 -WP93,03/17/2020 00:13,-38.265116,176.564573,467.265 -WP93,03/17/2020 00:14,-38.265116,176.564573,467.265 -WP98,03/17/2020 00:28,-38.256793,176.571165,457.455 -WP139,03/17/2020 01:04,-38.262333,176.556635,494.174 -WP139,03/17/2020 01:06,-38.262333,176.556635,494.174 -WP139,03/17/2020 01:07,-38.262333,176.556635,494.174 -WP138,03/17/2020 01:15,-38.255525,176.560111,495.930 -WP138,03/17/2020 01:18,-38.255525,176.560111,495.930 -WP138,03/17/2020 01:19,-38.255525,176.560111,495.930 -WP96,03/17/2020 02:16,-38.262988,176.543263,514.214 -WP96,03/17/2020 02:17,-38.262988,176.543263,514.214 -WP93,03/17/2020 02:36,-38.265116,176.564573,467.265 -WP93,03/17/2020 02:37,-38.265116,176.564573,467.265 -ACNN,03/17/2020 03:39,-38.512209,176.331926,297.958 -ACNN,03/17/2020 03:41,-38.512209,176.331926,297.958 -ACNN,03/17/2020 03:42,-38.512209,176.331926,297.958 -ACNN,03/17/2020 20:44,-38.512209,176.331926,297.958 -ACNN,03/17/2020 20:46,-38.512209,176.331926,297.958 -ACNN,03/17/2020 20:47,-38.512209,176.331926,297.958 -WP93,03/17/2020 21:40,-38.265116,176.564573,467.265 -WP93,03/17/2020 21:42,-38.265116,176.564573,467.265 -WP93,03/17/2020 21:43,-38.265116,176.564573,467.265 -WP148,03/17/2020 22:01,-38.290511,176.575760,471.560 -WP148,03/17/2020 22:06,-38.290511,176.575760,471.560 -WP148,03/17/2020 22:07,-38.290511,176.575760,471.560 -WP141,03/17/2020 22:28,-38.281541,176.576093,453.541 -WP141,03/17/2020 22:29,-38.281541,176.576093,453.541 -WP141,03/17/2020 22:30,-38.281541,176.576093,453.541 -WP142,03/17/2020 22:52,-38.281560,176.566694,474.395 -WP142,03/17/2020 22:53,-38.281560,176.566694,474.395 -WP142,03/17/2020 22:54,-38.281560,176.566694,474.395 -WP147,03/17/2020 23:14,-38.293007,176.555309,449.272 -WP147,03/17/2020 23:17,-38.293007,176.555309,449.272 -WP139,03/17/2020 23:39,-38.262333,176.556635,494.174 -WP139,03/17/2020 23:40,-38.262333,176.556635,494.174 -WP93,03/17/2020 23:52,-38.265116,176.564573,467.265 -WP93,03/17/2020 23:54,-38.265116,176.564573,467.265 -WP93,03/17/2020 23:55,-38.265116,176.564573,467.265 -WP99,03/18/2020 00:17,-38.269413,176.550480,475.842 -WP99,03/18/2020 00:20,-38.269413,176.550480,475.842 -WP136,03/18/2020 00:39,-38.274086,176.545530,473.901 -WP136,03/18/2020 00:44,-38.274086,176.545530,473.901 -WP95,03/18/2020 00:58,-38.272596,176.535114,486.335 -WP95,03/18/2020 01:03,-38.272596,176.535114,486.335 -WP95,03/18/2020 01:04,-38.272596,176.535114,486.335 -WP145,03/18/2020 01:43,-38.281429,176.543511,458.052 -WP145,03/18/2020 01:45,-38.281429,176.543511,458.052 -WP145,03/18/2020 01:46,-38.281429,176.543511,458.052 -WP93,03/18/2020 02:03,-38.265116,176.564573,467.265 -WP93,03/18/2020 02:07,-38.265116,176.564573,467.265 -WP93,03/18/2020 02:08,-38.265116,176.564573,467.265 -WP149,03/18/2020 02:26,-38.307750,176.558839,445.231 -WP149,03/18/2020 02:29,-38.307750,176.558839,445.231 -WP149,03/18/2020 02:30,-38.307750,176.558839,445.231 -ACNN,03/18/2020 03:10,-38.512209,176.331926,297.958 -ACNN,03/18/2020 03:15,-38.512209,176.331926,297.958 -AGUP,06/10/2020 23:48,-38.054648,176.441513,284.462 -AGUP,06/10/2020 23:50,-38.054648,176.441513,284.462 -OG32,06/11/2020 00:56,-38.122774,176.475586,820.424 -OG32,06/11/2020 00:57,-38.122774,176.475586,820.424 -OG36,06/11/2020 01:55,-38.138471,176.467108,926.489 -OG36,06/11/2020 01:57,-38.138471,176.467108,926.489 -OG37,06/11/2020 02:23,-38.138224,176.459507,854.027 -OG37,06/11/2020 02:24,-38.138224,176.459507,854.027 -OG38,06/11/2020 02:44,-38.134208,176.461352,851.227 -OG38,06/11/2020 02:45,-38.134208,176.461352,851.227 -OG34,06/11/2020 03:12,-38.131201,176.474881,800.569 -OG34,06/11/2020 03:13,-38.131201,176.474881,800.569 -OG34,06/11/2020 03:14,-38.131201,176.474881,800.569 -OG33,06/11/2020 03:32,-38.128162,176.479851,837.787 -OG33,06/11/2020 03:33,-38.128162,176.479851,837.787 -OG32,06/11/2020 03:43,-38.122774,176.475586,820.424 -OG32,06/11/2020 03:44,-38.122774,176.475586,820.424 -AGUP,06/11/2020 04:35,-38.054648,176.441513,284.462 -AGUP,06/11/2020 04:36,-38.054648,176.441513,284.462 -AGUP,06/11/2020 23:06,-38.054648,176.441513,284.462 -AGUP,06/11/2020 23:08,-38.054648,176.441513,284.462 -AGUP,06/11/2020 23:10,-38.054648,176.441513,284.462 -OG29,06/12/2020 00:36,-38.090789,176.456845,591.652 -OG29,06/12/2020 00:37,-38.090789,176.456845,591.652 -OG29,06/12/2020 00:38,-38.090789,176.456845,591.652 -OG16,06/12/2020 00:51,-38.096062,176.456555,620.377 -OG16,06/12/2020 00:53,-38.096062,176.456555,620.377 -OG16,06/12/2020 00:54,-38.096062,176.456555,620.377 -OG09,06/12/2020 01:05,-38.094752,176.447202,646.313 -OG09,06/12/2020 01:06,-38.094752,176.447202,646.313 -OG07,06/12/2020 01:21,-38.086364,176.446698,592.772 -OG05,06/12/2020 01:34,-38.083088,176.456330,545.289 -OG05,06/12/2020 01:35,-38.083088,176.456330,545.289 -OG05,06/12/2020 01:36,-38.083088,176.456330,545.289 -OG29,06/12/2020 01:52,-38.090789,176.456845,591.652 -OG29,06/12/2020 01:53,-38.090789,176.456845,591.652 -OG32,06/12/2020 02:32,-38.122774,176.475586,820.424 -OG32,06/12/2020 02:33,-38.122774,176.475586,820.424 -OG31,06/12/2020 02:45,-38.117953,176.479049,756.718 -OG31,06/12/2020 02:46,-38.117953,176.479049,756.718 -OG20,06/12/2020 03:03,-38.115873,176.472735,673.949 -OG20,06/12/2020 03:04,-38.115873,176.472735,673.949 -OG20,06/12/2020 03:05,-38.115873,176.472735,673.949 -OG28,06/12/2020 03:20,-38.105198,176.474171,662.676 -OG28,06/12/2020 03:21,-38.105198,176.474171,662.676 -OG27,06/12/2020 03:34,-38.099356,176.468858,628.158 -OG27,06/12/2020 03:35,-38.099356,176.468858,628.158 -OG27,06/12/2020 03:36,-38.099356,176.468858,628.158 -OG05,06/12/2020 03:54,-38.083088,176.456330,545.289 -OG05,06/12/2020 03:55,-38.083088,176.456330,545.289 -OG05,06/12/2020 03:56,-38.083088,176.456330,545.289 -AGUP,06/12/2020 04:16,-38.054648,176.441513,284.462 -AGUP,06/12/2020 04:18,-38.054648,176.441513,284.462 -AGUP,06/12/2020 04:19,-38.054648,176.441513,284.462 -AGUP,06/15/2020 20:18,-38.054648,176.441513,284.462 -AGUP,06/15/2020 20:20,-38.054648,176.441513,284.462 -AGUP,06/15/2020 20:21,-38.054648,176.441513,284.462 -AGUP,06/15/2020 20:22,-38.054648,176.441513,284.462 -OK100,06/15/2020 21:02,-38.085725,176.461508,571.934 -OK100,06/15/2020 21:03,-38.085725,176.461508,571.934 -OK100,06/15/2020 21:04,-38.085725,176.461508,571.934 -OK100,06/15/2020 21:05,-38.085725,176.461508,571.934 -OK101,06/15/2020 21:29,-38.095606,176.464133,611.804 -OK101,06/15/2020 21:30,-38.095606,176.464133,611.804 -OK101,06/15/2020 21:31,-38.095606,176.464133,611.804 -OK101,06/15/2020 21:32,-38.095606,176.464133,611.804 -OK103,06/15/2020 22:13,-38.126974,176.457878,817.623 -OK103,06/15/2020 22:14,-38.126974,176.457878,817.623 -OK103,06/15/2020 22:16,-38.126974,176.457878,817.623 -OK103,06/15/2020 22:17,-38.126974,176.457878,817.623 -OK102,06/15/2020 22:55,-38.116363,176.464753,653.245 -OK102,06/15/2020 22:56,-38.116363,176.464753,653.245 -OK102,06/15/2020 22:57,-38.116363,176.464753,653.245 -OK104,06/15/2020 23:08,-38.116836,176.459708,653.269 -OK104,06/15/2020 23:09,-38.116836,176.459708,653.269 -OK104,06/15/2020 23:10,-38.116836,176.459708,653.269 -OK105,06/15/2020 23:22,-38.113316,176.450891,624.164 -OK105,06/15/2020 23:23,-38.113316,176.450891,624.164 -OK105,06/15/2020 23:24,-38.113316,176.450891,624.164 -OK105,06/15/2020 23:25,-38.113316,176.450891,624.164 -OK106,06/15/2020 23:45,-38.109690,176.447081,595.380 -OK106,06/15/2020 23:46,-38.109690,176.447081,595.380 -OK106,06/15/2020 23:47,-38.109690,176.447081,595.380 -OK107,06/15/2020 23:59,-38.105259,176.443636,569.412 -OK107,06/16/2020 00:00,-38.105259,176.443636,569.412 -OK107,06/16/2020 00:01,-38.105259,176.443636,569.412 -OK101,06/16/2020 00:19,-38.095606,176.464133,611.804 -OK101,06/16/2020 00:20,-38.095606,176.464133,611.804 -OK101,06/16/2020 00:21,-38.095606,176.464133,611.804 -OK108,06/16/2020 00:35,-38.089592,176.462151,570.424 -OK108,06/16/2020 00:36,-38.089592,176.462151,570.424 -OK108,06/16/2020 00:37,-38.089592,176.462151,570.424 -OK100,06/16/2020 00:47,-38.085725,176.461508,571.934 -OK100,06/16/2020 00:48,-38.085725,176.461508,571.934 -OK100,06/16/2020 00:49,-38.085725,176.461508,571.934 -OK109,06/16/2020 01:01,-38.090300,176.473812,574.586 -OK109,06/16/2020 01:02,-38.090300,176.473812,574.586 -OK109,06/16/2020 01:03,-38.090300,176.473812,574.586 -OK110,06/16/2020 01:27,-38.089956,176.509779,724.067 -OK110,06/16/2020 01:28,-38.089956,176.509779,724.067 -OK110,06/16/2020 01:29,-38.089956,176.509779,724.067 -OK111,06/16/2020 01:44,-38.092895,176.507268,766.928 -OK111,06/16/2020 01:45,-38.092895,176.507268,766.928 -OK111,06/16/2020 01:46,-38.092895,176.507268,766.928 -OK112,06/16/2020 01:57,-38.098088,176.503781,745.889 -OK112,06/16/2020 01:58,-38.098088,176.503781,745.889 -OK112,06/16/2020 01:59,-38.098088,176.503781,745.889 -OK113,06/16/2020 02:10,-38.101380,176.501816,699.390 -OK113,06/16/2020 02:11,-38.101380,176.501816,699.390 -OK113,06/16/2020 02:12,-38.101380,176.501816,699.390 -OK110,06/16/2020 02:33,-38.089956,176.509779,724.067 -OK110,06/16/2020 02:34,-38.089956,176.509779,724.067 -OK110,06/16/2020 02:35,-38.089956,176.509779,724.067 -OK110,06/16/2020 02:36,-38.089956,176.509779,724.067 -OK114,06/16/2020 02:49,-38.095670,176.514649,676.003 -OK114,06/16/2020 02:51,-38.095670,176.514649,676.003 -OK114,06/16/2020 02:52,-38.095670,176.514649,676.003 -OK100,06/16/2020 03:17,-38.085725,176.461508,571.934 -OK100,06/16/2020 03:18,-38.085725,176.461508,571.934 -OK100,06/16/2020 03:19,-38.085725,176.461508,571.934 -OK115,06/16/2020 03:48,-38.072775,176.445752,531.606 -OK115,06/16/2020 03:49,-38.072775,176.445752,531.606 -OK115,06/16/2020 03:50,-38.072775,176.445752,531.606 -AGUP,06/16/2020 04:13,-38.054648,176.441513,284.462 -AGUP,06/16/2020 04:14,-38.054648,176.441513,284.462 -AGUP,06/16/2020 04:15,-38.054648,176.441513,284.462 -AGUP,06/16/2020 04:16,-38.054648,176.441513,284.462 -AGUP,06/16/2020 20:24,-38.054648,176.441513,284.462 -AGUP,06/16/2020 20:25,-38.054648,176.441513,284.462 -AGUP,06/16/2020 20:26,-38.054648,176.441513,284.462 -AGUP,06/16/2020 20:27,-38.054648,176.441513,284.462 -AGUP,06/16/2020 20:28,-38.054648,176.441513,284.462 -OK116,06/16/2020 20:47,-38.058756,176.439179,358.501 -OK116,06/16/2020 20:48,-38.058756,176.439179,358.501 -OK116,06/16/2020 20:49,-38.058756,176.439179,358.501 -OK117,06/16/2020 21:03,-38.068929,176.437052,483.316 -OK117,06/16/2020 21:04,-38.068929,176.437052,483.316 -OK117,06/16/2020 21:05,-38.068929,176.437052,483.316 -OK118,06/16/2020 21:16,-38.073657,176.441712,522.573 -OK118,06/16/2020 21:17,-38.073657,176.441712,522.573 -OK118,06/16/2020 21:18,-38.073657,176.441712,522.573 -OK115,06/16/2020 21:24,-38.072775,176.445752,531.606 -OK115,06/16/2020 21:26,-38.072775,176.445752,531.606 -OK115,06/16/2020 21:27,-38.072775,176.445752,531.606 -OK120,06/16/2020 21:58,-38.082193,176.443110,557.141 -OK120,06/16/2020 21:59,-38.082193,176.443110,557.141 -OK120,06/16/2020 22:00,-38.082193,176.443110,557.141 -OK121,06/16/2020 22:22,-38.081916,176.450346,560.335 -OK121,06/16/2020 22:23,-38.081916,176.450346,560.335 -OK121,06/16/2020 22:24,-38.081916,176.450346,560.335 -OK121,06/16/2020 22:25,-38.081916,176.450346,560.335 -OK100,06/16/2020 22:59,-38.085725,176.461508,571.934 -OK100,06/16/2020 23:00,-38.085725,176.461508,571.934 -OK100,06/16/2020 23:01,-38.085725,176.461508,571.934 -OK122,06/16/2020 23:31,-38.145088,176.547568,138.257 -OK122,06/16/2020 23:32,-38.145088,176.547568,138.257 -OK122,06/16/2020 23:33,-38.145088,176.547568,138.257 -OK123,06/16/2020 23:48,-38.147439,176.529044,151.811 -OK123,06/16/2020 23:49,-38.147439,176.529044,151.811 -OK123,06/16/2020 23:50,-38.147439,176.529044,151.811 -OK124,06/16/2020 23:58,-38.152082,176.523221,165.996 -OK124,06/16/2020 23:59,-38.152082,176.523221,165.996 -OK124,06/17/2020 00:00,-38.152082,176.523221,165.996 -OK125,06/17/2020 00:32,-38.161871,176.556924,269.597 -OK125,06/17/2020 00:33,-38.161871,176.556924,269.597 -OK125,06/17/2020 00:34,-38.161871,176.556924,269.597 -OK126,06/17/2020 00:48,-38.159109,176.567787,275.636 -OK126,06/17/2020 00:49,-38.159109,176.567787,275.636 -OK126,06/17/2020 00:50,-38.159109,176.567787,275.636 -OK127,06/17/2020 01:11,-38.164775,176.539095,328.887 -OK127,06/17/2020 01:12,-38.164775,176.539095,328.887 -OK127,06/17/2020 01:13,-38.164775,176.539095,328.887 -OK128,06/17/2020 01:28,-38.171419,176.550971,321.940 -OK128,06/17/2020 01:29,-38.171419,176.550971,321.940 -OK128,06/17/2020 01:30,-38.171419,176.550971,321.940 -OK129,06/17/2020 01:43,-38.178758,176.546944,324.494 -OK129,06/17/2020 01:44,-38.178758,176.546944,324.494 -OK129,06/17/2020 01:45,-38.178758,176.546944,324.494 -OK130,06/17/2020 01:56,-38.188041,176.536996,315.136 -OK130,06/17/2020 01:58,-38.188041,176.536996,315.136 -OK130,06/17/2020 01:59,-38.188041,176.536996,315.136 -OK131,06/17/2020 02:14,-38.174561,176.533057,325.182 -OK131,06/17/2020 02:15,-38.174561,176.533057,325.182 -OK131,06/17/2020 02:16,-38.174561,176.533057,325.182 -OK100,06/17/2020 02:50,-38.085725,176.461508,571.934 -OK100,06/17/2020 02:51,-38.085725,176.461508,571.934 -OK100,06/17/2020 02:52,-38.085725,176.461508,571.934 -OK100,06/17/2020 02:53,-38.085725,176.461508,571.934 -OK116,06/17/2020 03:16,-38.058756,176.439179,358.501 -OK116,06/17/2020 03:17,-38.058756,176.439179,358.501 -OK116,06/17/2020 03:18,-38.058756,176.439179,358.501 -AGUP,06/17/2020 03:27,-38.054648,176.441513,284.462 -AGUP,06/17/2020 03:28,-38.054648,176.441513,284.462 -AGUP,06/17/2020 03:29,-38.054648,176.441513,284.462 -AGUP,06/17/2020 03:30,-38.054648,176.441513,284.462 -AGUP,06/23/2020 23:09,-38.054648,176.441513,284.462 -AGUP,06/23/2020 23:10,-38.054648,176.441513,284.462 -OK133,06/24/2020 00:19,-38.181733,176.529888,358.585 -OK133,06/24/2020 00:20,-38.181733,176.529888,358.585 -OK134,06/24/2020 00:40,-38.187150,176.516708,383.824 -OK134,06/24/2020 00:41,-38.187150,176.516708,383.824 -OK134,06/24/2020 00:42,-38.187150,176.516708,383.824 -OK135,06/24/2020 00:58,-38.193711,176.518258,417.043 -OK135,06/24/2020 00:59,-38.193711,176.518258,417.043 -OK136,06/24/2020 01:13,-38.190659,176.512660,394.510 -OK136,06/24/2020 01:14,-38.190659,176.512660,394.510 -OK133,06/24/2020 01:40,-38.181733,176.529888,358.585 -OK133,06/24/2020 01:41,-38.181733,176.529888,358.585 -OK137,06/24/2020 02:04,-38.201600,176.556378,400.296 -OK137,06/24/2020 02:05,-38.201600,176.556378,400.296 -OK138,06/24/2020 02:31,-38.215092,176.559455,536.408 -OK138,06/24/2020 02:32,-38.215092,176.559455,536.408 -OK139,06/24/2020 02:48,-38.219739,176.556725,517.360 -OK139,06/24/2020 02:49,-38.219739,176.556725,517.360 -OK140,06/24/2020 03:05,-38.211466,176.553502,444.138 -OK140,06/24/2020 03:06,-38.211466,176.553502,444.138 -OK141,06/24/2020 03:16,-38.206190,176.559453,402.706 -OK141,06/24/2020 03:17,-38.206190,176.559453,402.706 -OK137,06/24/2020 03:26,-38.201600,176.556378,400.296 -OK137,06/24/2020 03:27,-38.201600,176.556378,400.296 -AGUP,06/24/2020 04:55,-38.054648,176.441513,284.462 -AGUP,06/24/2020 04:56,-38.054648,176.441513,284.462 -AGUP,07/01/2020 20:28,-38.054648,176.441513,284.462 -AGUP,07/01/2020 20:29,-38.054648,176.441513,284.462 -AGUP,07/01/2020 20:30,-38.054648,176.441513,284.462 -AGUP,07/01/2020 20:31,-38.054648,176.441513,284.462 -OK138A,07/01/2020 21:45,-38.195393,176.603737,193.810 -OK138A,07/01/2020 21:46,-38.195393,176.603737,193.810 -OK138A,07/01/2020 21:47,-38.195393,176.603737,193.810 -OK139A,07/01/2020 21:58,-38.192269,176.612474,190.458 -OK139A,07/01/2020 21:59,-38.192269,176.612474,190.458 -OK139A,07/01/2020 22:00,-38.192269,176.612474,190.458 -OK140A,07/01/2020 22:24,-38.188830,176.618648,215.986 -OK140A,07/01/2020 22:25,-38.188830,176.618648,215.986 -OK140A,07/01/2020 22:26,-38.188830,176.618648,215.986 -OK141A,07/01/2020 23:01,-38.212563,176.634155,366.801 -OK141A,07/01/2020 23:02,-38.212563,176.634155,366.801 -OK141A,07/01/2020 23:03,-38.212563,176.634155,366.801 -OK141A,07/01/2020 23:04,-38.212563,176.634155,366.801 -OK142,07/01/2020 23:29,-38.230168,176.632750,423.289 -OK142,07/01/2020 23:30,-38.230168,176.632750,423.289 -OK142,07/01/2020 23:31,-38.230168,176.632750,423.289 -OK143,07/01/2020 23:43,-38.239846,176.624572,409.859 -OK143,07/01/2020 23:45,-38.239846,176.624572,409.859 -OK143,07/01/2020 23:46,-38.239846,176.624572,409.859 -OK144,07/01/2020 23:58,-38.246316,176.618187,453.636 -OK144,07/01/2020 23:59,-38.246316,176.618187,453.636 -OK144,07/02/2020 00:00,-38.246316,176.618187,453.636 -OK145,07/02/2020 00:12,-38.251962,176.613961,445.032 -OK145,07/02/2020 00:13,-38.251962,176.613961,445.032 -OK145,07/02/2020 00:14,-38.251962,176.613961,445.032 -OK146,07/02/2020 00:30,-38.264961,176.597951,489.019 -OK146,07/02/2020 00:31,-38.264961,176.597951,489.019 -OK146,07/02/2020 00:32,-38.264961,176.597951,489.019 -OK146,07/02/2020 00:34,-38.264961,176.597951,489.019 -OK147,07/02/2020 01:14,-38.246345,176.596586,478.170 -OK147,07/02/2020 01:15,-38.246345,176.596586,478.170 -OK147,07/02/2020 01:16,-38.246345,176.596586,478.170 -OK148,07/02/2020 01:32,-38.240404,176.606183,470.007 -OK148,07/02/2020 01:33,-38.240404,176.606183,470.007 -OK148,07/02/2020 01:34,-38.240404,176.606183,470.007 -OK146,07/02/2020 01:49,-38.264961,176.597951,489.019 -OK146,07/02/2020 01:50,-38.264961,176.597951,489.019 -OK146,07/02/2020 01:51,-38.264961,176.597951,489.019 -OK144,07/02/2020 02:02,-38.246316,176.618187,453.636 -OK144,07/02/2020 02:03,-38.246316,176.618187,453.636 -OK144,07/02/2020 02:04,-38.246316,176.618187,453.636 -OK141A,07/02/2020 02:21,-38.212563,176.634155,366.801 -OK141A,07/02/2020 02:22,-38.212563,176.634155,366.801 -OK141A,07/02/2020 02:23,-38.212563,176.634155,366.801 -OK149,07/02/2020 02:35,-38.208633,176.625981,348.487 -OK149,07/02/2020 02:36,-38.208633,176.625981,348.487 -OK149,07/02/2020 02:37,-38.208633,176.625981,348.487 -OK150,07/02/2020 02:49,-38.202049,176.625910,308.349 -OK150,07/02/2020 02:50,-38.202049,176.625910,308.349 -OK150,07/02/2020 02:51,-38.202049,176.625910,308.349 -OK151,07/02/2020 03:07,-38.196244,176.614920,199.383 -OK151,07/02/2020 03:08,-38.196244,176.614920,199.383 -OK151,07/02/2020 03:09,-38.196244,176.614920,199.383 -OK139A,07/02/2020 03:15,-38.192269,176.612474,190.458 -OK139A,07/02/2020 03:16,-38.192269,176.612474,190.458 -OK139A,07/02/2020 03:17,-38.192269,176.612474,190.458 -OK100,07/02/2020 03:53,-38.085725,176.461508,571.934 -OK100,07/02/2020 03:54,-38.085725,176.461508,571.934 -OK100,07/02/2020 03:55,-38.085725,176.461508,571.934 -AGUP,07/02/2020 04:11,-38.054648,176.441513,284.462 -AGUP,07/02/2020 04:12,-38.054648,176.441513,284.462 -AGUP,07/02/2020 04:14,-38.054648,176.441513,284.462 -AGUP,07/02/2020 04:16,-38.054648,176.441513,284.462 -AGUP,07/02/2020 04:17,-38.054648,176.441513,284.462 -AGUP,07/09/2020 22:00,-38.054648,176.441513,284.462 -AGUP,07/09/2020 22:01,-38.054648,176.441513,284.462 -AGUP,07/09/2020 22:02,-38.054648,176.441513,284.462 -OK137,07/09/2020 22:53,-38.201600,176.556378,400.296 -OK137,07/09/2020 22:54,-38.201600,176.556378,400.296 -OK137,07/09/2020 22:55,-38.201600,176.556378,400.296 -OK171,07/09/2020 23:31,-38.210813,176.577778,417.779 -OK171,07/09/2020 23:32,-38.210813,176.577778,417.779 -OK171,07/09/2020 23:33,-38.210813,176.577778,417.779 -OK172,07/10/2020 00:25,-38.198771,176.564368,380.106 -OK172,07/10/2020 00:26,-38.198771,176.564368,380.106 -OK172,07/10/2020 00:27,-38.198771,176.564368,380.106 -OK137,07/10/2020 00:38,-38.201600,176.556378,400.296 -OK137,07/10/2020 00:39,-38.201600,176.556378,400.296 -OK137,07/10/2020 00:40,-38.201600,176.556378,400.296 -OK174,07/10/2020 01:11,-38.195509,176.585421,317.670 -OK174,07/10/2020 01:12,-38.195509,176.585421,317.670 -OK174,07/10/2020 01:13,-38.195509,176.585421,317.670 -OK176,07/10/2020 01:34,-38.195406,176.601072,211.301 -OK176,07/10/2020 01:35,-38.195406,176.601072,211.301 -OK176,07/10/2020 01:36,-38.195406,176.601072,211.301 -OK172,07/10/2020 01:56,-38.198771,176.564368,380.106 -OK172,07/10/2020 01:57,-38.198771,176.564368,380.106 -OK172,07/10/2020 01:58,-38.198771,176.564368,380.106 -AGUP,07/10/2020 02:56,-38.054648,176.441513,284.462 -AGUP,07/10/2020 02:57,-38.054648,176.441513,284.462 -AGUP,07/10/2020 02:58,-38.054648,176.441513,284.462 -AGUP,07/13/2020 20:28,-38.054648,176.441513,284.462 -AGUP,07/13/2020 20:29,-38.054648,176.441513,284.462 -AGUP,07/13/2020 20:30,-38.054648,176.441513,284.462 -AGUP,07/13/2020 20:31,-38.054648,176.441513,284.462 -OK138A,07/13/2020 21:18,-38.195393,176.603737,193.810 -OK138A,07/13/2020 21:19,-38.195393,176.603737,193.810 -OK138A,07/13/2020 21:20,-38.195393,176.603737,193.810 -OK177,07/13/2020 22:13,-38.233895,176.611976,442.387 -OK177,07/13/2020 22:15,-38.233895,176.611976,442.387 -OK177,07/13/2020 22:16,-38.233895,176.611976,442.387 -OK178,07/13/2020 22:33,-38.228040,176.616384,387.498 -OK178,07/13/2020 22:34,-38.228040,176.616384,387.498 -OK178,07/13/2020 22:35,-38.228040,176.616384,387.498 -OK179,07/13/2020 22:49,-38.215902,176.616529,390.535 -OK179,07/13/2020 22:50,-38.215902,176.616529,390.535 -OK179,07/13/2020 22:51,-38.215902,176.616529,390.535 -OK177,07/13/2020 23:02,-38.233895,176.611976,442.387 -OK177,07/13/2020 23:03,-38.233895,176.611976,442.387 -OK177,07/13/2020 23:04,-38.233895,176.611976,442.387 -OK148,07/13/2020 23:11,-38.240404,176.606183,470.007 -OK148,07/13/2020 23:12,-38.240404,176.606183,470.007 -OK148,07/13/2020 23:13,-38.240404,176.606183,470.007 -OK180,07/13/2020 23:27,-38.234471,176.601010,448.107 -OK180,07/13/2020 23:28,-38.234471,176.601010,448.107 -OK180,07/13/2020 23:30,-38.234471,176.601010,448.107 -OK181,07/14/2020 00:15,-38.240445,176.587644,415.143 -OK181,07/14/2020 00:16,-38.240445,176.587644,415.143 -OK181,07/14/2020 00:17,-38.240445,176.587644,415.143 -OK146,07/14/2020 00:49,-38.264961,176.597951,489.019 -OK146,07/14/2020 00:50,-38.264961,176.597951,489.019 -OK146,07/14/2020 00:52,-38.264961,176.597951,489.019 -OK141A,07/14/2020 01:10,-38.212563,176.634155,366.801 -OK141A,07/14/2020 01:11,-38.212563,176.634155,366.801 -OK141A,07/14/2020 01:12,-38.212563,176.634155,366.801 -OK182,07/14/2020 01:46,-38.204861,176.603277,331.123 -OK182,07/14/2020 01:47,-38.204861,176.603277,331.123 -OK182,07/14/2020 01:48,-38.204861,176.603277,331.123 -OK183,07/14/2020 02:51,-38.045402,176.612645,400.326 -OK183,07/14/2020 02:52,-38.045402,176.612645,400.326 -OK183,07/14/2020 02:53,-38.045402,176.612645,400.326 -OK184,07/14/2020 03:01,-38.039413,176.619614,435.705 -OK184,07/14/2020 03:02,-38.039413,176.619614,435.705 -OK184,07/14/2020 03:03,-38.039413,176.619614,435.705 -OK184,07/14/2020 03:04,-38.039413,176.619614,435.705 -OK185,07/14/2020 03:15,-38.027859,176.622243,411.981 -OK185,07/14/2020 03:16,-38.027859,176.622243,411.981 -OK185,07/14/2020 03:17,-38.027859,176.622243,411.981 -OK186,07/14/2020 03:26,-38.017519,176.620095,381.278 -OK186,07/14/2020 03:27,-38.017519,176.620095,381.278 -OK186,07/14/2020 03:28,-38.017519,176.620095,381.278 -OK185,07/14/2020 03:40,-38.027859,176.622243,411.981 -OK185,07/14/2020 03:41,-38.027859,176.622243,411.981 -OK185,07/14/2020 03:42,-38.027859,176.622243,411.981 -OK187,07/14/2020 04:09,-38.032377,176.486226,308.838 -OK187,07/14/2020 04:10,-38.032377,176.486226,308.838 -OK187,07/14/2020 04:11,-38.032377,176.486226,308.838 -AGUP,07/14/2020 04:22,-38.054648,176.441513,284.462 -AGUP,07/14/2020 04:23,-38.054648,176.441513,284.462 -AGUP,07/14/2020 04:24,-38.054648,176.441513,284.462 -AGUP,07/14/2020 04:25,-38.054648,176.441513,284.462 -AGUP,08/03/2020 21:04,-38.054648,176.441513,284.462 -AGUP,08/03/2020 21:05,-38.054648,176.441513,284.462 -AGUP,08/03/2020 21:07,-38.054648,176.441513,284.462 -OK188,08/03/2020 21:39,-38.093211,176.358686,478.667 -OK188,08/03/2020 21:40,-38.093211,176.358686,478.667 -OK188,08/03/2020 21:41,-38.093211,176.358686,478.667 -OK189,08/03/2020 21:53,-38.095590,176.364162,479.764 -OK189,08/03/2020 21:55,-38.095590,176.364162,479.764 -OK189,08/03/2020 21:56,-38.095590,176.364162,479.764 -OK190,08/03/2020 22:19,-38.094084,176.378021,634.881 -OK190,08/03/2020 22:20,-38.094084,176.378021,634.881 -OK190,08/03/2020 22:21,-38.094084,176.378021,634.881 -OK191,08/03/2020 22:37,-38.099299,176.384629,706.346 -OK191,08/03/2020 22:39,-38.099299,176.384629,706.346 -OK191,08/03/2020 22:40,-38.099299,176.384629,706.346 -OK190,08/03/2020 22:50,-38.094084,176.378021,634.881 -OK190,08/03/2020 22:51,-38.094084,176.378021,634.881 -OK190,08/03/2020 22:52,-38.094084,176.378021,634.881 -OK188,08/03/2020 23:09,-38.093211,176.358686,478.667 -OK188,08/03/2020 23:10,-38.093211,176.358686,478.667 -OK188,08/03/2020 23:11,-38.093211,176.358686,478.667 -OK192,08/03/2020 23:31,-38.121561,176.323775,342.614 -OK192,08/03/2020 23:32,-38.121561,176.323775,342.614 -OK192,08/03/2020 23:33,-38.121561,176.323775,342.614 -OK193,08/03/2020 23:55,-38.142186,176.294049,332.718 -OK193,08/03/2020 23:57,-38.142186,176.294049,332.718 -OK193,08/03/2020 23:58,-38.142186,176.294049,332.718 -OK194,08/04/2020 01:37,-38.183641,176.356454,396.364 -OK194,08/04/2020 01:38,-38.183641,176.356454,396.364 -OK194,08/04/2020 01:39,-38.183641,176.356454,396.364 -OK195,08/04/2020 01:56,-38.185005,176.366806,529.801 -OK195,08/04/2020 01:57,-38.185005,176.366806,529.801 -OK195,08/04/2020 01:58,-38.185005,176.366806,529.801 -OK196,08/04/2020 02:13,-38.189933,176.362674,595.614 -OK196,08/04/2020 02:14,-38.189933,176.362674,595.614 -OK196,08/04/2020 02:15,-38.189933,176.362674,595.614 -OK197,08/04/2020 02:27,-38.195770,176.357778,644.529 -OK197,08/04/2020 02:29,-38.195770,176.357778,644.529 -OK197,08/04/2020 02:30,-38.195770,176.357778,644.529 -OK198,08/04/2020 02:47,-38.201981,176.354741,674.669 -OK198,08/04/2020 02:48,-38.201981,176.354741,674.669 -OK198,08/04/2020 02:49,-38.201981,176.354741,674.669 -OK197,08/04/2020 02:57,-38.195770,176.357778,644.529 -OK197,08/04/2020 02:58,-38.195770,176.357778,644.529 -OK197,08/04/2020 02:59,-38.195770,176.357778,644.529 -OK199,08/04/2020 03:28,-38.189724,176.372250,552.176 -OK199,08/04/2020 03:29,-38.189724,176.372250,552.176 -OK199,08/04/2020 03:30,-38.189724,176.372250,552.176 -OK200,08/04/2020 03:38,-38.195507,176.373097,554.431 -OK200,08/04/2020 03:39,-38.195507,176.373097,554.431 -OK194,08/04/2020 03:56,-38.183641,176.356454,396.364 -OK194,08/04/2020 03:57,-38.183641,176.356454,396.364 -OK194,08/04/2020 03:58,-38.183641,176.356454,396.364 -OK194,08/04/2020 03:59,-38.183641,176.356454,396.364 -AGUP,08/04/2020 04:36,-38.054648,176.441513,284.462 -AGUP,08/04/2020 04:37,-38.054648,176.441513,284.462 -AGUP,08/04/2020 04:38,-38.054648,176.441513,284.462 -AGUP,08/04/2020 04:39,-38.054648,176.441513,284.462 -AGUP,08/04/2020 20:21,-38.054648,176.441513,284.462 -AGUP,08/04/2020 20:22,-38.054648,176.441513,284.462 -AGUP,08/04/2020 20:23,-38.054648,176.441513,284.462 -OK192,08/04/2020 20:44,-38.121561,176.323775,342.614 -OK192,08/04/2020 20:45,-38.121561,176.323775,342.614 -OK192,08/04/2020 20:46,-38.121561,176.323775,342.614 -OK201,08/04/2020 21:03,-38.131740,176.326182,370.204 -OK201,08/04/2020 21:04,-38.131740,176.326182,370.204 -OK201,08/04/2020 21:05,-38.131740,176.326182,370.204 -OK202,08/04/2020 21:16,-38.133073,176.332289,418.035 -OK202,08/04/2020 21:17,-38.133073,176.332289,418.035 -OK202,08/04/2020 21:18,-38.133073,176.332289,418.035 -OK203,08/04/2020 21:27,-38.135381,176.339115,444.017 -OK203,08/04/2020 21:28,-38.135381,176.339115,444.017 -OK203,08/04/2020 21:29,-38.135381,176.339115,444.017 -OK204,08/04/2020 21:41,-38.138838,176.342339,516.432 -OK204,08/04/2020 21:42,-38.138838,176.342339,516.432 -OK204,08/04/2020 21:43,-38.138838,176.342339,516.432 -OK205,08/04/2020 21:53,-38.137376,176.347594,604.821 -OK205,08/04/2020 21:54,-38.137376,176.347594,604.821 -OK205,08/04/2020 21:55,-38.137376,176.347594,604.821 -OK206,08/04/2020 22:14,-38.143476,176.343121,562.832 -OK206,08/04/2020 22:16,-38.143476,176.343121,562.832 -OK206,08/04/2020 22:17,-38.143476,176.343121,562.832 -OK193,08/04/2020 22:46,-38.142186,176.294049,332.718 -OK193,08/04/2020 22:47,-38.142186,176.294049,332.718 -OK193,08/04/2020 22:48,-38.142186,176.294049,332.718 -OK207,08/05/2020 01:11,-38.146965,176.320984,429.110 -OK207,08/05/2020 01:12,-38.146965,176.320984,429.110 -OK207,08/05/2020 01:13,-38.146965,176.320984,429.110 -OK208,08/05/2020 01:25,-38.148213,176.328482,474.397 -OK208,08/05/2020 01:26,-38.148213,176.328482,474.397 -OK208,08/05/2020 01:28,-38.148213,176.328482,474.397 -OK209,08/05/2020 01:44,-38.144801,176.339130,531.496 -OK209,08/05/2020 01:45,-38.144801,176.339130,531.496 -OK209,08/05/2020 01:46,-38.144801,176.339130,531.496 -OK207,08/05/2020 01:59,-38.146965,176.320984,429.110 -OK207,08/05/2020 02:00,-38.146965,176.320984,429.110 -OK207,08/05/2020 02:01,-38.146965,176.320984,429.110 -OK210,08/05/2020 02:54,-38.009821,176.565920,355.129 -OK210,08/05/2020 02:55,-38.009821,176.565920,355.129 -OK210,08/05/2020 02:56,-38.009821,176.565920,355.129 -OK211,08/05/2020 03:16,-38.008722,176.598310,358.532 -OK211,08/05/2020 03:17,-38.008722,176.598310,358.532 -OK211,08/05/2020 03:18,-38.008722,176.598310,358.532 -OK212,08/05/2020 03:26,-38.002019,176.601507,345.520 -OK212,08/05/2020 03:27,-38.002019,176.601507,345.520 -OK212,08/05/2020 03:28,-38.002019,176.601507,345.520 -OK213,08/05/2020 03:38,-38.005984,176.624406,391.087 -OK213,08/05/2020 03:39,-38.005984,176.624406,391.087 -OK213,08/05/2020 03:40,-38.005984,176.624406,391.087 -AGUP,08/05/2020 04:12,-38.054648,176.441513,284.462 -AGUP,08/05/2020 04:13,-38.054648,176.441513,284.462 -AGUP,08/05/2020 04:14,-38.054648,176.441513,284.462 -AGUP,08/05/2020 20:27,-38.054648,176.441513,284.462 -AGUP,08/05/2020 20:28,-38.054648,176.441513,284.462 -AGUP,08/05/2020 20:29,-38.054648,176.441513,284.462 -AGUP,08/05/2020 20:30,-38.054648,176.441513,284.462 -OK214,08/05/2020 21:33,-38.254737,176.340638,620.728 -OK214,08/05/2020 21:34,-38.254737,176.340638,620.728 -OK214,08/05/2020 21:35,-38.254737,176.340638,620.728 -OK215,08/05/2020 21:47,-38.250738,176.340473,626.858 -OK215,08/05/2020 21:48,-38.250738,176.340473,626.858 -OK215,08/05/2020 21:49,-38.250738,176.340473,626.858 -OK216,08/05/2020 22:00,-38.257281,176.351303,607.274 -OK216,08/05/2020 22:01,-38.257281,176.351303,607.274 -OK216,08/05/2020 22:02,-38.257281,176.351303,607.274 -OK217,08/05/2020 22:13,-38.252441,176.358712,629.077 -OK217,08/05/2020 22:14,-38.252441,176.358712,629.077 -OK217,08/05/2020 22:15,-38.252441,176.358712,629.077 -OK218,08/05/2020 22:35,-38.245770,176.359060,616.591 -OK218,08/05/2020 22:36,-38.245770,176.359060,616.591 -OK218,08/05/2020 22:37,-38.245770,176.359060,616.591 -OK217,08/05/2020 22:46,-38.252441,176.358712,629.077 -OK217,08/05/2020 22:47,-38.252441,176.358712,629.077 -OK217,08/05/2020 22:48,-38.252441,176.358712,629.077 -OK219,08/05/2020 23:00,-38.251288,176.371234,604.630 -OK219,08/05/2020 23:01,-38.251288,176.371234,604.630 -OK219,08/05/2020 23:02,-38.251288,176.371234,604.630 -OK220,08/05/2020 23:17,-38.246592,176.382302,558.717 -OK220,08/05/2020 23:18,-38.246592,176.382302,558.717 -OK220,08/05/2020 23:19,-38.246592,176.382302,558.717 -OK221,08/05/2020 23:39,-38.257584,176.380017,390.101 -OK221,08/05/2020 23:40,-38.257584,176.380017,390.101 -OK221,08/05/2020 23:41,-38.257584,176.380017,390.101 -OK222,08/05/2020 23:54,-38.262789,176.374937,401.484 -OK222,08/05/2020 23:55,-38.262789,176.374937,401.484 -OK222,08/05/2020 23:56,-38.262789,176.374937,401.484 -OK223,08/06/2020 00:08,-38.271050,176.374644,454.465 -OK223,08/06/2020 00:09,-38.271050,176.374644,454.465 -OK223,08/06/2020 00:10,-38.271050,176.374644,454.465 -OK224,08/06/2020 00:24,-38.275782,176.372950,497.412 -OK224,08/06/2020 00:25,-38.275782,176.372950,497.412 -OK224,08/06/2020 00:26,-38.275782,176.372950,497.412 -OK221,08/06/2020 00:43,-38.257584,176.380017,390.101 -OK221,08/06/2020 00:44,-38.257584,176.380017,390.101 -OK221,08/06/2020 00:45,-38.257584,176.380017,390.101 -OK214,08/06/2020 01:00,-38.254737,176.340638,620.728 -OK214,08/06/2020 01:01,-38.254737,176.340638,620.728 -OK214,08/06/2020 01:02,-38.254737,176.340638,620.728 -OK193,08/06/2020 01:53,-38.142186,176.294049,332.718 -OK193,08/06/2020 01:54,-38.142186,176.294049,332.718 -OK193,08/06/2020 01:55,-38.142186,176.294049,332.718 -AGUP,08/06/2020 02:23,-38.054648,176.441513,284.462 -AGUP,08/06/2020 02:24,-38.054648,176.441513,284.462 -AGUP,08/06/2020 02:25,-38.054648,176.441513,284.462 +ACNN,03/16/2020 21:08,-38.512209,176.331926,297.958 +ACNN,03/16/2020 21:12,-38.512209,176.331926,297.958 +ACNN,03/16/2020 21:13,-38.512209,176.331926,297.958 +WP93,03/16/2020 22:29,-38.265116,176.564573,467.265 +WP93,03/16/2020 22:30,-38.265116,176.564573,467.265 +WP146,03/16/2020 22:44,-38.268071,176.572404,446.828 +WP146,03/16/2020 22:45,-38.268071,176.572404,446.828 +WP140,03/16/2020 22:56,-38.269557,176.577605,434.051 +WP140,03/16/2020 22:59,-38.269557,176.577605,434.051 +WP140,03/16/2020 23:00,-38.269557,176.577605,434.051 +WP143,03/16/2020 23:24,-38.275345,176.584047,479.006 +WP143,03/16/2020 23:25,-38.275345,176.584047,479.006 +WP143,03/16/2020 23:26,-38.275345,176.584047,479.006 +WP144,03/16/2020 23:55,-38.274621,176.568323,448.606 +WP144,03/16/2020 23:57,-38.274621,176.568323,448.606 +WP144,03/16/2020 23:58,-38.274621,176.568323,448.606 +WP93,03/17/2020 00:13,-38.265116,176.564573,467.265 +WP93,03/17/2020 00:14,-38.265116,176.564573,467.265 +WP98,03/17/2020 00:28,-38.256793,176.571165,457.455 +WP139,03/17/2020 01:04,-38.262333,176.556635,494.174 +WP139,03/17/2020 01:06,-38.262333,176.556635,494.174 +WP139,03/17/2020 01:07,-38.262333,176.556635,494.174 +WP138,03/17/2020 01:15,-38.255525,176.560111,495.930 +WP138,03/17/2020 01:18,-38.255525,176.560111,495.930 +WP138,03/17/2020 01:19,-38.255525,176.560111,495.930 +WP96,03/17/2020 02:16,-38.262988,176.543263,514.214 +WP96,03/17/2020 02:17,-38.262988,176.543263,514.214 +WP93,03/17/2020 02:36,-38.265116,176.564573,467.265 +WP93,03/17/2020 02:37,-38.265116,176.564573,467.265 +ACNN,03/17/2020 03:39,-38.512209,176.331926,297.958 +ACNN,03/17/2020 03:41,-38.512209,176.331926,297.958 +ACNN,03/17/2020 03:42,-38.512209,176.331926,297.958 +ACNN,03/17/2020 20:44,-38.512209,176.331926,297.958 +ACNN,03/17/2020 20:46,-38.512209,176.331926,297.958 +ACNN,03/17/2020 20:47,-38.512209,176.331926,297.958 +WP93,03/17/2020 21:40,-38.265116,176.564573,467.265 +WP93,03/17/2020 21:42,-38.265116,176.564573,467.265 +WP93,03/17/2020 21:43,-38.265116,176.564573,467.265 +WP148,03/17/2020 22:01,-38.290511,176.575760,471.560 +WP148,03/17/2020 22:06,-38.290511,176.575760,471.560 +WP148,03/17/2020 22:07,-38.290511,176.575760,471.560 +WP141,03/17/2020 22:28,-38.281541,176.576093,453.541 +WP141,03/17/2020 22:29,-38.281541,176.576093,453.541 +WP141,03/17/2020 22:30,-38.281541,176.576093,453.541 +WP142,03/17/2020 22:52,-38.281560,176.566694,474.395 +WP142,03/17/2020 22:53,-38.281560,176.566694,474.395 +WP142,03/17/2020 22:54,-38.281560,176.566694,474.395 +WP147,03/17/2020 23:14,-38.293007,176.555309,449.272 +WP147,03/17/2020 23:17,-38.293007,176.555309,449.272 +WP139,03/17/2020 23:39,-38.262333,176.556635,494.174 +WP139,03/17/2020 23:40,-38.262333,176.556635,494.174 +WP93,03/17/2020 23:52,-38.265116,176.564573,467.265 +WP93,03/17/2020 23:54,-38.265116,176.564573,467.265 +WP93,03/17/2020 23:55,-38.265116,176.564573,467.265 +WP99,03/18/2020 00:17,-38.269413,176.550480,475.842 +WP99,03/18/2020 00:20,-38.269413,176.550480,475.842 +WP136,03/18/2020 00:39,-38.274086,176.545530,473.901 +WP136,03/18/2020 00:44,-38.274086,176.545530,473.901 +WP95,03/18/2020 00:58,-38.272596,176.535114,486.335 +WP95,03/18/2020 01:03,-38.272596,176.535114,486.335 +WP95,03/18/2020 01:04,-38.272596,176.535114,486.335 +WP145,03/18/2020 01:43,-38.281429,176.543511,458.052 +WP145,03/18/2020 01:45,-38.281429,176.543511,458.052 +WP145,03/18/2020 01:46,-38.281429,176.543511,458.052 +WP93,03/18/2020 02:03,-38.265116,176.564573,467.265 +WP93,03/18/2020 02:07,-38.265116,176.564573,467.265 +WP93,03/18/2020 02:08,-38.265116,176.564573,467.265 +WP149,03/18/2020 02:26,-38.307750,176.558839,445.231 +WP149,03/18/2020 02:29,-38.307750,176.558839,445.231 +WP149,03/18/2020 02:30,-38.307750,176.558839,445.231 +ACNN,03/18/2020 03:10,-38.512209,176.331926,297.958 +ACNN,03/18/2020 03:15,-38.512209,176.331926,297.958 +AGUP,06/10/2020 23:48,-38.054648,176.441513,284.462 +AGUP,06/10/2020 23:50,-38.054648,176.441513,284.462 +OG32,06/11/2020 00:56,-38.122774,176.475586,820.424 +OG32,06/11/2020 00:57,-38.122774,176.475586,820.424 +OG36,06/11/2020 01:55,-38.138471,176.467108,926.489 +OG36,06/11/2020 01:57,-38.138471,176.467108,926.489 +OG37,06/11/2020 02:23,-38.138224,176.459507,854.027 +OG37,06/11/2020 02:24,-38.138224,176.459507,854.027 +OG38,06/11/2020 02:44,-38.134208,176.461352,851.227 +OG38,06/11/2020 02:45,-38.134208,176.461352,851.227 +OG34,06/11/2020 03:12,-38.131201,176.474881,800.569 +OG34,06/11/2020 03:13,-38.131201,176.474881,800.569 +OG34,06/11/2020 03:14,-38.131201,176.474881,800.569 +OG33,06/11/2020 03:32,-38.128162,176.479851,837.787 +OG33,06/11/2020 03:33,-38.128162,176.479851,837.787 +OG32,06/11/2020 03:43,-38.122774,176.475586,820.424 +OG32,06/11/2020 03:44,-38.122774,176.475586,820.424 +AGUP,06/11/2020 04:35,-38.054648,176.441513,284.462 +AGUP,06/11/2020 04:36,-38.054648,176.441513,284.462 +AGUP,06/11/2020 23:06,-38.054648,176.441513,284.462 +AGUP,06/11/2020 23:08,-38.054648,176.441513,284.462 +AGUP,06/11/2020 23:10,-38.054648,176.441513,284.462 +OG29,06/12/2020 00:36,-38.090789,176.456845,591.652 +OG29,06/12/2020 00:37,-38.090789,176.456845,591.652 +OG29,06/12/2020 00:38,-38.090789,176.456845,591.652 +OG16,06/12/2020 00:51,-38.096062,176.456555,620.377 +OG16,06/12/2020 00:53,-38.096062,176.456555,620.377 +OG16,06/12/2020 00:54,-38.096062,176.456555,620.377 +OG09,06/12/2020 01:05,-38.094752,176.447202,646.313 +OG09,06/12/2020 01:06,-38.094752,176.447202,646.313 +OG07,06/12/2020 01:21,-38.086364,176.446698,592.772 +OG05,06/12/2020 01:34,-38.083088,176.456330,545.289 +OG05,06/12/2020 01:35,-38.083088,176.456330,545.289 +OG05,06/12/2020 01:36,-38.083088,176.456330,545.289 +OG29,06/12/2020 01:52,-38.090789,176.456845,591.652 +OG29,06/12/2020 01:53,-38.090789,176.456845,591.652 +OG32,06/12/2020 02:32,-38.122774,176.475586,820.424 +OG32,06/12/2020 02:33,-38.122774,176.475586,820.424 +OG31,06/12/2020 02:45,-38.117953,176.479049,756.718 +OG31,06/12/2020 02:46,-38.117953,176.479049,756.718 +OG20,06/12/2020 03:03,-38.115873,176.472735,673.949 +OG20,06/12/2020 03:04,-38.115873,176.472735,673.949 +OG20,06/12/2020 03:05,-38.115873,176.472735,673.949 +OG28,06/12/2020 03:20,-38.105198,176.474171,662.676 +OG28,06/12/2020 03:21,-38.105198,176.474171,662.676 +OG27,06/12/2020 03:34,-38.099356,176.468858,628.158 +OG27,06/12/2020 03:35,-38.099356,176.468858,628.158 +OG27,06/12/2020 03:36,-38.099356,176.468858,628.158 +OG05,06/12/2020 03:54,-38.083088,176.456330,545.289 +OG05,06/12/2020 03:55,-38.083088,176.456330,545.289 +OG05,06/12/2020 03:56,-38.083088,176.456330,545.289 +AGUP,06/12/2020 04:16,-38.054648,176.441513,284.462 +AGUP,06/12/2020 04:18,-38.054648,176.441513,284.462 +AGUP,06/12/2020 04:19,-38.054648,176.441513,284.462 +AGUP,06/15/2020 20:18,-38.054648,176.441513,284.462 +AGUP,06/15/2020 20:20,-38.054648,176.441513,284.462 +AGUP,06/15/2020 20:21,-38.054648,176.441513,284.462 +AGUP,06/15/2020 20:22,-38.054648,176.441513,284.462 +OK100,06/15/2020 21:02,-38.085725,176.461508,571.934 +OK100,06/15/2020 21:03,-38.085725,176.461508,571.934 +OK100,06/15/2020 21:04,-38.085725,176.461508,571.934 +OK100,06/15/2020 21:05,-38.085725,176.461508,571.934 +OK101,06/15/2020 21:29,-38.095606,176.464133,611.804 +OK101,06/15/2020 21:30,-38.095606,176.464133,611.804 +OK101,06/15/2020 21:31,-38.095606,176.464133,611.804 +OK101,06/15/2020 21:32,-38.095606,176.464133,611.804 +OK103,06/15/2020 22:13,-38.126974,176.457878,817.623 +OK103,06/15/2020 22:14,-38.126974,176.457878,817.623 +OK103,06/15/2020 22:16,-38.126974,176.457878,817.623 +OK103,06/15/2020 22:17,-38.126974,176.457878,817.623 +OK102,06/15/2020 22:55,-38.116363,176.464753,653.245 +OK102,06/15/2020 22:56,-38.116363,176.464753,653.245 +OK102,06/15/2020 22:57,-38.116363,176.464753,653.245 +OK104,06/15/2020 23:08,-38.116836,176.459708,653.269 +OK104,06/15/2020 23:09,-38.116836,176.459708,653.269 +OK104,06/15/2020 23:10,-38.116836,176.459708,653.269 +OK105,06/15/2020 23:22,-38.113316,176.450891,624.164 +OK105,06/15/2020 23:23,-38.113316,176.450891,624.164 +OK105,06/15/2020 23:24,-38.113316,176.450891,624.164 +OK105,06/15/2020 23:25,-38.113316,176.450891,624.164 +OK106,06/15/2020 23:45,-38.109690,176.447081,595.380 +OK106,06/15/2020 23:46,-38.109690,176.447081,595.380 +OK106,06/15/2020 23:47,-38.109690,176.447081,595.380 +OK107,06/15/2020 23:59,-38.105259,176.443636,569.412 +OK107,06/16/2020 00:00,-38.105259,176.443636,569.412 +OK107,06/16/2020 00:01,-38.105259,176.443636,569.412 +OK101,06/16/2020 00:19,-38.095606,176.464133,611.804 +OK101,06/16/2020 00:20,-38.095606,176.464133,611.804 +OK101,06/16/2020 00:21,-38.095606,176.464133,611.804 +OK108,06/16/2020 00:35,-38.089592,176.462151,570.424 +OK108,06/16/2020 00:36,-38.089592,176.462151,570.424 +OK108,06/16/2020 00:37,-38.089592,176.462151,570.424 +OK100,06/16/2020 00:47,-38.085725,176.461508,571.934 +OK100,06/16/2020 00:48,-38.085725,176.461508,571.934 +OK100,06/16/2020 00:49,-38.085725,176.461508,571.934 +OK109,06/16/2020 01:01,-38.090300,176.473812,574.586 +OK109,06/16/2020 01:02,-38.090300,176.473812,574.586 +OK109,06/16/2020 01:03,-38.090300,176.473812,574.586 +OK110,06/16/2020 01:27,-38.089956,176.509779,724.067 +OK110,06/16/2020 01:28,-38.089956,176.509779,724.067 +OK110,06/16/2020 01:29,-38.089956,176.509779,724.067 +OK111,06/16/2020 01:44,-38.092895,176.507268,766.928 +OK111,06/16/2020 01:45,-38.092895,176.507268,766.928 +OK111,06/16/2020 01:46,-38.092895,176.507268,766.928 +OK112,06/16/2020 01:57,-38.098088,176.503781,745.889 +OK112,06/16/2020 01:58,-38.098088,176.503781,745.889 +OK112,06/16/2020 01:59,-38.098088,176.503781,745.889 +OK113,06/16/2020 02:10,-38.101380,176.501816,699.390 +OK113,06/16/2020 02:11,-38.101380,176.501816,699.390 +OK113,06/16/2020 02:12,-38.101380,176.501816,699.390 +OK110,06/16/2020 02:33,-38.089956,176.509779,724.067 +OK110,06/16/2020 02:34,-38.089956,176.509779,724.067 +OK110,06/16/2020 02:35,-38.089956,176.509779,724.067 +OK110,06/16/2020 02:36,-38.089956,176.509779,724.067 +OK114,06/16/2020 02:49,-38.095670,176.514649,676.003 +OK114,06/16/2020 02:51,-38.095670,176.514649,676.003 +OK114,06/16/2020 02:52,-38.095670,176.514649,676.003 +OK100,06/16/2020 03:17,-38.085725,176.461508,571.934 +OK100,06/16/2020 03:18,-38.085725,176.461508,571.934 +OK100,06/16/2020 03:19,-38.085725,176.461508,571.934 +OK115,06/16/2020 03:48,-38.072775,176.445752,531.606 +OK115,06/16/2020 03:49,-38.072775,176.445752,531.606 +OK115,06/16/2020 03:50,-38.072775,176.445752,531.606 +AGUP,06/16/2020 04:13,-38.054648,176.441513,284.462 +AGUP,06/16/2020 04:14,-38.054648,176.441513,284.462 +AGUP,06/16/2020 04:15,-38.054648,176.441513,284.462 +AGUP,06/16/2020 04:16,-38.054648,176.441513,284.462 +AGUP,06/16/2020 20:24,-38.054648,176.441513,284.462 +AGUP,06/16/2020 20:25,-38.054648,176.441513,284.462 +AGUP,06/16/2020 20:26,-38.054648,176.441513,284.462 +AGUP,06/16/2020 20:27,-38.054648,176.441513,284.462 +AGUP,06/16/2020 20:28,-38.054648,176.441513,284.462 +OK116,06/16/2020 20:47,-38.058756,176.439179,358.501 +OK116,06/16/2020 20:48,-38.058756,176.439179,358.501 +OK116,06/16/2020 20:49,-38.058756,176.439179,358.501 +OK117,06/16/2020 21:03,-38.068929,176.437052,483.316 +OK117,06/16/2020 21:04,-38.068929,176.437052,483.316 +OK117,06/16/2020 21:05,-38.068929,176.437052,483.316 +OK118,06/16/2020 21:16,-38.073657,176.441712,522.573 +OK118,06/16/2020 21:17,-38.073657,176.441712,522.573 +OK118,06/16/2020 21:18,-38.073657,176.441712,522.573 +OK115,06/16/2020 21:24,-38.072775,176.445752,531.606 +OK115,06/16/2020 21:26,-38.072775,176.445752,531.606 +OK115,06/16/2020 21:27,-38.072775,176.445752,531.606 +OK120,06/16/2020 21:58,-38.082193,176.443110,557.141 +OK120,06/16/2020 21:59,-38.082193,176.443110,557.141 +OK120,06/16/2020 22:00,-38.082193,176.443110,557.141 +OK121,06/16/2020 22:22,-38.081916,176.450346,560.335 +OK121,06/16/2020 22:23,-38.081916,176.450346,560.335 +OK121,06/16/2020 22:24,-38.081916,176.450346,560.335 +OK121,06/16/2020 22:25,-38.081916,176.450346,560.335 +OK100,06/16/2020 22:59,-38.085725,176.461508,571.934 +OK100,06/16/2020 23:00,-38.085725,176.461508,571.934 +OK100,06/16/2020 23:01,-38.085725,176.461508,571.934 +OK122,06/16/2020 23:31,-38.145088,176.547568,138.257 +OK122,06/16/2020 23:32,-38.145088,176.547568,138.257 +OK122,06/16/2020 23:33,-38.145088,176.547568,138.257 +OK123,06/16/2020 23:48,-38.147439,176.529044,151.811 +OK123,06/16/2020 23:49,-38.147439,176.529044,151.811 +OK123,06/16/2020 23:50,-38.147439,176.529044,151.811 +OK124,06/16/2020 23:58,-38.152082,176.523221,165.996 +OK124,06/16/2020 23:59,-38.152082,176.523221,165.996 +OK124,06/17/2020 00:00,-38.152082,176.523221,165.996 +OK125,06/17/2020 00:32,-38.161871,176.556924,269.597 +OK125,06/17/2020 00:33,-38.161871,176.556924,269.597 +OK125,06/17/2020 00:34,-38.161871,176.556924,269.597 +OK126,06/17/2020 00:48,-38.159109,176.567787,275.636 +OK126,06/17/2020 00:49,-38.159109,176.567787,275.636 +OK126,06/17/2020 00:50,-38.159109,176.567787,275.636 +OK127,06/17/2020 01:11,-38.164775,176.539095,328.887 +OK127,06/17/2020 01:12,-38.164775,176.539095,328.887 +OK127,06/17/2020 01:13,-38.164775,176.539095,328.887 +OK128,06/17/2020 01:28,-38.171419,176.550971,321.940 +OK128,06/17/2020 01:29,-38.171419,176.550971,321.940 +OK128,06/17/2020 01:30,-38.171419,176.550971,321.940 +OK129,06/17/2020 01:43,-38.178758,176.546944,324.494 +OK129,06/17/2020 01:44,-38.178758,176.546944,324.494 +OK129,06/17/2020 01:45,-38.178758,176.546944,324.494 +OK130,06/17/2020 01:56,-38.188041,176.536996,315.136 +OK130,06/17/2020 01:58,-38.188041,176.536996,315.136 +OK130,06/17/2020 01:59,-38.188041,176.536996,315.136 +OK131,06/17/2020 02:14,-38.174561,176.533057,325.182 +OK131,06/17/2020 02:15,-38.174561,176.533057,325.182 +OK131,06/17/2020 02:16,-38.174561,176.533057,325.182 +OK100,06/17/2020 02:50,-38.085725,176.461508,571.934 +OK100,06/17/2020 02:51,-38.085725,176.461508,571.934 +OK100,06/17/2020 02:52,-38.085725,176.461508,571.934 +OK100,06/17/2020 02:53,-38.085725,176.461508,571.934 +OK116,06/17/2020 03:16,-38.058756,176.439179,358.501 +OK116,06/17/2020 03:17,-38.058756,176.439179,358.501 +OK116,06/17/2020 03:18,-38.058756,176.439179,358.501 +AGUP,06/17/2020 03:27,-38.054648,176.441513,284.462 +AGUP,06/17/2020 03:28,-38.054648,176.441513,284.462 +AGUP,06/17/2020 03:29,-38.054648,176.441513,284.462 +AGUP,06/17/2020 03:30,-38.054648,176.441513,284.462 +AGUP,06/23/2020 23:09,-38.054648,176.441513,284.462 +AGUP,06/23/2020 23:10,-38.054648,176.441513,284.462 +OK133,06/24/2020 00:19,-38.181733,176.529888,358.585 +OK133,06/24/2020 00:20,-38.181733,176.529888,358.585 +OK134,06/24/2020 00:40,-38.187150,176.516708,383.824 +OK134,06/24/2020 00:41,-38.187150,176.516708,383.824 +OK134,06/24/2020 00:42,-38.187150,176.516708,383.824 +OK135,06/24/2020 00:58,-38.193711,176.518258,417.043 +OK135,06/24/2020 00:59,-38.193711,176.518258,417.043 +OK136,06/24/2020 01:13,-38.190659,176.512660,394.510 +OK136,06/24/2020 01:14,-38.190659,176.512660,394.510 +OK133,06/24/2020 01:40,-38.181733,176.529888,358.585 +OK133,06/24/2020 01:41,-38.181733,176.529888,358.585 +OK137,06/24/2020 02:04,-38.201600,176.556378,400.296 +OK137,06/24/2020 02:05,-38.201600,176.556378,400.296 +OK138,06/24/2020 02:31,-38.215092,176.559455,536.408 +OK138,06/24/2020 02:32,-38.215092,176.559455,536.408 +OK139,06/24/2020 02:48,-38.219739,176.556725,517.360 +OK139,06/24/2020 02:49,-38.219739,176.556725,517.360 +OK140,06/24/2020 03:05,-38.211466,176.553502,444.138 +OK140,06/24/2020 03:06,-38.211466,176.553502,444.138 +OK141,06/24/2020 03:16,-38.206190,176.559453,402.706 +OK141,06/24/2020 03:17,-38.206190,176.559453,402.706 +OK137,06/24/2020 03:26,-38.201600,176.556378,400.296 +OK137,06/24/2020 03:27,-38.201600,176.556378,400.296 +AGUP,06/24/2020 04:55,-38.054648,176.441513,284.462 +AGUP,06/24/2020 04:56,-38.054648,176.441513,284.462 +AGUP,07/01/2020 20:28,-38.054648,176.441513,284.462 +AGUP,07/01/2020 20:29,-38.054648,176.441513,284.462 +AGUP,07/01/2020 20:30,-38.054648,176.441513,284.462 +AGUP,07/01/2020 20:31,-38.054648,176.441513,284.462 +OK138A,07/01/2020 21:45,-38.195393,176.603737,193.810 +OK138A,07/01/2020 21:46,-38.195393,176.603737,193.810 +OK138A,07/01/2020 21:47,-38.195393,176.603737,193.810 +OK139A,07/01/2020 21:58,-38.192269,176.612474,190.458 +OK139A,07/01/2020 21:59,-38.192269,176.612474,190.458 +OK139A,07/01/2020 22:00,-38.192269,176.612474,190.458 +OK140A,07/01/2020 22:24,-38.188830,176.618648,215.986 +OK140A,07/01/2020 22:25,-38.188830,176.618648,215.986 +OK140A,07/01/2020 22:26,-38.188830,176.618648,215.986 +OK141A,07/01/2020 23:01,-38.212563,176.634155,366.801 +OK141A,07/01/2020 23:02,-38.212563,176.634155,366.801 +OK141A,07/01/2020 23:03,-38.212563,176.634155,366.801 +OK141A,07/01/2020 23:04,-38.212563,176.634155,366.801 +OK142,07/01/2020 23:29,-38.230168,176.632750,423.289 +OK142,07/01/2020 23:30,-38.230168,176.632750,423.289 +OK142,07/01/2020 23:31,-38.230168,176.632750,423.289 +OK143,07/01/2020 23:43,-38.239846,176.624572,409.859 +OK143,07/01/2020 23:45,-38.239846,176.624572,409.859 +OK143,07/01/2020 23:46,-38.239846,176.624572,409.859 +OK144,07/01/2020 23:58,-38.246316,176.618187,453.636 +OK144,07/01/2020 23:59,-38.246316,176.618187,453.636 +OK144,07/02/2020 00:00,-38.246316,176.618187,453.636 +OK145,07/02/2020 00:12,-38.251962,176.613961,445.032 +OK145,07/02/2020 00:13,-38.251962,176.613961,445.032 +OK145,07/02/2020 00:14,-38.251962,176.613961,445.032 +OK146,07/02/2020 00:30,-38.264961,176.597951,489.019 +OK146,07/02/2020 00:31,-38.264961,176.597951,489.019 +OK146,07/02/2020 00:32,-38.264961,176.597951,489.019 +OK146,07/02/2020 00:34,-38.264961,176.597951,489.019 +OK147,07/02/2020 01:14,-38.246345,176.596586,478.170 +OK147,07/02/2020 01:15,-38.246345,176.596586,478.170 +OK147,07/02/2020 01:16,-38.246345,176.596586,478.170 +OK148,07/02/2020 01:32,-38.240404,176.606183,470.007 +OK148,07/02/2020 01:33,-38.240404,176.606183,470.007 +OK148,07/02/2020 01:34,-38.240404,176.606183,470.007 +OK146,07/02/2020 01:49,-38.264961,176.597951,489.019 +OK146,07/02/2020 01:50,-38.264961,176.597951,489.019 +OK146,07/02/2020 01:51,-38.264961,176.597951,489.019 +OK144,07/02/2020 02:02,-38.246316,176.618187,453.636 +OK144,07/02/2020 02:03,-38.246316,176.618187,453.636 +OK144,07/02/2020 02:04,-38.246316,176.618187,453.636 +OK141A,07/02/2020 02:21,-38.212563,176.634155,366.801 +OK141A,07/02/2020 02:22,-38.212563,176.634155,366.801 +OK141A,07/02/2020 02:23,-38.212563,176.634155,366.801 +OK149,07/02/2020 02:35,-38.208633,176.625981,348.487 +OK149,07/02/2020 02:36,-38.208633,176.625981,348.487 +OK149,07/02/2020 02:37,-38.208633,176.625981,348.487 +OK150,07/02/2020 02:49,-38.202049,176.625910,308.349 +OK150,07/02/2020 02:50,-38.202049,176.625910,308.349 +OK150,07/02/2020 02:51,-38.202049,176.625910,308.349 +OK151,07/02/2020 03:07,-38.196244,176.614920,199.383 +OK151,07/02/2020 03:08,-38.196244,176.614920,199.383 +OK151,07/02/2020 03:09,-38.196244,176.614920,199.383 +OK139A,07/02/2020 03:15,-38.192269,176.612474,190.458 +OK139A,07/02/2020 03:16,-38.192269,176.612474,190.458 +OK139A,07/02/2020 03:17,-38.192269,176.612474,190.458 +OK100,07/02/2020 03:53,-38.085725,176.461508,571.934 +OK100,07/02/2020 03:54,-38.085725,176.461508,571.934 +OK100,07/02/2020 03:55,-38.085725,176.461508,571.934 +AGUP,07/02/2020 04:11,-38.054648,176.441513,284.462 +AGUP,07/02/2020 04:12,-38.054648,176.441513,284.462 +AGUP,07/02/2020 04:14,-38.054648,176.441513,284.462 +AGUP,07/02/2020 04:16,-38.054648,176.441513,284.462 +AGUP,07/02/2020 04:17,-38.054648,176.441513,284.462 +AGUP,07/09/2020 22:00,-38.054648,176.441513,284.462 +AGUP,07/09/2020 22:01,-38.054648,176.441513,284.462 +AGUP,07/09/2020 22:02,-38.054648,176.441513,284.462 +OK137,07/09/2020 22:53,-38.201600,176.556378,400.296 +OK137,07/09/2020 22:54,-38.201600,176.556378,400.296 +OK137,07/09/2020 22:55,-38.201600,176.556378,400.296 +OK171,07/09/2020 23:31,-38.210813,176.577778,417.779 +OK171,07/09/2020 23:32,-38.210813,176.577778,417.779 +OK171,07/09/2020 23:33,-38.210813,176.577778,417.779 +OK172,07/10/2020 00:25,-38.198771,176.564368,380.106 +OK172,07/10/2020 00:26,-38.198771,176.564368,380.106 +OK172,07/10/2020 00:27,-38.198771,176.564368,380.106 +OK137,07/10/2020 00:38,-38.201600,176.556378,400.296 +OK137,07/10/2020 00:39,-38.201600,176.556378,400.296 +OK137,07/10/2020 00:40,-38.201600,176.556378,400.296 +OK174,07/10/2020 01:11,-38.195509,176.585421,317.670 +OK174,07/10/2020 01:12,-38.195509,176.585421,317.670 +OK174,07/10/2020 01:13,-38.195509,176.585421,317.670 +OK176,07/10/2020 01:34,-38.195406,176.601072,211.301 +OK176,07/10/2020 01:35,-38.195406,176.601072,211.301 +OK176,07/10/2020 01:36,-38.195406,176.601072,211.301 +OK172,07/10/2020 01:56,-38.198771,176.564368,380.106 +OK172,07/10/2020 01:57,-38.198771,176.564368,380.106 +OK172,07/10/2020 01:58,-38.198771,176.564368,380.106 +AGUP,07/10/2020 02:56,-38.054648,176.441513,284.462 +AGUP,07/10/2020 02:57,-38.054648,176.441513,284.462 +AGUP,07/10/2020 02:58,-38.054648,176.441513,284.462 +AGUP,07/13/2020 20:28,-38.054648,176.441513,284.462 +AGUP,07/13/2020 20:29,-38.054648,176.441513,284.462 +AGUP,07/13/2020 20:30,-38.054648,176.441513,284.462 +AGUP,07/13/2020 20:31,-38.054648,176.441513,284.462 +OK138A,07/13/2020 21:18,-38.195393,176.603737,193.810 +OK138A,07/13/2020 21:19,-38.195393,176.603737,193.810 +OK138A,07/13/2020 21:20,-38.195393,176.603737,193.810 +OK177,07/13/2020 22:13,-38.233895,176.611976,442.387 +OK177,07/13/2020 22:15,-38.233895,176.611976,442.387 +OK177,07/13/2020 22:16,-38.233895,176.611976,442.387 +OK178,07/13/2020 22:33,-38.228040,176.616384,387.498 +OK178,07/13/2020 22:34,-38.228040,176.616384,387.498 +OK178,07/13/2020 22:35,-38.228040,176.616384,387.498 +OK179,07/13/2020 22:49,-38.215902,176.616529,390.535 +OK179,07/13/2020 22:50,-38.215902,176.616529,390.535 +OK179,07/13/2020 22:51,-38.215902,176.616529,390.535 +OK177,07/13/2020 23:02,-38.233895,176.611976,442.387 +OK177,07/13/2020 23:03,-38.233895,176.611976,442.387 +OK177,07/13/2020 23:04,-38.233895,176.611976,442.387 +OK148,07/13/2020 23:11,-38.240404,176.606183,470.007 +OK148,07/13/2020 23:12,-38.240404,176.606183,470.007 +OK148,07/13/2020 23:13,-38.240404,176.606183,470.007 +OK180,07/13/2020 23:27,-38.234471,176.601010,448.107 +OK180,07/13/2020 23:28,-38.234471,176.601010,448.107 +OK180,07/13/2020 23:30,-38.234471,176.601010,448.107 +OK181,07/14/2020 00:15,-38.240445,176.587644,415.143 +OK181,07/14/2020 00:16,-38.240445,176.587644,415.143 +OK181,07/14/2020 00:17,-38.240445,176.587644,415.143 +OK146,07/14/2020 00:49,-38.264961,176.597951,489.019 +OK146,07/14/2020 00:50,-38.264961,176.597951,489.019 +OK146,07/14/2020 00:52,-38.264961,176.597951,489.019 +OK141A,07/14/2020 01:10,-38.212563,176.634155,366.801 +OK141A,07/14/2020 01:11,-38.212563,176.634155,366.801 +OK141A,07/14/2020 01:12,-38.212563,176.634155,366.801 +OK182,07/14/2020 01:46,-38.204861,176.603277,331.123 +OK182,07/14/2020 01:47,-38.204861,176.603277,331.123 +OK182,07/14/2020 01:48,-38.204861,176.603277,331.123 +OK183,07/14/2020 02:51,-38.045402,176.612645,400.326 +OK183,07/14/2020 02:52,-38.045402,176.612645,400.326 +OK183,07/14/2020 02:53,-38.045402,176.612645,400.326 +OK184,07/14/2020 03:01,-38.039413,176.619614,435.705 +OK184,07/14/2020 03:02,-38.039413,176.619614,435.705 +OK184,07/14/2020 03:03,-38.039413,176.619614,435.705 +OK184,07/14/2020 03:04,-38.039413,176.619614,435.705 +OK185,07/14/2020 03:15,-38.027859,176.622243,411.981 +OK185,07/14/2020 03:16,-38.027859,176.622243,411.981 +OK185,07/14/2020 03:17,-38.027859,176.622243,411.981 +OK186,07/14/2020 03:26,-38.017519,176.620095,381.278 +OK186,07/14/2020 03:27,-38.017519,176.620095,381.278 +OK186,07/14/2020 03:28,-38.017519,176.620095,381.278 +OK185,07/14/2020 03:40,-38.027859,176.622243,411.981 +OK185,07/14/2020 03:41,-38.027859,176.622243,411.981 +OK185,07/14/2020 03:42,-38.027859,176.622243,411.981 +OK187,07/14/2020 04:09,-38.032377,176.486226,308.838 +OK187,07/14/2020 04:10,-38.032377,176.486226,308.838 +OK187,07/14/2020 04:11,-38.032377,176.486226,308.838 +AGUP,07/14/2020 04:22,-38.054648,176.441513,284.462 +AGUP,07/14/2020 04:23,-38.054648,176.441513,284.462 +AGUP,07/14/2020 04:24,-38.054648,176.441513,284.462 +AGUP,07/14/2020 04:25,-38.054648,176.441513,284.462 +AGUP,08/03/2020 21:04,-38.054648,176.441513,284.462 +AGUP,08/03/2020 21:05,-38.054648,176.441513,284.462 +AGUP,08/03/2020 21:07,-38.054648,176.441513,284.462 +OK188,08/03/2020 21:39,-38.093211,176.358686,478.667 +OK188,08/03/2020 21:40,-38.093211,176.358686,478.667 +OK188,08/03/2020 21:41,-38.093211,176.358686,478.667 +OK189,08/03/2020 21:53,-38.095590,176.364162,479.764 +OK189,08/03/2020 21:55,-38.095590,176.364162,479.764 +OK189,08/03/2020 21:56,-38.095590,176.364162,479.764 +OK190,08/03/2020 22:19,-38.094084,176.378021,634.881 +OK190,08/03/2020 22:20,-38.094084,176.378021,634.881 +OK190,08/03/2020 22:21,-38.094084,176.378021,634.881 +OK191,08/03/2020 22:37,-38.099299,176.384629,706.346 +OK191,08/03/2020 22:39,-38.099299,176.384629,706.346 +OK191,08/03/2020 22:40,-38.099299,176.384629,706.346 +OK190,08/03/2020 22:50,-38.094084,176.378021,634.881 +OK190,08/03/2020 22:51,-38.094084,176.378021,634.881 +OK190,08/03/2020 22:52,-38.094084,176.378021,634.881 +OK188,08/03/2020 23:09,-38.093211,176.358686,478.667 +OK188,08/03/2020 23:10,-38.093211,176.358686,478.667 +OK188,08/03/2020 23:11,-38.093211,176.358686,478.667 +OK192,08/03/2020 23:31,-38.121561,176.323775,342.614 +OK192,08/03/2020 23:32,-38.121561,176.323775,342.614 +OK192,08/03/2020 23:33,-38.121561,176.323775,342.614 +OK193,08/03/2020 23:55,-38.142186,176.294049,332.718 +OK193,08/03/2020 23:57,-38.142186,176.294049,332.718 +OK193,08/03/2020 23:58,-38.142186,176.294049,332.718 +OK194,08/04/2020 01:37,-38.183641,176.356454,396.364 +OK194,08/04/2020 01:38,-38.183641,176.356454,396.364 +OK194,08/04/2020 01:39,-38.183641,176.356454,396.364 +OK195,08/04/2020 01:56,-38.185005,176.366806,529.801 +OK195,08/04/2020 01:57,-38.185005,176.366806,529.801 +OK195,08/04/2020 01:58,-38.185005,176.366806,529.801 +OK196,08/04/2020 02:13,-38.189933,176.362674,595.614 +OK196,08/04/2020 02:14,-38.189933,176.362674,595.614 +OK196,08/04/2020 02:15,-38.189933,176.362674,595.614 +OK197,08/04/2020 02:27,-38.195770,176.357778,644.529 +OK197,08/04/2020 02:29,-38.195770,176.357778,644.529 +OK197,08/04/2020 02:30,-38.195770,176.357778,644.529 +OK198,08/04/2020 02:47,-38.201981,176.354741,674.669 +OK198,08/04/2020 02:48,-38.201981,176.354741,674.669 +OK198,08/04/2020 02:49,-38.201981,176.354741,674.669 +OK197,08/04/2020 02:57,-38.195770,176.357778,644.529 +OK197,08/04/2020 02:58,-38.195770,176.357778,644.529 +OK197,08/04/2020 02:59,-38.195770,176.357778,644.529 +OK199,08/04/2020 03:28,-38.189724,176.372250,552.176 +OK199,08/04/2020 03:29,-38.189724,176.372250,552.176 +OK199,08/04/2020 03:30,-38.189724,176.372250,552.176 +OK200,08/04/2020 03:38,-38.195507,176.373097,554.431 +OK200,08/04/2020 03:39,-38.195507,176.373097,554.431 +OK194,08/04/2020 03:56,-38.183641,176.356454,396.364 +OK194,08/04/2020 03:57,-38.183641,176.356454,396.364 +OK194,08/04/2020 03:58,-38.183641,176.356454,396.364 +OK194,08/04/2020 03:59,-38.183641,176.356454,396.364 +AGUP,08/04/2020 04:36,-38.054648,176.441513,284.462 +AGUP,08/04/2020 04:37,-38.054648,176.441513,284.462 +AGUP,08/04/2020 04:38,-38.054648,176.441513,284.462 +AGUP,08/04/2020 04:39,-38.054648,176.441513,284.462 +AGUP,08/04/2020 20:21,-38.054648,176.441513,284.462 +AGUP,08/04/2020 20:22,-38.054648,176.441513,284.462 +AGUP,08/04/2020 20:23,-38.054648,176.441513,284.462 +OK192,08/04/2020 20:44,-38.121561,176.323775,342.614 +OK192,08/04/2020 20:45,-38.121561,176.323775,342.614 +OK192,08/04/2020 20:46,-38.121561,176.323775,342.614 +OK201,08/04/2020 21:03,-38.131740,176.326182,370.204 +OK201,08/04/2020 21:04,-38.131740,176.326182,370.204 +OK201,08/04/2020 21:05,-38.131740,176.326182,370.204 +OK202,08/04/2020 21:16,-38.133073,176.332289,418.035 +OK202,08/04/2020 21:17,-38.133073,176.332289,418.035 +OK202,08/04/2020 21:18,-38.133073,176.332289,418.035 +OK203,08/04/2020 21:27,-38.135381,176.339115,444.017 +OK203,08/04/2020 21:28,-38.135381,176.339115,444.017 +OK203,08/04/2020 21:29,-38.135381,176.339115,444.017 +OK204,08/04/2020 21:41,-38.138838,176.342339,516.432 +OK204,08/04/2020 21:42,-38.138838,176.342339,516.432 +OK204,08/04/2020 21:43,-38.138838,176.342339,516.432 +OK205,08/04/2020 21:53,-38.137376,176.347594,604.821 +OK205,08/04/2020 21:54,-38.137376,176.347594,604.821 +OK205,08/04/2020 21:55,-38.137376,176.347594,604.821 +OK206,08/04/2020 22:14,-38.143476,176.343121,562.832 +OK206,08/04/2020 22:16,-38.143476,176.343121,562.832 +OK206,08/04/2020 22:17,-38.143476,176.343121,562.832 +OK193,08/04/2020 22:46,-38.142186,176.294049,332.718 +OK193,08/04/2020 22:47,-38.142186,176.294049,332.718 +OK193,08/04/2020 22:48,-38.142186,176.294049,332.718 +OK207,08/05/2020 01:11,-38.146965,176.320984,429.110 +OK207,08/05/2020 01:12,-38.146965,176.320984,429.110 +OK207,08/05/2020 01:13,-38.146965,176.320984,429.110 +OK208,08/05/2020 01:25,-38.148213,176.328482,474.397 +OK208,08/05/2020 01:26,-38.148213,176.328482,474.397 +OK208,08/05/2020 01:28,-38.148213,176.328482,474.397 +OK209,08/05/2020 01:44,-38.144801,176.339130,531.496 +OK209,08/05/2020 01:45,-38.144801,176.339130,531.496 +OK209,08/05/2020 01:46,-38.144801,176.339130,531.496 +OK207,08/05/2020 01:59,-38.146965,176.320984,429.110 +OK207,08/05/2020 02:00,-38.146965,176.320984,429.110 +OK207,08/05/2020 02:01,-38.146965,176.320984,429.110 +OK210,08/05/2020 02:54,-38.009821,176.565920,355.129 +OK210,08/05/2020 02:55,-38.009821,176.565920,355.129 +OK210,08/05/2020 02:56,-38.009821,176.565920,355.129 +OK211,08/05/2020 03:16,-38.008722,176.598310,358.532 +OK211,08/05/2020 03:17,-38.008722,176.598310,358.532 +OK211,08/05/2020 03:18,-38.008722,176.598310,358.532 +OK212,08/05/2020 03:26,-38.002019,176.601507,345.520 +OK212,08/05/2020 03:27,-38.002019,176.601507,345.520 +OK212,08/05/2020 03:28,-38.002019,176.601507,345.520 +OK213,08/05/2020 03:38,-38.005984,176.624406,391.087 +OK213,08/05/2020 03:39,-38.005984,176.624406,391.087 +OK213,08/05/2020 03:40,-38.005984,176.624406,391.087 +AGUP,08/05/2020 04:12,-38.054648,176.441513,284.462 +AGUP,08/05/2020 04:13,-38.054648,176.441513,284.462 +AGUP,08/05/2020 04:14,-38.054648,176.441513,284.462 +AGUP,08/05/2020 20:27,-38.054648,176.441513,284.462 +AGUP,08/05/2020 20:28,-38.054648,176.441513,284.462 +AGUP,08/05/2020 20:29,-38.054648,176.441513,284.462 +AGUP,08/05/2020 20:30,-38.054648,176.441513,284.462 +OK214,08/05/2020 21:33,-38.254737,176.340638,620.728 +OK214,08/05/2020 21:34,-38.254737,176.340638,620.728 +OK214,08/05/2020 21:35,-38.254737,176.340638,620.728 +OK215,08/05/2020 21:47,-38.250738,176.340473,626.858 +OK215,08/05/2020 21:48,-38.250738,176.340473,626.858 +OK215,08/05/2020 21:49,-38.250738,176.340473,626.858 +OK216,08/05/2020 22:00,-38.257281,176.351303,607.274 +OK216,08/05/2020 22:01,-38.257281,176.351303,607.274 +OK216,08/05/2020 22:02,-38.257281,176.351303,607.274 +OK217,08/05/2020 22:13,-38.252441,176.358712,629.077 +OK217,08/05/2020 22:14,-38.252441,176.358712,629.077 +OK217,08/05/2020 22:15,-38.252441,176.358712,629.077 +OK218,08/05/2020 22:35,-38.245770,176.359060,616.591 +OK218,08/05/2020 22:36,-38.245770,176.359060,616.591 +OK218,08/05/2020 22:37,-38.245770,176.359060,616.591 +OK217,08/05/2020 22:46,-38.252441,176.358712,629.077 +OK217,08/05/2020 22:47,-38.252441,176.358712,629.077 +OK217,08/05/2020 22:48,-38.252441,176.358712,629.077 +OK219,08/05/2020 23:00,-38.251288,176.371234,604.630 +OK219,08/05/2020 23:01,-38.251288,176.371234,604.630 +OK219,08/05/2020 23:02,-38.251288,176.371234,604.630 +OK220,08/05/2020 23:17,-38.246592,176.382302,558.717 +OK220,08/05/2020 23:18,-38.246592,176.382302,558.717 +OK220,08/05/2020 23:19,-38.246592,176.382302,558.717 +OK221,08/05/2020 23:39,-38.257584,176.380017,390.101 +OK221,08/05/2020 23:40,-38.257584,176.380017,390.101 +OK221,08/05/2020 23:41,-38.257584,176.380017,390.101 +OK222,08/05/2020 23:54,-38.262789,176.374937,401.484 +OK222,08/05/2020 23:55,-38.262789,176.374937,401.484 +OK222,08/05/2020 23:56,-38.262789,176.374937,401.484 +OK223,08/06/2020 00:08,-38.271050,176.374644,454.465 +OK223,08/06/2020 00:09,-38.271050,176.374644,454.465 +OK223,08/06/2020 00:10,-38.271050,176.374644,454.465 +OK224,08/06/2020 00:24,-38.275782,176.372950,497.412 +OK224,08/06/2020 00:25,-38.275782,176.372950,497.412 +OK224,08/06/2020 00:26,-38.275782,176.372950,497.412 +OK221,08/06/2020 00:43,-38.257584,176.380017,390.101 +OK221,08/06/2020 00:44,-38.257584,176.380017,390.101 +OK221,08/06/2020 00:45,-38.257584,176.380017,390.101 +OK214,08/06/2020 01:00,-38.254737,176.340638,620.728 +OK214,08/06/2020 01:01,-38.254737,176.340638,620.728 +OK214,08/06/2020 01:02,-38.254737,176.340638,620.728 +OK193,08/06/2020 01:53,-38.142186,176.294049,332.718 +OK193,08/06/2020 01:54,-38.142186,176.294049,332.718 +OK193,08/06/2020 01:55,-38.142186,176.294049,332.718 +AGUP,08/06/2020 02:23,-38.054648,176.441513,284.462 +AGUP,08/06/2020 02:24,-38.054648,176.441513,284.462 +AGUP,08/06/2020 02:25,-38.054648,176.441513,284.462 diff --git a/project-words.txt b/project-words.txt index a006a95..8d58f98 100644 --- a/project-words.txt +++ b/project-words.txt @@ -1,5 +1,6 @@ addopts afactor +aimport airgap Aleksandr alisonk diff --git a/pyproject.toml b/pyproject.toml index 2e2682f..4cee5db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,6 +73,7 @@ dev = [ "burocrata>=0.3.0", "ipykernel>=7.2.0", "ipywidgets>=8.1.8", + "marimo>=0.25.0", "pandas-stubs==2.3.3.260113", "pandas-vet>=2023.8.2", "pyrefly>=1.3.1", diff --git a/uv.lock b/uv.lock index 75a7b8f..92ee761 100644 --- a/uv.lock +++ b/uv.lock @@ -32,6 +32,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, ] +[[package]] +name = "anyio" +version = "4.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.15'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" }, +] + [[package]] name = "appnope" version = "0.1.4" @@ -642,6 +655,7 @@ dev = [ { name = "burocrata" }, { name = "ipykernel" }, { name = "ipywidgets" }, + { name = "marimo" }, { name = "pandas-stubs" }, { name = "pandas-vet" }, { name = "pyrefly" }, @@ -688,6 +702,7 @@ dev = [ { name = "burocrata", specifier = ">=0.3.0" }, { name = "ipykernel", specifier = ">=7.2.0" }, { name = "ipywidgets", specifier = ">=8.1.8" }, + { name = "marimo", specifier = ">=0.25.0" }, { name = "pandas-stubs", specifier = "==2.3.3.260113" }, { name = "pandas-vet", specifier = ">=2023.8.2" }, { name = "pyrefly", specifier = ">=1.3.1" }, @@ -708,6 +723,15 @@ docs = [ { name = "sphinx-gallery", specifier = ">=0.21.0" }, ] +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + [[package]] name = "harmonica" version = "0.7.0" @@ -828,6 +852,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl", hash = "sha256:ecaca67aed704a338f88f67b1181b58f821ab5dc89c1f0f5ef99db43c1c2921e", size = 139808, upload-time = "2025-11-01T21:18:10.956Z" }, ] +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + [[package]] name = "jedi" version = "0.20.0" @@ -1019,6 +1052,105 @@ 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 = "loro" +version = "1.16.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/83/1dd9181d4b3abfa56c2a7e677a18ae209b38e754a1503e4007adfde47c72/loro-1.16.2.tar.gz", hash = "sha256:b3e30b8d9aadc600662acd31e90ae4da9b84c98900225aa044d4c28c140a12ce", size = 70470, upload-time = "2026-09-21T10:53:40.636Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/10/67676c4f0b247b340179a68fad7bb5d981c2e54e7f4682513d9250d90c85/loro-1.16.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:84ef4084a7be2fc3391d530ef302840459ee23dd95ef77dd638b3c8fe24c863f", size = 3408380, upload-time = "2026-09-21T10:50:54.686Z" }, + { url = "https://files.pythonhosted.org/packages/08/d3/e90f2c9db0b2420a4501eea1898d10769e82b67787e8a505eca176d73dc4/loro-1.16.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3e158f81a2227b9ccd9b66139c098b79ff0eccc4825f2977a8ecf6247dfd4983", size = 3215934, upload-time = "2026-09-21T10:50:36.555Z" }, + { url = "https://files.pythonhosted.org/packages/54/cc/4226e8156d80640afd74d1b3053c62adbd307d080a21d2a4256156a3ddc0/loro-1.16.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad39c737e42007b9d1e63d4936798a444f5fbd9dce76a1fdc41096900a8ae58c", size = 3560484, upload-time = "2026-09-21T10:47:16.306Z" }, + { url = "https://files.pythonhosted.org/packages/68/a6/3aca16bad28189450fff14aed1f7a5fec2577bfba95778aa1a580f514df3/loro-1.16.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c09f50be03b526c9823bfe3e2cc02f42bd53c1d4c6d81fe9b22035b12399594", size = 3636885, upload-time = "2026-09-21T10:47:53.506Z" }, + { url = "https://files.pythonhosted.org/packages/72/35/581373a232c53e6e205600e0d065c42824e3106aea35cb0990ad9bd6edd9/loro-1.16.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad8b88e00a1e7f9a8348935c73abb33dbc573af7d1bd542f180804a39e8f7988", size = 4011097, upload-time = "2026-09-21T10:48:29.927Z" }, + { url = "https://files.pythonhosted.org/packages/37/ee/b55a0971765f129582ae7862ed9f30981f71f93cf78b7cd401e56b3d3091/loro-1.16.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5ef353110b2e1c2cc332e66aa07704b8571960523117841cfc0f5711dc4310c", size = 3721604, upload-time = "2026-09-21T10:49:06.991Z" }, + { url = "https://files.pythonhosted.org/packages/6c/07/f539e4d5dcf252430af9c4b76a27be756c3dbcb400ebc0e33578fa8552b6/loro-1.16.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d42620092340597fc9b8c76ec76bed92de1326b7c9418a49a8909371b598803f", size = 3558887, upload-time = "2026-09-21T10:50:09.16Z" }, + { url = "https://files.pythonhosted.org/packages/79/18/093a00b64aedd5d7ce33e75b88f9f25278baf46fffbaf54f60e6495411c7/loro-1.16.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fd2c787299b6332b308f543253f4bb2885536cb80799799168836936cfa619", size = 3951738, upload-time = "2026-09-21T10:49:42.201Z" }, + { url = "https://files.pythonhosted.org/packages/29/6e/bfb0c49f06f99b7acbe90872f327b49ba7ca5a7b22d777f528aab67bf94c/loro-1.16.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b19e18c5c0deefded5b1f41d15a34c3c3dd8e89491c1f086cada9e9b6ab5942c", size = 3736893, upload-time = "2026-09-21T10:51:13.212Z" }, + { url = "https://files.pythonhosted.org/packages/93/cc/0f08597cdd6124dbbb42330dda2e25e6e7231df3e809c212dec2c419e3f2/loro-1.16.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7af386703e10df2d9613479b2d558225063023f726902cefd39a799a404ee69a", size = 3909291, upload-time = "2026-09-21T10:51:49.644Z" }, + { url = "https://files.pythonhosted.org/packages/38/1d/3ce1099cf71e882906e01d476864459956c136a41a561ce73a34b0a02818/loro-1.16.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ef6d5ca7f32aa26d5c4f61f794ed0343b893f3e6034bea760e9ece1d526ba23", size = 3967519, upload-time = "2026-09-21T10:52:26.537Z" }, + { url = "https://files.pythonhosted.org/packages/cf/2d/9453c9381cc9002cf2eff160c705aa3404711c909d162e3585640223c3fe/loro-1.16.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4ac4641cacf192d59f0daa92a5cbea101f50a5d7c44c0f466ad98f4571dd1d53", size = 3877159, upload-time = "2026-09-21T10:53:04.999Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/9b078f371c6372667253fa82d15184dfcfcbdf1f478d7e57f3a20abd9ef7/loro-1.16.2-cp312-cp312-win32.whl", hash = "sha256:5a29103d094e592df751cb35b1f3197586f1fdda9cdcf707b80bedbe5394ef56", size = 2950327, upload-time = "2026-09-21T10:54:06.791Z" }, + { url = "https://files.pythonhosted.org/packages/34/0d/6b8140ff5fa6d350548b4e088d2303f667ace864b189da441c9e1dcf6593/loro-1.16.2-cp312-cp312-win_amd64.whl", hash = "sha256:503af039fe68fff1874cfbac86801784f541d00cce61284f15f28fc0ba8bbfd2", size = 3183597, upload-time = "2026-09-21T10:53:45.227Z" }, + { url = "https://files.pythonhosted.org/packages/77/85/21ea0adba99b7a890318ea69a31f72eafc411449b9024f845f07793d6e57/loro-1.16.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f10995db58779d3cfc5f233f84fca09fac6fc20bc5b7db71b4afe7b366f41b24", size = 3408650, upload-time = "2026-09-21T10:50:56.222Z" }, + { url = "https://files.pythonhosted.org/packages/43/05/c6f98a5638373ca2b967194fe1c778cbd7bdd715adcefb91b79f325c0191/loro-1.16.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:455771cfda2c84d08861ab64f0fb541d67e32193ac82a83a9dc8ea6e711e79ce", size = 3215484, upload-time = "2026-09-21T10:50:38.224Z" }, + { url = "https://files.pythonhosted.org/packages/fb/fa/94c321f615fe2cc0178d466e8c92c00c77615b89994c5378335b52f3cb90/loro-1.16.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11e29b820fd88f86e18e4f05055db724fd0a68372356581f4318cb4b9e368f62", size = 3561932, upload-time = "2026-09-21T10:47:17.786Z" }, + { url = "https://files.pythonhosted.org/packages/30/1a/3b73c87eed13667fc04bf4627b628e8e38fdcf9ed5898b9c122f0aaaa3d5/loro-1.16.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd0738bd6d2d3149a7c2d9496abbdcb6d5105a70d8d61e23a6536d253d7fadae", size = 3636574, upload-time = "2026-09-21T10:47:54.873Z" }, + { url = "https://files.pythonhosted.org/packages/12/eb/776be0d65ea35a83d52d84817ef04ee62e9cb496285f7d13582baf9b065a/loro-1.16.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d277aa9422ec314a1eb96503a827138dbc0e5650301304904fb9b35eb3605911", size = 4011370, upload-time = "2026-09-21T10:48:31.564Z" }, + { url = "https://files.pythonhosted.org/packages/41/de/6aa57039390bd0fc39c33cd025d524ca8927bcfe1540f7d9b9b31e1001e3/loro-1.16.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d812435e3a26b5a7b1417bf1a4fd7c9b951adb9817b407b1a5f935f2829617a", size = 3722827, upload-time = "2026-09-21T10:49:08.519Z" }, + { url = "https://files.pythonhosted.org/packages/18/f4/dc9ec2b7c347a3b7694bbb4412e58a650f5ad79a7ee72a70277f31cd98b5/loro-1.16.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b6c069242a3a7128a6c929c7bc87d730644bac01c83fc29141755b014621772", size = 3558981, upload-time = "2026-09-21T10:50:10.469Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a9/17658f6ec319e195ae86d38c11d0478404aac0f13296cace2fc8002e0786/loro-1.16.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:adec0565eecc806ee9dc6470ca26b784c0d67270129ba8611f546552140629be", size = 3950831, upload-time = "2026-09-21T10:49:43.565Z" }, + { url = "https://files.pythonhosted.org/packages/48/f1/e055e5a8f047f404425399ef09431bea6bf7404aae5fd120d20579a241ae/loro-1.16.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:acb942480d16226a538dd63dc1fbe532ae66dd1fb9e8800ed454bc1e6a12cda8", size = 3738048, upload-time = "2026-09-21T10:51:14.845Z" }, + { url = "https://files.pythonhosted.org/packages/58/fd/e84706fde319f217dab5e6b5a53d88619a22f9b32b8cddb52ef1cf49c561/loro-1.16.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8e0c1108c204fef6db8d1107a4c2c4367ddc6759c6b3428c904d5a283d0e9f72", size = 3909244, upload-time = "2026-09-21T10:51:51.163Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ee/6cd5e8b3ef105bd28d005747e3a2846bd2a76dfd49b710dec7279af4a0ee/loro-1.16.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:aa72e367432153907849b4f290ce287932563b73818b83f8aa3d5f2a2142af02", size = 3967937, upload-time = "2026-09-21T10:52:28.48Z" }, + { url = "https://files.pythonhosted.org/packages/fa/db/979b4a54007caee21885b21ef319d465671242e57551ea5cbedb1d327722/loro-1.16.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:32d7979ef4bd2f4dff73cca1d8b2634e035bbaee590e167570866721623edfeb", size = 3878292, upload-time = "2026-09-21T10:53:06.785Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b6/186255c8859f2c27e7bf94ef6d80d56e9a30fb8ecf3131b3d8a6a027e783/loro-1.16.2-cp313-cp313-win32.whl", hash = "sha256:4eaf8e500d4c1e9da8edbec6ded28758f44d59e0f923778301480ead59504e97", size = 2949712, upload-time = "2026-09-21T10:54:08.75Z" }, + { url = "https://files.pythonhosted.org/packages/b9/76/a4abc1bdb824b130508eab9ffaab7e99eedc2325070300713adcae6a46be/loro-1.16.2-cp313-cp313-win_amd64.whl", hash = "sha256:c05c11ad43c57190e7d912a570328f7294a6b569ccbff6eedb788414710e4245", size = 3183368, upload-time = "2026-09-21T10:53:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/f9ae6e61722041e062245b3f2d9464fc2eb28631b688ca2589b6321f48e7/loro-1.16.2-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:a02cc0c788a070a8b6b8c111642fce80ed0f9bc52cfb8361f3a671ed29880bf7", size = 3388067, upload-time = "2026-09-21T10:50:57.712Z" }, + { url = "https://files.pythonhosted.org/packages/75/43/7d69e5faf912c51fe03c133ccb847fa7ed6f003a742d1959247c862e144e/loro-1.16.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c72a7d1e6a05c9630f75de9a9a78231c80eb1ad2a3c9f6554ef287fb3d57aa8", size = 3202677, upload-time = "2026-09-21T10:50:39.61Z" }, + { url = "https://files.pythonhosted.org/packages/52/d7/c19d7d6e984cae8a2a3a6171d16565e39fb0eb5fdeccd507af5ba1946e05/loro-1.16.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a523e5c3a7b321b9664ac9ebf992b8f872f53280d2735e2340dbd77f7cceb8d", size = 3551977, upload-time = "2026-09-21T10:47:19.047Z" }, + { url = "https://files.pythonhosted.org/packages/49/d6/33b86554151228fc562e069eb9cd7246466864ca0d22242515029ab802dd/loro-1.16.2-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf8f3d7fbbbe8921eb0a89dea6fbbef4bec361aaa03541a957e3778a820c72ec", size = 3615097, upload-time = "2026-09-21T10:47:56.763Z" }, + { url = "https://files.pythonhosted.org/packages/73/aa/bd12570b97cc9afa306be444ef45322433c1a630b8816923c497da85d182/loro-1.16.2-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6276a87a64904a568f7f846d568a187fcd125213f4f1a3f756ab3f9350dc9aac", size = 4007066, upload-time = "2026-09-21T10:48:32.928Z" }, + { url = "https://files.pythonhosted.org/packages/61/11/334982f10a2ff7f66b1d91157ed502c5ebffff7d1f6cbd7199ee9a722d8d/loro-1.16.2-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0ada36b1965a81d50d7dc9e35df42fd84077ea8192b072f9286822fa53f74af", size = 3714840, upload-time = "2026-09-21T10:49:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f3/c5f18b9883bf30703b9d5e02f878e4c81960cf7c20a4c6502b669eb00e39/loro-1.16.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb3aab12b9e7e4b85141a47fc90183a1cb0fe6549f6cd33666827b5da470721c", size = 3541250, upload-time = "2026-09-21T10:50:12.242Z" }, + { url = "https://files.pythonhosted.org/packages/9e/72/0bcf0d64ff5e5071667e2a059a81223586ffee493c116e6975bc5c95d429/loro-1.16.2-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0822ec78a2cb2087203adc0f7c4c105de5c9ab31a735efdc6e0d64c05d8e30b", size = 3935828, upload-time = "2026-09-21T10:49:45.12Z" }, + { url = "https://files.pythonhosted.org/packages/ed/f0/0d2be34b9ddfa38f0990e17456a91e5cb39d1cf740dbe302063999bfa917/loro-1.16.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:333778715b84d9d46f547d4c4f53d8f02739f3a1199fed172f345d472002b642", size = 3726581, upload-time = "2026-09-21T10:51:16.402Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a2/a924c60fc045f35ffab0f3b05d84cc62787b85ed8177b92b724c15d2076a/loro-1.16.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ecaa21e06a3707cdb7066b8bd5e9752aff4a2196bd6154a55863ab26185ae718", size = 3889480, upload-time = "2026-09-21T10:51:52.908Z" }, + { url = "https://files.pythonhosted.org/packages/b9/39/9aedb1538a672b5d9867d080ce23f2af169f36bc2302e6a96fb6d96a1988/loro-1.16.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:671f5270d9520eb157de9be16365c29e921919e9bc622f15e7ae7d28196b9209", size = 3959009, upload-time = "2026-09-21T10:52:29.843Z" }, + { url = "https://files.pythonhosted.org/packages/8f/45/6305eae57f5b55fcea8334a4fbb828440e4a8abe532889f2e05872f4e103/loro-1.16.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e3dc7188c2a68a38350a98838d97e216f93b803d2a93fe501111c08d219095a", size = 3861624, upload-time = "2026-09-21T10:53:08.504Z" }, + { url = "https://files.pythonhosted.org/packages/f2/25/e2eb3cc092d64ba5e579d6c217082a10c2d55090b9ac30c0a1728672579e/loro-1.16.2-cp314-cp314-win32.whl", hash = "sha256:c6d28f85e19ac88ecee4fd535999d71196d1a3fba2c66dae54b6168687419c1d", size = 2937950, upload-time = "2026-09-21T10:54:10.436Z" }, + { url = "https://files.pythonhosted.org/packages/6e/40/6557fddb37ad3adfb36b60350d930e1eb69412e7a5328c46108fdae67cf1/loro-1.16.2-cp314-cp314-win_amd64.whl", hash = "sha256:e420efe54720a7a6b2c9f421d0cc5c56c01b09351162a452d33d377f2ee87778", size = 3162432, upload-time = "2026-09-21T10:53:48.75Z" }, + { url = "https://files.pythonhosted.org/packages/fd/16/56abbe5452186e8546ca62f4f508016550202e1551d136eebab45090317d/loro-1.16.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46f2ff5434616e6d5b118dad992804ed1d4e8dfefe1a4c4c64273b0bd3e5616f", size = 3553092, upload-time = "2026-09-21T10:47:20.883Z" }, + { url = "https://files.pythonhosted.org/packages/a2/19/fbf6efa486978407882395062b65687c2074d831c3f5d1211dfdac63ccb5/loro-1.16.2-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c551daf053fa5ca594db147c8b2044b57144595111b0f57a672edf54b5756c4", size = 3617321, upload-time = "2026-09-21T10:47:58.106Z" }, + { url = "https://files.pythonhosted.org/packages/c1/96/fafc3df7cf5ef142c77327c9fcfde24a9825e31fa87cc20bbf164bf3a9e9/loro-1.16.2-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4931866c3a6bb09715c55ce9ba01062a678f44d6478cf1b1c163222f954f2f98", size = 4004449, upload-time = "2026-09-21T10:48:34.467Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/e9c6db12d1daa3df1cd78ccc4232bb20a2e9b09d702c7e57fc8c6c60775b/loro-1.16.2-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:93f62f2939544319d2bac1292c788e473ba5aac946ac8f03e0a19ebe46ee882b", size = 3709917, upload-time = "2026-09-21T10:49:11.304Z" }, + { url = "https://files.pythonhosted.org/packages/90/4f/26f496f65fd053ee527a8da720138b52a521404fcaa58110c4329f7b68e4/loro-1.16.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1298a0f27e22589df53ffbc6178785965bf86cabd0a7fdf839b112c37f034c4d", size = 3536458, upload-time = "2026-09-21T10:50:13.773Z" }, + { url = "https://files.pythonhosted.org/packages/90/fb/0b5760638b48c5cd7c16831797b151697c08fb10b7f629803efc655f683e/loro-1.16.2-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4281a93df46ec02583365b7403e703787e0a794303495b7b61d6a121090bed15", size = 3940244, upload-time = "2026-09-21T10:49:46.804Z" }, + { url = "https://files.pythonhosted.org/packages/58/3b/dae1e99592b225b9b757059c15c5351fd0f165ce2309631d73a534cf4428/loro-1.16.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:505f78453cda745dd1ef6e9e13cce11208a546730e45989ae4bccabee5e72697", size = 3728368, upload-time = "2026-09-21T10:51:17.814Z" }, + { url = "https://files.pythonhosted.org/packages/54/7f/59737c7bee98d07b3057bdd3f8824e42347d3846d235336fedaf3980fd07/loro-1.16.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf2dc9dee8b12aa87585decdc11f337ff7f67a31629c3cda00094c894e3f69ff", size = 3889805, upload-time = "2026-09-21T10:51:54.693Z" }, + { url = "https://files.pythonhosted.org/packages/1b/79/2602758cea0d22b07c7b63aa7277d64a10d2017fe7d2b6349811b64b1d50/loro-1.16.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6dc4e8ed7bcdbceb784fc4976428016fa572177f7757503d489d3aee8b8d31f9", size = 3961692, upload-time = "2026-09-21T10:52:31.548Z" }, + { url = "https://files.pythonhosted.org/packages/50/8e/df588b63fe352bf6ea9154df1417b4bb061237068beaba24bf7b2b8a5435/loro-1.16.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d95c5223f23f6be9cd1439ab5b65ad9e7e23ca6b294f8ac14af982bed36d984a", size = 3860841, upload-time = "2026-09-21T10:53:10.065Z" }, +] + +[[package]] +name = "marimo" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", marker = "sys_platform != 'emscripten'" }, + { name = "docutils" }, + { name = "itsdangerous" }, + { name = "jedi" }, + { name = "loro", marker = "sys_platform != 'android' and sys_platform != 'emscripten'" }, + { name = "markdown" }, + { name = "msgspec" }, + { name = "narwhals" }, + { name = "packaging" }, + { name = "psutil", marker = "sys_platform != 'android' and sys_platform != 'emscripten'" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "python-multipart" }, + { name = "pyyaml" }, + { name = "pyzmq", marker = "python_full_version < '3.15' and sys_platform != 'emscripten'" }, + { name = "starlette", marker = "sys_platform != 'emscripten'" }, + { name = "tomlkit" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, + { name = "websockets", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/a7/4f0cc52e7c875997e7026e737f7a20f75786d2d65e572a1a0ef9cd5204ce/marimo-0.25.0.tar.gz", hash = "sha256:0005827578027cf9ea2cce039e099c029a9323a3467cb15100228918c1608ad1", size = 50236089, upload-time = "2026-09-23T21:48:33.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/0d/055b436b509ffd6e7f9307dacd95f58c7fbb6c75a1eafca3c6047e7f13a6/marimo-0.25.0-py3-none-any.whl", hash = "sha256:43bb4d82d713f44fc064e77668e59a9fbb6172a6cf7c3abe25c15ef2cb1cce31", size = 50902999, upload-time = "2026-09-23T21:48:29.047Z" }, +] + +[[package]] +name = "markdown" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/4f/700155c8c20d9e655dd0732b5fc3c7614f291b9148da271d7388e50bf774/markdown-3.11.tar.gz", hash = "sha256:180224db6aed87ba9ce1f2781ebcd5826253de8ff637112090e24b84502bbf9f", size = 485623, upload-time = "2026-09-25T13:46:23.473Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/1e/32971905a7ab47f8b66866ed949fa48b104ba1c4a6fa57794c4f2c4b2cb8/markdown-3.11-py3-none-any.whl", hash = "sha256:cd6c89e7eb308c8b332ed673215a52d208a43f8bacc030b1419376129408719e", size = 111296, upload-time = "2026-09-25T13:46:22.163Z" }, +] + [[package]] name = "markdown-it-py" version = "4.2.0" @@ -1190,6 +1322,46 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] +[[package]] +name = "msgspec" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/60/f79b9b013a16fa3a58350c9295ddc6789f2e335f36ea61ed10a21b215364/msgspec-0.21.1.tar.gz", hash = "sha256:2313508e394b0d208f8f56892ca9b2799e2561329de9763b19619595a6c0f72c", size = 319193, upload-time = "2026-04-12T21:44:50.394Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/cf/317224852c00248c620a9bcf4b26e2e4ab8afd752f18d2a6ef73ebd423b6/msgspec-0.21.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4248cf0b6129b7d230eacd493c17cc2d4f3989f3bb7f633a928a85b7dcfa251", size = 196188, upload-time = "2026-04-12T21:44:07.181Z" }, + { url = "https://files.pythonhosted.org/packages/6d/81/074612945c0666078f7366f40000013de9f6ba687491d450df699bceebc9/msgspec-0.21.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5102c7e9b3acff82178449b85006d96310e690291bb1ea0142f1b24bcb8aabcb", size = 188473, upload-time = "2026-04-12T21:44:08.736Z" }, + { url = "https://files.pythonhosted.org/packages/8a/37/655101799590bcc5fddb2bd3fe0e6194e816c2d1da7c361725f5eb89a910/msgspec-0.21.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:846758412e9518252b2ac9bffd6f0e54d9ff614f5f9488df7749f81ff5c80920", size = 218871, upload-time = "2026-04-12T21:44:09.917Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d1/d4cd9fe89c7d400d7a18f86ccc94daa3f0927f53558846fcb60791dce5d6/msgspec-0.21.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21995e74b5c598c2e004110ad66ec7f1b8c20bf2bcf3b2de8fd9a3094422d3ff", size = 225025, upload-time = "2026-04-12T21:44:11.191Z" }, + { url = "https://files.pythonhosted.org/packages/24/bf/e20549e602b9edccadeeff98760345a416f9cce846a657e8b18e3396b212/msgspec-0.21.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6129f0cca52992e898fd5344187f7c8127b63d810b2fd73e36fca73b4c6475ee", size = 222672, upload-time = "2026-04-12T21:44:12.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/68/04d7a8f0f786545cf9b8c280c57aa6befb5977af6e884b8b54191cbe44b3/msgspec-0.21.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ef3ec2296248d1f8b9231acb051b6d471dfde8f21819e86c9adaaa9f42918521", size = 227303, upload-time = "2026-04-12T21:44:13.709Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4d/619866af2840875be408047bf9e70ceafbae6ab50660de7134ed1b25eb86/msgspec-0.21.1-cp312-cp312-win_amd64.whl", hash = "sha256:d4ab834a054c6f0cbeef6df9e7e1b33d5f1bc7b86dea1d2fd7cad003873e783d", size = 190017, upload-time = "2026-04-12T21:44:14.977Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2e/a8f9eca8fd00e097d7a9e99ba8a4685db994494448e3d4f0b7f6e9a3c0f7/msgspec-0.21.1-cp312-cp312-win_arm64.whl", hash = "sha256:628aaa35c74950a8c59da330d7e98917e1c7188f983745782027748ee4ca573e", size = 175345, upload-time = "2026-04-12T21:44:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/7e/74/f11ede02839b19ff459f88e3145df5d711626ca84da4e23520cebf819367/msgspec-0.21.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:764173717a01743f007e9f74520ed281f24672c604514f7d76c1c3a10e8edb66", size = 196176, upload-time = "2026-04-12T21:44:17.613Z" }, + { url = "https://files.pythonhosted.org/packages/bb/40/4476c1bd341418a046c4955aff632ec769315d1e3cb94e6acf86d461f9ed/msgspec-0.21.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:344c7cd0eaed1fb81d7959f99100ef71ec9b536881a376f11b9a6c4803365697", size = 188524, upload-time = "2026-04-12T21:44:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d9/9e9d7d7e5061b47540d03d640fab9b3965ba7ae49c1b2154861c8f007518/msgspec-0.21.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48943e278b3854c2f89f955ddc6f9f430d3f0784b16e47d10604ee0463cd21f5", size = 218880, upload-time = "2026-04-12T21:44:20.028Z" }, + { url = "https://files.pythonhosted.org/packages/74/66/2bb344f34abb4b57e60c7c9c761994e0417b9718ec1460bf00c296f2a7ea/msgspec-0.21.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9aa659ebb0101b1cbc31461212b87e341d961f0ab0772aaf068a99e001ec4aa", size = 225050, upload-time = "2026-04-12T21:44:21.577Z" }, + { url = "https://files.pythonhosted.org/packages/1a/84/7c1e412f76092277bf760cef12b7979d03314d259ab5b5cafde5d0c1722d/msgspec-0.21.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7b27d1a8ead2b6f5b0c4f2d07b8be1ccfcc041c8a0e704781edebe3ae13c484", size = 222713, upload-time = "2026-04-12T21:44:22.83Z" }, + { url = "https://files.pythonhosted.org/packages/4e/27/0bba04b2b4ef05f3d068429410bc71d2cea925f1596a8f41152cccd5edb8/msgspec-0.21.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:38fe93e86b61328fe544cb7fd871fad5a27c8734bfda90f65e5dbe288ae50f61", size = 227259, upload-time = "2026-04-12T21:44:24.11Z" }, + { url = "https://files.pythonhosted.org/packages/b0/2d/09574b0eea02fed2c2c1383dbaae2c7f79dc16dcd6487a886000afb5d7c4/msgspec-0.21.1-cp313-cp313-win_amd64.whl", hash = "sha256:8bc666331c35fcce05a7cd2d6221adbe0f6058f8e750711413d22793c080ac6a", size = 189857, upload-time = "2026-04-12T21:44:25.359Z" }, + { url = "https://files.pythonhosted.org/packages/46/34/105b1576ad182879914f0c821f17ee1d13abb165cb060448f96fe2aff078/msgspec-0.21.1-cp313-cp313-win_arm64.whl", hash = "sha256:42bb1241e0750c1a4346f2aa84db26c5ffd99a4eb3a954927d9f149ff2f42898", size = 175403, upload-time = "2026-04-12T21:44:26.608Z" }, + { url = "https://files.pythonhosted.org/packages/5a/ad/86954e987d1d6a5c579e2c2e7832b65e0fff194179fdac4f581536086024/msgspec-0.21.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fab48eb45fdbfbdb2c0edfec00ffc53b6b6085beefc6b50b61e01659f9f8757f", size = 196261, upload-time = "2026-04-12T21:44:27.807Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a1/c5e46c3e42b866199365e35d11dddfd1fbd8bba4fdb3c52f965b1607ce94/msgspec-0.21.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3cb779ea0c35bc807ff941d415875c1f69ca0be91a2e907ab99a171811d86a9a", size = 188729, upload-time = "2026-04-12T21:44:28.99Z" }, + { url = "https://files.pythonhosted.org/packages/85/7d/1e29a319d678d6cb962ae5bdf32a6858ebdf38f73bc654c0e9c742a0c2c8/msgspec-0.21.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68604db36b3b4dd9bf160e436e12798a4738848144cea1aca1cb984011eb160f", size = 219866, upload-time = "2026-04-12T21:44:31.104Z" }, + { url = "https://files.pythonhosted.org/packages/25/1f/cca084ca2572810fff12ea9dbdcbe39eac048f40daf4a9077b49fcbe8cee/msgspec-0.21.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d6b9dc50948eaf65df54d2fd0ff66e6d8c32f116037209ee861810eb9b676cb", size = 224993, upload-time = "2026-04-12T21:44:32.649Z" }, + { url = "https://files.pythonhosted.org/packages/71/94/d2120fc9d419a89a3a7c13e5b7078798c4b392a96a02a6e2b3ce43a8766c/msgspec-0.21.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:52c5e21930942302394429c5a582ce7e6b62c7f983b3760834c2ce107e0dd6df", size = 223535, upload-time = "2026-04-12T21:44:33.839Z" }, + { url = "https://files.pythonhosted.org/packages/75/17/42418b66a3ad972a89bab73dd78b79cc6282bb488a25e73c853cee7443b9/msgspec-0.21.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:abbb39d65681fa24ed394e01af3d59d869068324f900c61d06062b7fb9980f2f", size = 227222, upload-time = "2026-04-12T21:44:35.093Z" }, + { url = "https://files.pythonhosted.org/packages/c4/33/265c894268cca88ff67b144ca2b4c522fc8b9a6f1966a3640c70516e78e1/msgspec-0.21.1-cp314-cp314-win_amd64.whl", hash = "sha256:5666b1b560b97b6ec2eb3fca8a502298ebac56e13bbca1f88523538ce83d01ea", size = 193810, upload-time = "2026-04-12T21:44:36.612Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8f/a6d35f25bf1fc63c492fdd88fdce01ba0875ead48c2b91f90f33653b4131/msgspec-0.21.1-cp314-cp314-win_arm64.whl", hash = "sha256:d8b8578e4c83b14ceea4cef0d0b747e31d9330fe4b03b2b2ad4063866a178f93", size = 179125, upload-time = "2026-04-12T21:44:38.198Z" }, + { url = "https://files.pythonhosted.org/packages/c6/39/74839641e64b99d87da55af0fc472854d42b46e2183b9e2a67fe1bb2a512/msgspec-0.21.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:15f523d51c00ebad412213bfe9f06f0a50ec2b93e0c19e824a2d267cabb48ea2", size = 200171, upload-time = "2026-04-12T21:44:39.414Z" }, + { url = "https://files.pythonhosted.org/packages/70/9b/ce0cca6d2d87fcd4b6ff97600790494e64f26a2c55d61507cd2755c16193/msgspec-0.21.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e47390360583ba3d5c6cb44cf0a9f61b0a06a899d3c2c00627cedebb2e2884b", size = 192879, upload-time = "2026-04-12T21:44:40.882Z" }, + { url = "https://files.pythonhosted.org/packages/a7/08/673a7bb05e5702dc787ddd3011195b509f9867927970da59052211929987/msgspec-0.21.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f60800e6299b798142dc40b0644da77ceac5ea0568be58228417eae14135c847", size = 226281, upload-time = "2026-04-12T21:44:42.181Z" }, + { url = "https://files.pythonhosted.org/packages/7d/45/86508cf57283e9070b3c447e3ab25b792a7a0855a3ea4e0c6d111ac34c97/msgspec-0.21.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5f8e9dfcd98419cf7568808470c4317a3fb30bef0e3715b568730a2b272a20d7", size = 229863, upload-time = "2026-04-12T21:44:43.442Z" }, + { url = "https://files.pythonhosted.org/packages/2c/62/e7c9367cd08d590559faacd711edbae36840342843e669440363f33c7d36/msgspec-0.21.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92d89dfad13bd1ea640dc3e37e724ed380da1030b272bdf5ecafb983c3ad7c75", size = 230445, upload-time = "2026-04-12T21:44:44.806Z" }, + { url = "https://files.pythonhosted.org/packages/42/b4/c0f54632103846b658a10930025f4de41c8724b5e4805a5f3b395586cb7e/msgspec-0.21.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0d03867786e5d7ba25d666df4b11320c27170f4aeafcb8e3a8b0a50a4fb742ca", size = 231822, upload-time = "2026-04-12T21:44:46.343Z" }, + { url = "https://files.pythonhosted.org/packages/ea/1d/0d85cc79d0ccf5508e9c846cc66552a6a16bf92abd1dbd8362617f7b35cd/msgspec-0.21.1-cp314-cp314t-win_amd64.whl", hash = "sha256:740fbf1c9d59992ca3537d6fbe9ebbf9eaf726a65fbf31448e0ecbc710697a63", size = 206650, upload-time = "2026-04-12T21:44:47.601Z" }, + { url = "https://files.pythonhosted.org/packages/90/91/56c5d560f20e6c20e9e4f55bd0e458f7f162aa689ee350346c04c48eac0b/msgspec-0.21.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0d2cc73df6058d811a126ac3a8ad63a4dfa210c82f9cf5a004802eaf4712de90", size = 183149, upload-time = "2026-04-12T21:44:48.833Z" }, +] + [[package]] name = "myst-parser" version = "5.1.0" @@ -1717,6 +1889,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/87/6bbe3586cfa3d20159c566cde5b9266faf66a0bd51b9cbe98d462532e5c1/pyhardisp-0.2.4-py3-none-any.whl", hash = "sha256:d1b772bd4a9ade194e10ad55c17185308bd5edcf4092f11380b1b56d5b0539a8", size = 20777, upload-time = "2026-05-25T00:16:57.1Z" }, ] +[[package]] +name = "pymdown-extensions" +version = "11.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/17/2db4b414de89659144488e0d9c6c0bf0c8395841dc12d81d0532cc6ef310/pymdown_extensions-11.0.2.tar.gz", hash = "sha256:9506fcbe66fa355a775b768084334238dd6805020ac4b92bea0c0dda6f8f223d", size = 855419, upload-time = "2026-08-22T19:28:47.236Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/43/9f45ec4d14e596efc32c925a78104934790438b0c0628b70d741016734ad/pymdown_extensions-11.0.2-py3-none-any.whl", hash = "sha256:259910762019732caa1dfd76f3faa62c59f191d46573e80bcb1d13c0f675bbe5", size = 269929, upload-time = "2026-08-22T19:28:45.389Z" }, +] + [[package]] name = "pyparsing" version = "3.3.2" @@ -1855,6 +2040,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] +[[package]] +name = "python-multipart" +version = "0.0.32" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, +] + [[package]] name = "pytz" version = "2026.3.post1" @@ -2343,6 +2537,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, ] +[[package]] +name = "starlette" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/2b/3850dc6bf7ef71b088962eba31dafc6cffd2f96e577ebb0bb316df96da3e/starlette-1.7.0.tar.gz", hash = "sha256:c79f74ea63cff761804fbbfb182f1e0b440c2d07b164d24700c5a1bab5d6ff5d", size = 2736246, upload-time = "2026-09-23T07:30:26.35Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/d6/1ec1b290f9e0fb067899b61e1d37a30c923068bad260b216dbe37a7d2967/starlette-1.7.0-py3-none-any.whl", hash = "sha256:67f8e99895493dd2911a03f11314af6ceebeae4e704bb9f43dfc6a9db151c93e", size = 78980, upload-time = "2026-09-23T07:30:24.567Z" }, +] + [[package]] name = "threadpoolctl" version = "3.6.0" @@ -2397,6 +2604,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, ] +[[package]] +name = "tomlkit" +version = "0.15.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/96/e07752635b98536177fa1f37671c8f3cdde2e724c6bcf6034b2cfb571565/tomlkit-0.15.1.tar.gz", hash = "sha256:e25bbf38843005246210a12982776f27f99cb9be67160e14434d0c0d21ee1e97", size = 180129, upload-time = "2026-07-17T01:48:04.562Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/bc/8c13eb66537dce1d2bd3a57132902f38d0e7f5bb46fa9f4daed9fe9d76ee/tomlkit-0.15.1-py3-none-any.whl", hash = "sha256:177a05aece5a8ca5266fd3c448abb47b8d352f09d477d3ca8332db4d89b24304", size = 49449, upload-time = "2026-07-17T01:48:05.728Z" }, +] + [[package]] name = "toolz" version = "1.1.0" @@ -2505,6 +2721,19 @@ wheels = [ { 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 = "uvicorn" +version = "0.54.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/34/30e9280707135d2cfc589dfff3cb796bd07a3aeb1a3e415ba09dd89d7bb4/uvicorn-0.54.0.tar.gz", hash = "sha256:a2e33cbfaa0306f8e6b0c13e0cb89d7d7a2da3e62b90c66e18c33d9807b28620", size = 112283, upload-time = "2026-09-25T06:52:37.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0c/b54a4fdd7f90a3af8b02ebc9ce6712c2c208b7926a2f7bad95c33ebbe943/uvicorn-0.54.0-py3-none-any.whl", hash = "sha256:505bdb0f318731d45f1f712071fc781a8981f6847a31c902c9f5e652d4f67faf", size = 87427, upload-time = "2026-09-25T06:52:35.829Z" }, +] + [[package]] name = "verde" version = "1.9.0" @@ -2532,6 +2761,135 @@ wheels = [ { 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" }, ] +[[package]] +name = "websockets" +version = "17.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/72/fba934cb3dff7a85d811820efffcd141ddd52b5a2a01637f64551373ff4d/websockets-17.1.tar.gz", hash = "sha256:acfea4c20bf54384883ea33b1240fc1db4f52e190823a4e2b334bc3e8bfca96a", size = 187520, upload-time = "2026-08-26T17:25:33.063Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/0d/098f23c4c858e5de9459ffc554fa07d5493fbcfca7f040b5800cf1cecc35/websockets-17.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:76dd004f59115087c7b700474cb18f01325e37250032e19396c08ae41448e4b3", size = 217015, upload-time = "2026-08-26T14:55:45.194Z" }, + { url = "https://files.pythonhosted.org/packages/13/86/bc1317b1a4d8c4688e2a7e564b5e004dab44c2534d7ca05de6ae9a863fca/websockets-17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:581fa678ef46f4277cc8491312468e582f8ad609dbab907ba6096a08c6a0ff98", size = 214692, upload-time = "2026-08-26T14:55:46.366Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e7/df821761772beaa48c211ee0e234930b35c1473778470773823f56d3911b/websockets-17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:87f0d5e77548b0c40c8464cdb6108792e7e53f487c6400028a4ec28a8afbe5ab", size = 214959, upload-time = "2026-08-26T14:55:47.885Z" }, + { url = "https://files.pythonhosted.org/packages/3e/92/c3fb72f11764812fc648bf3838d224972427b348e8b3989d9e0a9df87da3/websockets-17.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:882af300d2c6a092b93767d5de03c7bb56dfb06314140c8e872d3f48e09f7b74", size = 224278, upload-time = "2026-08-26T14:55:49.241Z" }, + { url = "https://files.pythonhosted.org/packages/fb/05/9f82d090c8d2d861604147ef6dfb938a90b039f9358d5193f1df62558593/websockets-17.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0c863507ada5805517ca6dff1c524dcd42942efe6304dacf06700878398d21a6", size = 224557, upload-time = "2026-08-26T14:55:50.348Z" }, + { url = "https://files.pythonhosted.org/packages/8a/50/5cbf677b865290fe36819ff00615826e7edc1df38786f770123ff39a933d/websockets-17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d41ef69d5416fbc1d98cf96c37be6192d10fd101c3e0f8b3ddc36e09432b3c08", size = 225791, upload-time = "2026-08-26T14:55:51.75Z" }, + { url = "https://files.pythonhosted.org/packages/c1/1c/eb8a032285243381b09a221ae384c972d5000453ad136add4d1595cec798/websockets-17.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5aefe78e6a3077fe22b5e64b04666a85a3eb8b934d40e8595a693adcbceb6f11", size = 228574, upload-time = "2026-08-26T14:55:52.922Z" }, + { url = "https://files.pythonhosted.org/packages/69/85/413736251cb3ac04ce84cbd90e893d9a36a9698d4820b323aff3aa187e50/websockets-17.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f64e001bb7fa89b9f32cfa600bf8e9ac8ca26759d9b92ae01453ee303d9cd7b4", size = 226428, upload-time = "2026-08-26T14:55:54.263Z" }, + { url = "https://files.pythonhosted.org/packages/d2/2b/a08bcc7fa1ca81a10f84ba32b6e6edd73a913f4b0c2640eed1fd626efacd/websockets-17.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:677014a073bcb1fbaa7e21144786864f16c08f856d66834f611eceb9006cbab8", size = 225184, upload-time = "2026-08-26T14:55:55.943Z" }, + { url = "https://files.pythonhosted.org/packages/e5/8a/3bd2d0cf6b148c8c866d5d9fdcde30c04bfd81fdfac86813e69377eb4448/websockets-17.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0de501b7f2db11e83739ac20e2d33d46da4604b829f506c24be80e7def069391", size = 222430, upload-time = "2026-08-26T14:55:57.103Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c9/8e891ae342668735eabbbc669895e15195e4b45f24a4beeb58af76f414c7/websockets-17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f62114a54117e4948a1e414e89521f7fe1e3c2f83f2a571a06a4fc6718b0900a", size = 225227, upload-time = "2026-08-26T14:55:58.375Z" }, + { url = "https://files.pythonhosted.org/packages/e1/6f/c816f332dca11425e9bda7c07f7573eb5c5f8a735849d02b0d81e8ee20fa/websockets-17.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:eec113a5b41d124ef42ff56b0d74a6da3fd986400038eab9e58ee42a4024e837", size = 223831, upload-time = "2026-08-26T14:55:59.664Z" }, + { url = "https://files.pythonhosted.org/packages/53/67/5e91d5308ce24fc1ec74f56536c12f4888bad45ff5ea50f3180f8c518c57/websockets-17.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5f051f8030a51815dc00e24bd2e5f1435af095c1cc111d747ac6e2a3620d7641", size = 224600, upload-time = "2026-08-26T14:56:00.873Z" }, + { url = "https://files.pythonhosted.org/packages/bb/96/faa298ecf2570d35b0eb37caddf4992178d907e108ed74bfffb6bc092c29/websockets-17.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:655a8e28010f09fd6fa317e857afab3af7647f33e41dee88fa421e92086d1090", size = 225707, upload-time = "2026-08-26T14:56:02.001Z" }, + { url = "https://files.pythonhosted.org/packages/0b/12/5710d2482ca5061c1eec5eb46f6313837c760d4115b1795c85b6c08be4e3/websockets-17.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dc2b79afc074d2f3e64b26539350f697fe1b85ea1c49ea24eb588f247b053ce1", size = 223263, upload-time = "2026-08-26T14:56:03.092Z" }, + { url = "https://files.pythonhosted.org/packages/27/47/0c30f4eebfd1d93fae779d268f678d48847fb98516f5200849574eee8820/websockets-17.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e4bd7eacb87d8cf3ed70d6392c770a0d92441f05d7d2a3efafb5bc171d5e3067", size = 224244, upload-time = "2026-08-26T14:56:04.321Z" }, + { url = "https://files.pythonhosted.org/packages/41/33/46c256195a1255079ae23d1b1267b2e1843dc5f46a67f973cdf2a3523dff/websockets-17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ccbf3f4a9890d50b3a08ee04029fde30a03bfdeffaa19977628bf17251764e60", size = 224520, upload-time = "2026-08-26T14:56:05.521Z" }, + { url = "https://files.pythonhosted.org/packages/06/9a/aef0792731df4352e5f417369b532b3325fe434765ca90c193f594ae1e67/websockets-17.1-cp312-cp312-win32.whl", hash = "sha256:7e724f843fa6a0614aece65a7c73e51d0f4412ca41dccac13c3caf98e69536bb", size = 217485, upload-time = "2026-08-26T14:56:06.715Z" }, + { url = "https://files.pythonhosted.org/packages/50/23/493ecfdaf32898e5ea24dc900e33e5e317f9662d5d9ab2d44b2e111b4e1c/websockets-17.1-cp312-cp312-win_amd64.whl", hash = "sha256:617243e19a0992095956f406ee9cd3bc4ba92862d83cb1d83bb59ce574412bec", size = 217786, upload-time = "2026-08-26T14:56:08.055Z" }, + { url = "https://files.pythonhosted.org/packages/97/3d/91954e2f7876f74ce1213e9b92c65a63b559cc4b942a931ebeb351cd9932/websockets-17.1-cp312-cp312-win_arm64.whl", hash = "sha256:9f4a08ff7cb68c27b18e09223cc6304e01d0f82d5a240d251266dfd2e6e44729", size = 217711, upload-time = "2026-08-26T14:56:09.267Z" }, + { url = "https://files.pythonhosted.org/packages/1d/31/5f6450a7879f4f063ef08897cc385ea3ce3f1fe17f08b11e3fd959abdf27/websockets-17.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2a0162a6372110a5601cb5c9fd826635cedf69f3e110c545dd19774e040b970e", size = 217006, upload-time = "2026-08-26T14:56:10.509Z" }, + { url = "https://files.pythonhosted.org/packages/d0/2a/c1b006fc861695d2aa4e35327b842015ce1d98cf8f99241829b3d6460bfc/websockets-17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:829dba1bc049779de9b332088c1a6a9858e96bd67e50b6b644a95e02b67836bc", size = 214690, upload-time = "2026-08-26T14:56:11.681Z" }, + { url = "https://files.pythonhosted.org/packages/46/69/66e5b7d01445e0eeb1d4ab419c30315f2c90cf7a8a8cd4ecc47f894dba54/websockets-17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd8f47dbf2e8adb15c847215f83436de3fdb120b51fdae0fbbdf69fd97a3ad80", size = 214947, upload-time = "2026-08-26T14:56:12.923Z" }, + { url = "https://files.pythonhosted.org/packages/07/ce/033cafe2d2538562efa876b9149a2c7a0f7787870a4b1bb6e28adc9ceb6b/websockets-17.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9f4c0377a83e163a303514fdfab501dbe379bdc13e5b9312a91d112658b29dce", size = 224329, upload-time = "2026-08-26T14:56:14.212Z" }, + { url = "https://files.pythonhosted.org/packages/34/c7/e1c2e8a67f6cc0aa43abe0046fb3b7a020980649e6a843751dc7ce9eb170/websockets-17.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c3241d684a76eaaef8b2dc789afde4343cd3aad55ea81e4e8ab3605b529bae51", size = 224611, upload-time = "2026-08-26T14:56:15.702Z" }, + { url = "https://files.pythonhosted.org/packages/be/de/07c6d48eb3d2069709410c851e7de10ab83d752c4bd09862899627c2729b/websockets-17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e5f5c7a893507d0e83a80b88aefd6522f7e882cd53f9722c6f23f5a020c9557c", size = 225848, upload-time = "2026-08-26T14:56:16.962Z" }, + { url = "https://files.pythonhosted.org/packages/f3/dd/3c68572d20509648cc2fb6f50ccf3deeb4b87270f2c8966e99476e278ea3/websockets-17.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:00bf34b64501e3477e81fc281532ff3cbf4da26633c10b63979d5085d46602d3", size = 227290, upload-time = "2026-08-26T14:56:18.204Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4a/8f6651c8a22093539c9215af0c5bbf217b87b382c99d2112039b92d593c2/websockets-17.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ce0305b702b20d1e1d60a9aaace6bc89970e1753565543f310d549eab22c2435", size = 226476, upload-time = "2026-08-26T14:56:19.459Z" }, + { url = "https://files.pythonhosted.org/packages/f5/be/f6fc33cea86b1127fd1297b18c107e81580ab55a73a39f9a934441ef321f/websockets-17.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29176d8b429cfa0fa443c473878d37a5c06cfd0cb36b71ba4314accc71e05906", size = 225233, upload-time = "2026-08-26T14:56:20.939Z" }, + { url = "https://files.pythonhosted.org/packages/cb/83/65edaf05f7c9b1dea82f4d252fdc37706a84571646f06119a27b0a16fe19/websockets-17.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3709a1ab30b4b922027d22f68d2b61a0656a91680ac894a537624e6be7dd7f7c", size = 222488, upload-time = "2026-08-26T14:56:22.208Z" }, + { url = "https://files.pythonhosted.org/packages/07/42/d1169c2f7f1f0032b0d4b0c00f0711a070cd7c735de37bfeb876bc0f9606/websockets-17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:43bd0c1ceb924d67f5c1a5254d8361dd9d94246e6331a726064dfa2917880780", size = 225295, upload-time = "2026-08-26T14:56:23.445Z" }, + { url = "https://files.pythonhosted.org/packages/a6/f4/64e2a386c3899b917c2933225c9b47887874229d159797f3bf1a11c20d51/websockets-17.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:1fce0f43e0d41422e0b2cad6561e1970df22f212f4c7e884967df7cf591b031c", size = 223891, upload-time = "2026-08-26T14:56:24.647Z" }, + { url = "https://files.pythonhosted.org/packages/26/b3/dfb5c482f7e310a3432fdbb045ddfe6d34114680e89a233d4ff900a32961/websockets-17.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4031152769179ab8dcdeafc7b0e58052a49117560a28671700b47b2c7b717aad", size = 224661, upload-time = "2026-08-26T14:56:26.027Z" }, + { url = "https://files.pythonhosted.org/packages/a4/cf/94865130a336029f46412adc127c4fbe380f46172b90ce251369e35c4302/websockets-17.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a06f3b5085176763182449559e20391d7ce616a8972a9f7a33deda87ea6d4f3c", size = 225766, upload-time = "2026-08-26T14:56:27.455Z" }, + { url = "https://files.pythonhosted.org/packages/96/34/eb8c658f86dfe562ed49a887a27424bfe9e618c26ea6f865b093d075d3a6/websockets-17.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:77b37cceca17291897c3c73bd30a7c7c7909593554b5da574ec852af83c1742a", size = 223323, upload-time = "2026-08-26T14:56:28.807Z" }, + { url = "https://files.pythonhosted.org/packages/1b/7e/2629609652ece5ca0c7ac235927dd4511b08131e3a5d53439b798fddf002/websockets-17.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d8e83333385cac6030a5167fd18bf96cc6c58b914c308e683f05b0cf94bc8dd0", size = 224276, upload-time = "2026-08-26T14:56:29.991Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6b/8525737fe840b38e5f40956c198fb586a4fac1e07144d41a5b949b989cf8/websockets-17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:073c5c3f7e127041fa9d34a9e29ceefee8c3cafbd267ed2927318f425144380d", size = 224558, upload-time = "2026-08-26T14:56:31.184Z" }, + { url = "https://files.pythonhosted.org/packages/74/ab/3a958c6cbcf74b118f601c20a80ac8bd5e8dfec0bcf7345116feaeefb121/websockets-17.1-cp313-cp313-win32.whl", hash = "sha256:2afb58c7ba48b329d56769f8dfd89f394efe587b65ef806bae810a484d6d3608", size = 217475, upload-time = "2026-08-26T14:56:32.431Z" }, + { url = "https://files.pythonhosted.org/packages/22/36/fb521f0f2994c25509651f169efe5582dddd8713d57a0757ba87859372ef/websockets-17.1-cp313-cp313-win_amd64.whl", hash = "sha256:0340bbef6bfbe16da888b3983d666a4db4954ac3253c38f13bc7aba0c7db5a2f", size = 217784, upload-time = "2026-08-26T14:56:33.608Z" }, + { url = "https://files.pythonhosted.org/packages/68/92/9b8419584681a12a7534b746dfb2737c466efe2455483e2fbf8b941a04ec/websockets-17.1-cp313-cp313-win_arm64.whl", hash = "sha256:7a72efa3bf4fa3a6669a54420a472ad056da3973d827f10e3a536da463f926c2", size = 217715, upload-time = "2026-08-26T14:56:34.865Z" }, + { url = "https://files.pythonhosted.org/packages/90/0d/500cf5daea09d4669dff3a7d67159094a0bd6c4ef130381404f6edd3eb5f/websockets-17.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0c9982938980e086da59f70d05f9418cd143401a601a0faac10fa48f7bb1cd3e", size = 217048, upload-time = "2026-08-26T14:56:36.03Z" }, + { url = "https://files.pythonhosted.org/packages/97/12/5b12c6168aa269cffbfd24d177cd492b130120403a418c7e89462e27b4ac/websockets-17.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:57b39dc8541cf7ed3f639da82bf7451060483967f9e733da1f8173e4095f0642", size = 214737, upload-time = "2026-08-26T14:56:37.43Z" }, + { url = "https://files.pythonhosted.org/packages/0c/36/e453e5106e4e2416f008ac222837c2f1637a063b08008afcd1088889b631/websockets-17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:96abdecbaae746851b87c3a36cb4a661df93ca3d92f114270f79228bf1d00de6", size = 214955, upload-time = "2026-08-26T14:56:38.71Z" }, + { url = "https://files.pythonhosted.org/packages/dd/30/0204bb86176db02cdfc678ce65ed808a66fab87d250ce61a8790800a60b0/websockets-17.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d9fc873e239c5abeb150bc24dbd1a7af23a9254526383ce0a077f5e20adbeb19", size = 224331, upload-time = "2026-08-26T14:56:39.924Z" }, + { url = "https://files.pythonhosted.org/packages/46/c8/d8372256e00c4e3cab1115c45075d1eeedb642a3f2b42bd70c4deae03f06/websockets-17.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6f42912fa9eb4cb7c7ec9fde9b3332ba339eb8a8811981043d4029599f3d950b", size = 224685, upload-time = "2026-08-26T14:56:41.169Z" }, + { url = "https://files.pythonhosted.org/packages/12/7d/650355b8f67f908ff99603351d4458d1a0b787d627950a47c38db7e25308/websockets-17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f98bf378d7a5be047a044a1a27c987a8f355e10e3b5754617dbe756248cbc5ce", size = 225927, upload-time = "2026-08-26T14:56:42.359Z" }, + { url = "https://files.pythonhosted.org/packages/34/6c/a9ffa5b903579eed76017870f055d75ecc73988d9d0c9b65a92ba0bf2a27/websockets-17.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d334d11398086bb5559606cb42d51c013ea7c061c7db701521392373d3c087f5", size = 227300, upload-time = "2026-08-26T14:56:43.538Z" }, + { url = "https://files.pythonhosted.org/packages/9b/5d/4551c2269066af7481ee44605a0813770961615b5b5da3e87a8f5cb859ea/websockets-17.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c27336b1a0ac56569493e858497870347854372395f50483725f8cdacc5a45c", size = 226533, upload-time = "2026-08-26T14:56:44.669Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/237a99233e5c445759a613831b3a92e91905afc064dc3bd0ad33c35fd1e2/websockets-17.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:67258b00302a5aaf0b267771c7014b13429abd7ea17eebc4c55bd935ff101555", size = 225280, upload-time = "2026-08-26T14:56:45.83Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b5/e9407a91613d1d1cd932414143a1012096b26674a782fc55a0bd23217ee4/websockets-17.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:455ffeea0879d313205df1e745e5883e1feb7f31ecd26be882f5f0babd3db04f", size = 222540, upload-time = "2026-08-26T14:56:47.053Z" }, + { url = "https://files.pythonhosted.org/packages/db/d2/db76628db0577b783205d9779f64d8e373416b04c62d1546be4b75dc8540/websockets-17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f7233eaf441a345a5943a929fd4b5ea3278f11aed35a9ed0f3106b8cb3ca846a", size = 225354, upload-time = "2026-08-26T14:56:48.32Z" }, + { url = "https://files.pythonhosted.org/packages/a9/4c/2174181c067b89a74ae18e2650c2ac29959f4b796afe876ab3f4d30d642c/websockets-17.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c65da239a5ad553619804c1f9d65c1a0b3005381c6158ee14da2c7444cbd0c78", size = 223867, upload-time = "2026-08-26T14:56:49.579Z" }, + { url = "https://files.pythonhosted.org/packages/df/75/274decb9a8253561b5be3261e02a6676fc8ecdf31e95b722e53d5bfb8fd2/websockets-17.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9fa1ffa08c81a4f809cdab6129f8e55bee4650b9d6d3461019dda73aacd146b6", size = 224652, upload-time = "2026-08-26T14:56:50.885Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e6/49824f1fb4db7656d2f7492b1d8be16147b759d909490e32f4776843ee64/websockets-17.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:406b8107943a43ef4649b1e0cb0cdc052bbf08fe1c8905a623c4af9586e5cebb", size = 225822, upload-time = "2026-08-26T14:56:52.356Z" }, + { url = "https://files.pythonhosted.org/packages/b8/6a/5dc43838c0b02a95f42c47a0de33c5ddd7767a9feeb4d0d8777ac1cfefe4/websockets-17.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:4e8ffcb486c8490a34a4cef5e4409d8da5a1cb1681e5bf7d786ce5e84aa8540d", size = 223379, upload-time = "2026-08-26T14:56:53.699Z" }, + { url = "https://files.pythonhosted.org/packages/c2/62/585637cf06d6b321232f79c55dc14d65518d12cf87c94c44f5864068810e/websockets-17.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:fb88076df585b69c5761c387c0081aa87d7b9eb1b205a6535ca4777e25650d81", size = 224330, upload-time = "2026-08-26T14:56:55.184Z" }, + { url = "https://files.pythonhosted.org/packages/de/68/c3b234a6a1366b6ab5bbfaa4434a1b946e1dc4e8ddd6824bfd93a8835b7f/websockets-17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5d4724255fb8398acd9e583b97eb2279cec20e0bd0f9a94bf75f6056ef9f13da", size = 224622, upload-time = "2026-08-26T14:56:56.393Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d4/84cf3d1376f5d8207f55f43c1c818babd6b89447f5dcd01f18a6d5526796/websockets-17.1-cp314-cp314-win32.whl", hash = "sha256:be3f0129c5654517b2abf07dcb75bb1d9479759a4ccfb569e8293579e9fc029a", size = 217036, upload-time = "2026-08-26T14:56:57.652Z" }, + { url = "https://files.pythonhosted.org/packages/d0/0f/9e7ac63c5d7cb642952200814f584318e65146df008b7d375d5d9c6b2c97/websockets-17.1-cp314-cp314-win_amd64.whl", hash = "sha256:2a4dc6ef83f4559e0d05f313a375cb38f63c986096a9da99fe94fdd779d313e5", size = 217382, upload-time = "2026-08-26T14:56:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/1ae6b91f7f3ac05f5c9f14a72dc2181c115ff370bcd8a7f10f02c174adfd/websockets-17.1-cp314-cp314-win_arm64.whl", hash = "sha256:46c0331c9eaaf73a559f3a9e388466be0df96eb83d40f06f1ca6ab6613b35c82", size = 217268, upload-time = "2026-08-26T14:57:00.654Z" }, + { url = "https://files.pythonhosted.org/packages/b3/f0/f65644d0e0b2b90918a8c41503841cc4072a58f2bf76c09bc36e751fc0dd/websockets-17.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d411ea5ca18ac1b12c0c94be88b60c18ca641ac43bcdfdf1c9f79d46cdbe1603", size = 217379, upload-time = "2026-08-26T14:57:02.181Z" }, + { url = "https://files.pythonhosted.org/packages/ff/35/4c46d1f620ac1a30f92b6eae78ee40a772a93f568647ca7ccdc5ea283cf8/websockets-17.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:07fa3e7c30e2c577928d359b56bf872a3e0cbcc15553eaa0907c1ee86344b56f", size = 214911, upload-time = "2026-08-26T14:57:03.478Z" }, + { url = "https://files.pythonhosted.org/packages/04/6e/4587e8406d7c1188e97b9cf466c081e93399380d447f885bfce81626cd37/websockets-17.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6de9acef07e3a78e9567fcd26c29011a4da8f050b13004bbf880a0fd82a6eea5", size = 215115, upload-time = "2026-08-26T14:57:04.692Z" }, + { url = "https://files.pythonhosted.org/packages/ec/06/1381c8fff525041025909eb80ace32489194a00ba22a0a8d428030afcc84/websockets-17.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ea0ed9373b880115911d9d39634bccc95b8ce590c9c42e8589f5cacc3ef3cee2", size = 224696, upload-time = "2026-08-26T14:57:05.899Z" }, + { url = "https://files.pythonhosted.org/packages/36/9d/9034e867dc85340be058619751742b895f722326e83100d110063461ca07/websockets-17.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50903d335bfda026c2fa11dd9aed09d8cbee0c451e3a85122a9acb041b7dc69b", size = 224975, upload-time = "2026-08-26T14:57:07.262Z" }, + { url = "https://files.pythonhosted.org/packages/40/eb/ed03aa3cae748ebf6397e5d44028f433f746bad09dc568ff754fda3a3c9b/websockets-17.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a74531ce81af587f906ab42f194032388fcff8fc7938402e5917c9147a39441", size = 226151, upload-time = "2026-08-26T14:57:08.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c9/cc1964a096d16f3b73cb1ee5f14f277f5a3bcac07c6e8f9a1dcded99f4c8/websockets-17.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8fbf28e639544503b7d1c96452a5e5e043e4108d89b1f3fa02910603622d19db", size = 228292, upload-time = "2026-08-26T14:57:09.846Z" }, + { url = "https://files.pythonhosted.org/packages/1a/26/46da6dd0363c2db2e4876fd59a40fd40c1943a82d7018d0a33afbce47d52/websockets-17.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f612dc57f00c07cf4aa2673f7cbceabd654ad2457b7e639f061b794d6e11f9fd", size = 226722, upload-time = "2026-08-26T14:57:11.118Z" }, + { url = "https://files.pythonhosted.org/packages/78/98/ecd8f5e1c5d0e54c08ebc5c66852271112166db68107cb0e17ca1bf25009/websockets-17.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1c7ac77401227212dc6e849182feee50d57cf456ec6329ffd6979c94bb136c5c", size = 225451, upload-time = "2026-08-26T14:57:12.601Z" }, + { url = "https://files.pythonhosted.org/packages/65/4d/da8d2760db53e17aae763738b6ba834b1fcf16813d3632f3edb6951e1ec8/websockets-17.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32a2a68d989d6e5b74a9d5095415c51189ebae29fceb7cf2b64a1c0318a81256", size = 223003, upload-time = "2026-08-26T14:57:13.875Z" }, + { url = "https://files.pythonhosted.org/packages/a4/40/ea401c141a79c5b1d0021a0dab9d0df2051c108f1620fbb39a6e7c714c3b/websockets-17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:aec00f018d34c67500ff0438dc314b40277be4a1b983cbacbf53ccf7db63e257", size = 225704, upload-time = "2026-08-26T14:57:15.091Z" }, + { url = "https://files.pythonhosted.org/packages/e1/8e/07ab3f44215d89840d5385fdcaaab1fed8caeffa67c6899e15062957c12c/websockets-17.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:0014eaff8ad5b3b43feda2279f9d34bf2eaae040720b9fbbb55944b10f40b14d", size = 224192, upload-time = "2026-08-26T14:57:16.3Z" }, + { url = "https://files.pythonhosted.org/packages/58/93/ccf1af0a23e5748d4e22292a377d78d15cf294d7e707bbb11a8990ae6bd5/websockets-17.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:db9d7ee47f3ba531e278be539af39e2c7c7d28fb94897b6cd1120d63b0ef5922", size = 225082, upload-time = "2026-08-26T14:57:17.531Z" }, + { url = "https://files.pythonhosted.org/packages/e2/db/e32200f99ce282e728d2929f2c429db353cf3282db7d0eba99eb32c9fec1/websockets-17.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:ff3e2ba7a9f0a110b0555452e9b5a03a34e11662544e01beea15f144b48ba7b7", size = 226101, upload-time = "2026-08-26T14:57:18.802Z" }, + { url = "https://files.pythonhosted.org/packages/28/3d/e7a6e9777b29433620167c98f3caaff0d6b08b1239a273ef7f7fd1393349/websockets-17.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6da17fc94bd270f5987b10bee113461ac36a36a98b0481ddcc98056e5a90001a", size = 223794, upload-time = "2026-08-26T14:57:20.313Z" }, + { url = "https://files.pythonhosted.org/packages/48/05/ac569090726dedd6656f3ee28b0c02dfb1ba76e898dceaccc2987a237cef/websockets-17.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:e8dc3fa6d6b7ead3f9de57895f41b116a28787548e066365d9d90f7356bcaad2", size = 224567, upload-time = "2026-08-26T14:57:21.634Z" }, + { url = "https://files.pythonhosted.org/packages/14/50/4ef62941111db6b31193f4fabbb65f845a5177579040cb8fe0d774d25034/websockets-17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b65d5fe48219dc2d5e158de9e6514e75600f379cc7e37108d35f31764c155566", size = 224993, upload-time = "2026-08-26T14:57:22.86Z" }, + { url = "https://files.pythonhosted.org/packages/28/42/2b95ada4ea19bf3a2072b68669ce4f4afb212690b727d31640576287fd68/websockets-17.1-cp314-cp314t-win32.whl", hash = "sha256:2cce251f3e2469b99b6802b55435bcdd07123b41870f54c87b336183af9d7e68", size = 217168, upload-time = "2026-08-26T14:57:24.466Z" }, + { url = "https://files.pythonhosted.org/packages/32/0a/67d5ee08dd8060a37d612fd40a625b5376ad19ae48fe1c8ad428c278b817/websockets-17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8f6c38cdcaf98a911d7acc25577f2f9e710f3a2fc2bde1563556784320196b51", size = 217508, upload-time = "2026-08-26T14:57:25.983Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/822005d0c674451d2411027b878cdc128a2b7ea5a30d337d9e279da22eba/websockets-17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:d1e2f5fa2b6d01f0d85b4f223fea7ed1d504be282a02a81bd2be4817ef7a2f03", size = 217425, upload-time = "2026-08-26T14:57:27.324Z" }, + { url = "https://files.pythonhosted.org/packages/de/d5/99a6c6a1eb5d5ae9f45f59a3c97f4e3b21f310eb404a547fb3e7d2fc054c/websockets-17.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:88381602e379165b66244b2ebc29f9b23ea0851fbe63ae157f91ca324f072d6f", size = 216970, upload-time = "2026-08-26T14:57:28.575Z" }, + { url = "https://files.pythonhosted.org/packages/a6/0e/1e7f6e833728193958d3ed3d67b5d57c3c7cfa948abf94d4bc553257c954/websockets-17.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:88bc5138e53903a85c354e59df7ba73ce306f7b09724cef74dba121e60a88ce2", size = 214699, upload-time = "2026-08-26T14:57:29.862Z" }, + { url = "https://files.pythonhosted.org/packages/07/00/95d39549f86e34425a0412bcbe61708dd1fc46af654e2134a6c4389102ad/websockets-17.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:3546ef55b3a074494106508bc6505c73825970d2d9505f7bf53882b3e88b0d1e", size = 214927, upload-time = "2026-08-26T14:57:31.148Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ff/b442415fc4f7f9943b0fc8e8eebaa13923ca73361e167c439ba634eecbd9/websockets-17.1-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9ae55d24241fc055f22aea3ac924559069848bd0ad4ea065fdd72d2194685fe8", size = 224373, upload-time = "2026-08-26T14:57:32.833Z" }, + { url = "https://files.pythonhosted.org/packages/a8/dd/b83537aae4cf61615b9d8b2dbb235c0030ba85457a6d934798273814600f/websockets-17.1-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d7b349265fad6244013eecd99df8d83c12bf3013943e431f4fadd5bffc37db42", size = 224801, upload-time = "2026-08-26T14:57:34.041Z" }, + { url = "https://files.pythonhosted.org/packages/76/83/5ab0abed58454909e8dbab45086ac68ee4556d7a8ada26735addc909b903/websockets-17.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc5789e5ea182b77a38881383ada5347202a6c66f4857d054e075290e80b604b", size = 225967, upload-time = "2026-08-26T14:57:35.292Z" }, + { url = "https://files.pythonhosted.org/packages/4b/26/e2412f2b998a8c1dfc00c0709ff6ee0c634dd0b0b4f92bdfe9667876b71c/websockets-17.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ce13c7d233239e739600a57d4a73c1192ad8259e655a4d55aa1a454242bc809d", size = 227664, upload-time = "2026-08-26T14:57:36.493Z" }, + { url = "https://files.pythonhosted.org/packages/ec/25/0dd4495df3c0e02f6db705312ba85ab9b2dd42257dc23eb0da10066e4844/websockets-17.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1036189bd34b0bc1b10a4679321e2c7968af317efe6e8e4c1c5141c4254fb5bb", size = 226447, upload-time = "2026-08-26T14:57:37.781Z" }, + { url = "https://files.pythonhosted.org/packages/be/67/6df3f63ffc48f08126ed0cd2fd2a41092967c3e364f8ec100deae90b6d77/websockets-17.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e78fd4b7b2c5086a38671c9c882c1e643385eccea360b5b1fda4a105e590087e", size = 225343, upload-time = "2026-08-26T14:57:39.133Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8d/a8479bbb09ff054907d141123d8f52fb6ae5ac39c6dbe39e6a02a8408309/websockets-17.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:46e7a10bf04318c7b0c0273791925ae5e1cbe4a11e34aa934d2ef27862058a80", size = 222748, upload-time = "2026-08-26T14:57:40.478Z" }, + { url = "https://files.pythonhosted.org/packages/40/fb/4c3d2a3269cde3f3087916de9c3d9fc5d7196b46846d8c3a9ae59ad0a884/websockets-17.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:33e45c7ea38428e740a7f233555d71df0b875cef7fc080acebc9654475e35335", size = 225453, upload-time = "2026-08-26T14:57:41.859Z" }, + { url = "https://files.pythonhosted.org/packages/7f/1c/6467b401d19408f34e1c7389c222c2c7e1dfdf08c551190269b5eabc726c/websockets-17.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:6e63c01803be425ff062b7f7fc201a74def1d49fc94a2410dd17375df75936e9", size = 224112, upload-time = "2026-08-26T14:57:43.136Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5f/744e032ac80e11039a7447657ebabb46e9b5c2dbcec83be571335212932f/websockets-17.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:722ec21717eec6477bce582147a28acdfe034e604239466a6a95daedb863e774", size = 224646, upload-time = "2026-08-26T14:57:44.871Z" }, + { url = "https://files.pythonhosted.org/packages/9f/47/bcb9128d9afc4d0934d9192e2a24897ca2f7a63df2654904915349c6c46d/websockets-17.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:e74e41f0ad12ff1e8983e349daef79d37cc8280c743ce9d134d6c74c18dab5d6", size = 225797, upload-time = "2026-08-26T14:57:46.338Z" }, + { url = "https://files.pythonhosted.org/packages/c7/e0/b058047b7cf565e1105b10ef6b6b24a6ebe3575678c7dc75a645334705a7/websockets-17.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:12fe8984a32dbfd084e0603f1a8d740c0180cb85b3174585c54a80d2455a8394", size = 223605, upload-time = "2026-08-26T14:57:48.175Z" }, + { url = "https://files.pythonhosted.org/packages/b9/69/fc1555bff884de363f1bf9eebf2836dbeb29fa7e4f957debb7bbcf43abba/websockets-17.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:01dcb47deebc40b38fd4a493b9b9f4d0a704b7bec6f35e4d34085b329abce71a", size = 224508, upload-time = "2026-08-26T14:57:49.407Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f9/648d4e68621688b19093b06f7b497d520952e68cdea1c1b54371fe9491de/websockets-17.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f4c45ee2512d3757b5e6c67c5a34e435143f2ecb7df3324f9fd888688c45c0f4", size = 224767, upload-time = "2026-08-26T14:57:50.799Z" }, + { url = "https://files.pythonhosted.org/packages/58/93/f8342b55864f71df13eb8e9ef7dce691b87a87f04f75bb8a1385b3336e7c/websockets-17.1-cp315-cp315-win32.whl", hash = "sha256:0f4f50dfe2cc810fc4e2de979b35e83bf8bb4bccdc6fe472d93762ea7b1d5927", size = 217003, upload-time = "2026-08-26T14:57:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f0/7b5fdb774c245e0b6217009e2a24d2105c1a64923949f33be41aa7959302/websockets-17.1-cp315-cp315-win_amd64.whl", hash = "sha256:4af784f3e436f65b355c117c6497320f2b5cf6a559295cb1c4c7338e335d45cc", size = 217300, upload-time = "2026-08-26T14:57:53.492Z" }, + { url = "https://files.pythonhosted.org/packages/76/33/1fe6ed1b5087516115ca451b2c240314b010647071f8fc3bd78a21e4dddb/websockets-17.1-cp315-cp315-win_arm64.whl", hash = "sha256:d58159af7835fde09c462394293c0d7aaf8fb4557d8f8e5699f5e722ccae013d", size = 217214, upload-time = "2026-08-26T14:57:54.88Z" }, + { url = "https://files.pythonhosted.org/packages/94/ca/ed02e75996a266d76c5fcb5dd9b930db4cf2b388ca5fa3d2a72086f81568/websockets-17.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:1a5cf4e7bbe3ca499e6a289206cb4fcb7444b09919e129bd517f57d5fa192c13", size = 217282, upload-time = "2026-08-26T14:57:56.108Z" }, + { url = "https://files.pythonhosted.org/packages/bd/7d/d536f5bc89ea5b52fd1c1727c59fabafee6bc41f5ce92c3bd2f83047908c/websockets-17.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:416b4bc8789a1865a3ff643ec4ee073a5f52402d0dbeafd27b1798d5dd6b6a51", size = 214863, upload-time = "2026-08-26T14:57:57.355Z" }, + { url = "https://files.pythonhosted.org/packages/37/37/944cf17bad668e9be1247e6314f88a48b9faf7c250e383410db8b38af0b9/websockets-17.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:259f45358c76d3b18489e3e80636cdbe807e05ecf1b10fdf1a779106d23d0c8e", size = 215073, upload-time = "2026-08-26T14:57:58.719Z" }, + { url = "https://files.pythonhosted.org/packages/74/bf/3267966cc1bbc2b8fa62fd329651b0af502df1f5d1c0eed027ff339d6aa8/websockets-17.1-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d9d01e8ede41fea4f5a847dad9d628355f74905f437a5b6856d67aa66d193800", size = 225229, upload-time = "2026-08-26T14:58:00.235Z" }, + { url = "https://files.pythonhosted.org/packages/7f/d8/85ea722f483510abb39fc71aafb4465d17cf9051a275ab036874ff3c300c/websockets-17.1-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7b35181a14cbfcae163b4de545d22abfd07d06c2c41ca69cfcd99251d6888ab", size = 225500, upload-time = "2026-08-26T14:58:01.994Z" }, + { url = "https://files.pythonhosted.org/packages/50/ce/64c7d00005bd0d15ecb5c5fcb7fb2597b6b92ddd16c4fa6bbc3d2835ad63/websockets-17.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a8e768a048c2220697477ce2e67e4345dc9f693d0ee6af53945b5e30227c6a7", size = 226829, upload-time = "2026-08-26T14:58:03.327Z" }, + { url = "https://files.pythonhosted.org/packages/b4/dc/096c67940fb957e667ca3c542818150434eb0388c6fdc90b3a502f3c3e96/websockets-17.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:880069d21cc33a558dcf180924a546d1ecf8ada5be3e4e70acee87019d706a24", size = 228457, upload-time = "2026-08-26T14:58:04.78Z" }, + { url = "https://files.pythonhosted.org/packages/51/fe/f2331b6b7ccc67589891da354fa46a5cb79e95f83b9fd0e734d77f1f2140/websockets-17.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cec1bb8f22abccc8d20f8ca63df9be41600c26c190f4b97ee86c675fd4a863a6", size = 227265, upload-time = "2026-08-26T14:58:06.102Z" }, + { url = "https://files.pythonhosted.org/packages/47/a5/fb1642302f8ec77ca922203074f155a9831a5128ad75e725059a476d1227/websockets-17.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f3a1d577e081667dda7f8e5b4796e6e32f9713c93e2a3d930669519840a3c623", size = 226143, upload-time = "2026-08-26T14:58:07.464Z" }, + { url = "https://files.pythonhosted.org/packages/d7/41/7133fcfb63f5562750b269d6a845c689dde6a2c6407286da395beea19ddd/websockets-17.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc053f9e95a76213c5eb7ed95779f7daf0d2bf0e4e03073629ebfa43a033f151", size = 223501, upload-time = "2026-08-26T14:58:08.766Z" }, + { url = "https://files.pythonhosted.org/packages/64/b1/82b36bfabc79ff2d383a1fc043cee6a13f794ef4f6bf1b4810ad6988cf6f/websockets-17.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:bb0efe019480a1c93e168ce96479273aaebd672fc8c350d5eed1e507ababb1b8", size = 226330, upload-time = "2026-08-26T14:58:09.987Z" }, + { url = "https://files.pythonhosted.org/packages/41/7d/5b511b9bf6e9ad331e6ff902fcbcc71c3794d10ef3b5efe80ccb8f0a7861/websockets-17.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:615746b12b26a3fd4077bc6fbeb277a1c192a45dd57b531d07ad9ed5c52a9a7a", size = 224980, upload-time = "2026-08-26T14:58:11.303Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/aed08f25301f8eef23be903ff9319fcf35630ca2bdec9d226f7d804dd5b3/websockets-17.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:1a20136d61f9ca3a31493732762661fafc2c20e8861930214e21afc6a8a692a2", size = 225478, upload-time = "2026-08-26T14:58:12.543Z" }, + { url = "https://files.pythonhosted.org/packages/3e/47/0d63d4168536b4682c9d19b7399443b1176f25dbb68878374fa716670230/websockets-17.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:2786cbd273ab69c22612db8a41229ddf2c158060b17b5928884bf388d07887f3", size = 226588, upload-time = "2026-08-26T14:58:14.457Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/844bd0b6386fc81ed6a55f4b6dd26f01c6987eda205afa10175ea12b2164/websockets-17.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:b1c323fc3be1dc3f87f6c59458cb7d9e13dcbbf971d6c3f3e2bbaf58d3bfcdfe", size = 224336, upload-time = "2026-08-26T14:58:15.778Z" }, + { url = "https://files.pythonhosted.org/packages/96/18/03709c84bc88ec4dcea68d4be4ccd07d611073dec111203a5bf45af8809d/websockets-17.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:12c8e2b25df59755954a04dfa09c990b96691025aaf7eafd19ed6da24b09c18d", size = 225197, upload-time = "2026-08-26T14:58:17.141Z" }, + { url = "https://files.pythonhosted.org/packages/27/cf/0d1c694b6466c89e875b85b32b51312c472cf6708eee91914866f5087dde/websockets-17.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f58f58b4b29bbea2a3635e2c56eff4a3adab011fe383802a9e542e31b97085fc", size = 225493, upload-time = "2026-08-26T14:58:18.521Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f5/99857c3dd9676749f33e3668665a34ad6099505fb8d75eb084f49f7807a9/websockets-17.1-cp315-cp315t-win32.whl", hash = "sha256:f78a3ffb1994304db2c0c4588e4d1a518079b557054fa3bb985a6f5e50ff49a3", size = 217130, upload-time = "2026-08-26T14:58:20.037Z" }, + { url = "https://files.pythonhosted.org/packages/2c/84/77599922ab441bfe61508f97dab2c71f8e114d31793993ea54011db16199/websockets-17.1-cp315-cp315t-win_amd64.whl", hash = "sha256:ad68c28a27246fed109a4409393d677b7e1388345cbbd2f5aee5c182d8506110", size = 217448, upload-time = "2026-08-26T14:58:21.382Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3c/8b9a225b523f06a9389be81f1b0ab07c49bec6014742e6aa359c1f920f1f/websockets-17.1-cp315-cp315t-win_arm64.whl", hash = "sha256:e552e0037230ac16e5f568de7012041344d1b18c9feed30ec2891b8eba55af81", size = 217372, upload-time = "2026-08-26T14:58:22.807Z" }, + { url = "https://files.pythonhosted.org/packages/41/63/23572870e01836a98346075b9e17a8bc24a6ddd9800a3204ceee58677f3c/websockets-17.1-py3-none-any.whl", hash = "sha256:f221081107b8c48184d99f7019604486376e7ef826037e70aad6b02540732c23", size = 211134, upload-time = "2026-08-26T17:25:31.397Z" }, +] + [[package]] name = "widgetsnbextension" version = "4.0.15" From a5f3591732b075ed1e845a2e24e8829ee34831c8 Mon Sep 17 00:00:00 2001 From: Adrian Benson Date: Mon, 28 Sep 2026 14:28:15 +1300 Subject: [PATCH 29/36] Add method to catch percentile clipping issues. --- src/gsolve/gsolve_algorithms.py | 77 ++++++++++++++++++++- test_clip_check.py | 116 ++++++++++++++++++++++++++++++++ 2 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 test_clip_check.py diff --git a/src/gsolve/gsolve_algorithms.py b/src/gsolve/gsolve_algorithms.py index 60ee014..dc5e21a 100644 --- a/src/gsolve/gsolve_algorithms.py +++ b/src/gsolve/gsolve_algorithms.py @@ -17,6 +17,8 @@ # Copyright (c) 2025 Earth Sciences New Zealand. """Functions and Classes for performing network adjustment of gravity data.""" +import pathlib +import warnings from typing import Any import numpy as np @@ -27,6 +29,8 @@ __all__ = ["GSolveSolverMethod", "call_gsolve_calibration", "call_gsolve_lstsq"] +# make warnings show caller location + _GSOLVE_SOLVER_METHODS: dict[int, str] = { 1: "Unconstrained least squares", 2: "Partially constrained least squares", @@ -34,6 +38,18 @@ } +class GSolveSolverWarning(UserWarning): + """Raised when gsolve may produce unreliable results.""" + + +def _solver_warning(message: str): + warnings.warn( + message, + category=GSolveSolverWarning, + skip_file_prefixes=(str(pathlib.Path(__file__).parent),), + ) + + def call_gsolve_lstsq( obs: pd.DataFrame, ref_sites: pd.DataFrame, @@ -393,7 +409,13 @@ def g_solver_lstsq( # ruff: ignore[too-many-positional-arguments] # Build mask of outliers mask = ((residuals[:n_obs] > ci_l) & (residuals[:n_obs] < ci_h)).flatten() - + _check_post_clip_data_are_ok( + mask=mask, + obs_site_id=obs_site_id, + ties_site_id=ties_site_id, + obs_loop=obs_loop, + use_loops=use_loops, + ) # Mask outliers A[:n_obs, :][~mask] = 0 b[:n_obs, :][~mask] = 0 @@ -446,3 +468,56 @@ def g_solver_lstsq( # ruff: ignore[too-many-positional-arguments] calibration_factor = None return gravity, residuals, gravity_var, drift, baseline, calibration_factor, mask + + +def _check_post_clip_data_are_ok( + mask: np.ndarray[tuple[int]], + obs_site_id: np.ndarray[tuple[int]], + ties_site_id: np.ndarray[tuple[int]], + obs_loop: np.ndarray[tuple[int]], + use_loops: bool, +) -> bool: + obs_site_id_remain = obs_site_id[mask] + obs_loop_remain = obs_loop[mask] + errs = 0 + + # warn about site removal + if len(dropped_sites := np.setdiff1d(obs_site_id, obs_site_id_remain)) > 0: + _solver_warning( + f"Sites were completely removed after percentile clipping: {dropped_sites}" + ) + errs += 1 + + # ensure all tie sites made it + dropped_ties = np.intersect1d(ties_site_id, obs_site_id_remain) + if len(dropped_ties) == 0: + msg = ( + "All reference sites were completely removed due to " + f"percentile clipping: {dropped_ties}" + ) + raise ValueError(msg) + if len(dropped_ties) < len(ties_site_id): + _solver_warning( + f"{len(dropped_ties)} of {len(ties_site_id)} tie sites were completely" + f" removed after percentile clipping: {dropped_ties})" + ) + errs += 1 + + # check that loops still have at least one observation + if use_loops and len(dropped_loops := np.setdiff1d(obs_loop, obs_loop_remain)) > 0: + _solver_warning( + f"Loops were completely removed after percentile clipping: {dropped_loops}" + ) + errs += 1 + + for loop_id in np.unique(obs_loop_remain): + m = obs_loop_remain == loop_id + in_other_loops = np.intersect1d(obs_site_id_remain[m], obs_site_id_remain[~m]) + if len(in_other_loops) == 0: + _solver_warning( + f"After percentile clipping, loop '{loop_id}' has no sites in common " + "with the rest of survey" + ) + errs += 1 + + return errs == 0 diff --git a/test_clip_check.py b/test_clip_check.py new file mode 100644 index 0000000..6831c5d --- /dev/null +++ b/test_clip_check.py @@ -0,0 +1,116 @@ +# # %% +# %load_ext autoreload + +# %autoreload 1 + +# %aimport gsolve.gsolve_algorithms +# %aimport gsolve.observations +# %aimport gsolve +# %matplotlib inline + +# %% +import pathlib + +from gsolve import ( + GravityObservations, + GravitySites, + GravitySurvey, + LaCosteRombergDialConverter, + ReferenceGravity, +) +from gsolve.tide.earth_tide import LongmanTidalCorrection +from gsolve.tide.ocean_load import generate_qtp_input, qtp_to_corrector + +# %% +data_path = pathlib.Path("examples") + +obs_path = data_path / "surveys" / "Okataina" + +ocean_load_path = data_path / "ocean_load" / "quicktide" + +survey_file = obs_path / "Okataina_2020_all_4_gsolve.xlsx" + +ref_site_file = data_path / "absolute_gravity" / "base_stations.csv" + +corr_table_file = data_path / "correction_tables" / "G106.csv" + +# the calibration factor for your meter (determined in calibration survey) +calibration_factor = 1 - -0.0019 + + +# %% +# Read in observations +obs = GravityObservations.from_excel( + survey_file, sheet_name="Survey Data", parse_split_datetime=True +) + +# Read in site location information +sites = GravitySites.from_excel(survey_file, sheet_name="Locations") + +# Read in list reference (i.e. absolute) stations +ref_sites = ReferenceGravity.from_csv(ref_site_file) + +# set which reference stations are used in this survey (must be in sites) +_ = sites.set_reference_gravity(ref_sites) + +# plot a network map + +# %% +"""Process the observed data. +As this is a manually read G meter we need to convert dial values to mgal via a +conversion table. First read in the conversion table""" +g106converter = LaCosteRombergDialConverter.from_csv(corr_table_file) + +# apply dial conversion to convert values to mGal. +obs.apply_dial_to_mgal(g106converter) + +# set the calibration factor +obs.set_calibration_factor(calibration_factor) + +# calculate the earth tide correction which requires location information from sites +longman = LongmanTidalCorrection(amp_factor=1.2) +obs.apply_earth_tide_correction(sites, tide_corrector=longman) + +# Ocean Load Corrections +# - these are generated externally using Quick Tide Pro or similar. + +# Step 1: generate the input file for QTP using the site and observation datetimes. +# - this has been run, uncomment code below to generate a new file. + +# generate_qtp_input( +s = obs.data.site_id.to_numpy() +generate_qtp_input( + site_id=s, + datetimes=obs.data.datetime, + latitude=sites.data.loc[s, "latitude"].to_numpy(), + longitude=sites.data.loc[s, "longitude"].to_numpy(), + elevation=sites.data.loc[s, "height_ellipsoidal"].to_numpy(), + output_file=ocean_load_path / "okataina_qtp_input.csv", +) + +# step 2: run QTP externally to generate the output file (not shown here) +# Step 3: read in the QTP output file and convert to a corrector object. +# - QTP output file +qtp_output_file = ocean_load_path / "okataina_qtp_input_Modified.csv" + +if not qtp_output_file.exists(): + msg = "You didn't run QuickTide Pro yet did you?" + raise FileNotFoundError(msg) + +qtp_ocean_load_corrector = qtp_to_corrector( + qtp_output_file, +) +obs.apply_ocean_load_correction(corrector=qtp_ocean_load_corrector) + + +# Calculate the final corrected gravity value that will be passed to network adjustment +# - all previously applied corrections are included. +obs.calculate_tide_corrected_gravity() +survey = GravitySurvey(obs, sites) + +# %% +""" +Run the network adjustment. +Here we use solve method "2", see documentation. We process each loop individually +and apply a 95 percentile cutoff filter to the residuals.""" +results = survey.solve_lstsq(method=2, use_loops=True, percentile_clipping=90) From b01f5e7f96c59a1eeb97eb1183f568596465d84a Mon Sep 17 00:00:00 2001 From: Adrian Benson Date: Tue, 29 Sep 2026 11:38:01 +1300 Subject: [PATCH 30/36] Add todo notes for future work. --- src/gsolve/core/data.py | 4 +++- src/gsolve/gsolve_algorithms.py | 35 +++++++++++++++++++++------------ src/gsolve/tide/earth_tide.py | 4 ++-- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/gsolve/core/data.py b/src/gsolve/core/data.py index 6a8ef39..435332c 100644 --- a/src/gsolve/core/data.py +++ b/src/gsolve/core/data.py @@ -104,7 +104,9 @@ class DataFieldSpecification: DataFieldSpecification("absolute_gravity", float, default=np.nan), ] # TODO: make this a class? -COMMON_FIELDS: dict[str, DataFieldSpecification] = {f.name: f for f in _COMMON_FIELDS} +COMMON_FIELDS: MappingProxyType[str, DataFieldSpecification] = MappingProxyType( + {f.name: f for f in _COMMON_FIELDS} +) @dataclasses.dataclass diff --git a/src/gsolve/gsolve_algorithms.py b/src/gsolve/gsolve_algorithms.py index dc5e21a..7e0d12c 100644 --- a/src/gsolve/gsolve_algorithms.py +++ b/src/gsolve/gsolve_algorithms.py @@ -403,6 +403,7 @@ def g_solver_lstsq( # ruff: ignore[too-many-positional-arguments] residuals = b - np.dot(A, solution[:n_parameters]) # Define percentile clipping interval + # TODO: Should this be done using residual**2 and clip upper only? perc = (100.0 - percentile_clipping) / 2 ci_l = np.percentile(residuals[:n_obs], perc) ci_h = np.percentile(residuals[:n_obs], 100.0 - perc) @@ -498,26 +499,34 @@ def _check_post_clip_data_are_ok( raise ValueError(msg) if len(dropped_ties) < len(ties_site_id): _solver_warning( - f"{len(dropped_ties)} of {len(ties_site_id)} tie sites were completely" + f"{len(dropped_ties)} of {len(ties_site_id)} reference sites were completely" f" removed after percentile clipping: {dropped_ties})" ) errs += 1 - # check that loops still have at least one observation - if use_loops and len(dropped_loops := np.setdiff1d(obs_loop, obs_loop_remain)) > 0: - _solver_warning( - f"Loops were completely removed after percentile clipping: {dropped_loops}" - ) - errs += 1 + # if using loops and there was actually more than 1 loop + # - check that loops were not completely removed + # - check that loops have common stations + # - maybe? check that still have intra loop repeats - for loop_id in np.unique(obs_loop_remain): - m = obs_loop_remain == loop_id - in_other_loops = np.intersect1d(obs_site_id_remain[m], obs_site_id_remain[~m]) - if len(in_other_loops) == 0: + if use_loops and not (obs_loop[0] == obs_loop).all(): + if len(dropped_loops := np.setdiff1d(obs_loop, obs_loop_remain)) > 0: _solver_warning( - f"After percentile clipping, loop '{loop_id}' has no sites in common " - "with the rest of survey" + f"Loops were completely removed after percentile clipping: {dropped_loops}" ) errs += 1 + # is this necessary? + for loop_id in np.unique(obs_loop_remain): + m = obs_loop_remain == loop_id + in_other_loops = np.intersect1d( + obs_site_id_remain[m], obs_site_id_remain[~m] + ) + if len(in_other_loops) == 0: + _solver_warning( + f"After percentile clipping, loop '{loop_id}' has no sites in common " + "with the rest of survey" + ) + errs += 1 + return errs == 0 diff --git a/src/gsolve/tide/earth_tide.py b/src/gsolve/tide/earth_tide.py index a15f4ce..2b588d9 100644 --- a/src/gsolve/tide/earth_tide.py +++ b/src/gsolve/tide/earth_tide.py @@ -1261,8 +1261,8 @@ def tidal_correction( # of date_time for this site t0 = (date_time[site_mask].min() - pd.Timedelta(hours=1)).normalize() - # TODO: need to break this up into multiple calls to time_series if the - # duration is too long for pygtide to handle + # TODO: need to break this up into multiple calls to time_series + # if the duration is too long for pygtide to handle # e.g. sites visited days/weeks/years apart -> lots of work for nowt duration_hrs = ( int(np.ceil((date_time[site_mask].max() - t0).total_seconds() / 3600.0)) From 7a6636963f4bc3de597d2ec75e2ed97ca7002cb4 Mon Sep 17 00:00:00 2001 From: Adrian Benson Date: Wed, 30 Sep 2026 01:33:49 +1300 Subject: [PATCH 31/36] Add tests --- .gitignore | 1 + src/gsolve/gsolve_algorithms.py | 22 +++-- test_clip_check.py | 116 -------------------------- tests/test_gsolve_algorithms.py | 143 ++++++++++++++++++++++++++++++++ 4 files changed, 160 insertions(+), 122 deletions(-) delete mode 100644 test_clip_check.py diff --git a/.gitignore b/.gitignore index d290cdb..2a6c60f 100644 --- a/.gitignore +++ b/.gitignore @@ -186,3 +186,4 @@ docs/_build docs/source/api/generated docs/source/api/api/* docs/zz/* +ARCHITECTURE.md diff --git a/src/gsolve/gsolve_algorithms.py b/src/gsolve/gsolve_algorithms.py index 7e0d12c..7bffb5f 100644 --- a/src/gsolve/gsolve_algorithms.py +++ b/src/gsolve/gsolve_algorithms.py @@ -489,6 +489,8 @@ def _check_post_clip_data_are_ok( ) errs += 1 + check_loops = use_loops and not (obs_loop[0] == obs_loop).all() + # ensure all tie sites made it dropped_ties = np.intersect1d(ties_site_id, obs_site_id_remain) if len(dropped_ties) == 0: @@ -509,20 +511,28 @@ def _check_post_clip_data_are_ok( # - check that loops have common stations # - maybe? check that still have intra loop repeats - if use_loops and not (obs_loop[0] == obs_loop).all(): + if check_loops: if len(dropped_loops := np.setdiff1d(obs_loop, obs_loop_remain)) > 0: _solver_warning( - f"Loops were completely removed after percentile clipping: {dropped_loops}" + f"Loops completely removed after percentile clipping: {dropped_loops}" ) errs += 1 # is this necessary? for loop_id in np.unique(obs_loop_remain): - m = obs_loop_remain == loop_id - in_other_loops = np.intersect1d( - obs_site_id_remain[m], obs_site_id_remain[~m] + m_pre = obs_loop == loop_id + in_other_loops_pre_clip = np.intersect1d( + obs_site_id[m_pre], obs_site_id[~m_pre] + ) + if len(in_other_loops_pre_clip) == 0: + continue # nothing will have changed + + m_post = obs_loop_remain == loop_id + in_other_loops_post_clip = np.intersect1d( + obs_site_id_remain[m_post], obs_site_id_remain[~m_post] ) - if len(in_other_loops) == 0: + + if len(in_other_loops_post_clip) == 0: _solver_warning( f"After percentile clipping, loop '{loop_id}' has no sites in common " "with the rest of survey" diff --git a/test_clip_check.py b/test_clip_check.py deleted file mode 100644 index 6831c5d..0000000 --- a/test_clip_check.py +++ /dev/null @@ -1,116 +0,0 @@ -# # %% -# %load_ext autoreload - -# %autoreload 1 - -# %aimport gsolve.gsolve_algorithms -# %aimport gsolve.observations -# %aimport gsolve -# %matplotlib inline - -# %% -import pathlib - -from gsolve import ( - GravityObservations, - GravitySites, - GravitySurvey, - LaCosteRombergDialConverter, - ReferenceGravity, -) -from gsolve.tide.earth_tide import LongmanTidalCorrection -from gsolve.tide.ocean_load import generate_qtp_input, qtp_to_corrector - -# %% -data_path = pathlib.Path("examples") - -obs_path = data_path / "surveys" / "Okataina" - -ocean_load_path = data_path / "ocean_load" / "quicktide" - -survey_file = obs_path / "Okataina_2020_all_4_gsolve.xlsx" - -ref_site_file = data_path / "absolute_gravity" / "base_stations.csv" - -corr_table_file = data_path / "correction_tables" / "G106.csv" - -# the calibration factor for your meter (determined in calibration survey) -calibration_factor = 1 - -0.0019 - - -# %% -# Read in observations -obs = GravityObservations.from_excel( - survey_file, sheet_name="Survey Data", parse_split_datetime=True -) - -# Read in site location information -sites = GravitySites.from_excel(survey_file, sheet_name="Locations") - -# Read in list reference (i.e. absolute) stations -ref_sites = ReferenceGravity.from_csv(ref_site_file) - -# set which reference stations are used in this survey (must be in sites) -_ = sites.set_reference_gravity(ref_sites) - -# plot a network map - -# %% -"""Process the observed data. -As this is a manually read G meter we need to convert dial values to mgal via a -conversion table. First read in the conversion table""" -g106converter = LaCosteRombergDialConverter.from_csv(corr_table_file) - -# apply dial conversion to convert values to mGal. -obs.apply_dial_to_mgal(g106converter) - -# set the calibration factor -obs.set_calibration_factor(calibration_factor) - -# calculate the earth tide correction which requires location information from sites -longman = LongmanTidalCorrection(amp_factor=1.2) -obs.apply_earth_tide_correction(sites, tide_corrector=longman) - -# Ocean Load Corrections -# - these are generated externally using Quick Tide Pro or similar. - -# Step 1: generate the input file for QTP using the site and observation datetimes. -# - this has been run, uncomment code below to generate a new file. - -# generate_qtp_input( -s = obs.data.site_id.to_numpy() -generate_qtp_input( - site_id=s, - datetimes=obs.data.datetime, - latitude=sites.data.loc[s, "latitude"].to_numpy(), - longitude=sites.data.loc[s, "longitude"].to_numpy(), - elevation=sites.data.loc[s, "height_ellipsoidal"].to_numpy(), - output_file=ocean_load_path / "okataina_qtp_input.csv", -) - -# step 2: run QTP externally to generate the output file (not shown here) -# Step 3: read in the QTP output file and convert to a corrector object. -# - QTP output file -qtp_output_file = ocean_load_path / "okataina_qtp_input_Modified.csv" - -if not qtp_output_file.exists(): - msg = "You didn't run QuickTide Pro yet did you?" - raise FileNotFoundError(msg) - -qtp_ocean_load_corrector = qtp_to_corrector( - qtp_output_file, -) -obs.apply_ocean_load_correction(corrector=qtp_ocean_load_corrector) - - -# Calculate the final corrected gravity value that will be passed to network adjustment -# - all previously applied corrections are included. -obs.calculate_tide_corrected_gravity() -survey = GravitySurvey(obs, sites) - -# %% -""" -Run the network adjustment. -Here we use solve method "2", see documentation. We process each loop individually -and apply a 95 percentile cutoff filter to the residuals.""" -results = survey.solve_lstsq(method=2, use_loops=True, percentile_clipping=90) diff --git a/tests/test_gsolve_algorithms.py b/tests/test_gsolve_algorithms.py index af44802..a1fc2e3 100644 --- a/tests/test_gsolve_algorithms.py +++ b/tests/test_gsolve_algorithms.py @@ -25,6 +25,8 @@ import pytest from gsolve.gsolve_algorithms import ( + GSolveSolverWarning, + _check_post_clip_data_are_ok, call_gsolve_calibration, call_gsolve_lstsq, g_solver_lstsq, @@ -355,3 +357,144 @@ def test_percentile_clipping_robust_geometry_has_finite_outputs(self) -> None: assert np.sum(~mask) >= 1 assert np.all(np.isfinite(gravity)) assert np.all(np.isfinite(gravity_var)) + + +class TestCheckPostClipDataAreOk: + """Tests for the post-clipping sanity checker `_check_post_clip_data_are_ok`. + + Inputs are plain numpy arrays. The `mask` selects observations retained + after percentile clipping; the checker warns (via `GSolveSolverWarning`) + or raises when clipping degrades the network geometry. + """ + + def test_clean_data_returns_true_without_warnings(self) -> None: + """Nothing dropped: returns True and issues no warnings.""" + obs_site_id = np.array(["A", "B", "A", "B"]) + obs_loop = np.array(["L1", "L1", "L1", "L1"]) + ties_site_id = np.array(["A"]) + mask = np.array([True, True, True, True]) + + with warnings.catch_warnings(): + warnings.simplefilter("error", GSolveSolverWarning) + result = _check_post_clip_data_are_ok( + mask=mask, + obs_site_id=obs_site_id, + ties_site_id=ties_site_id, + obs_loop=obs_loop, + use_loops=False, + ) + + assert result is True + + def test_dropped_site_warns_and_returns_false(self) -> None: + """A site fully removed by the mask warns and returns False.""" + obs_site_id = np.array(["A", "B", "C", "A", "B"]) + obs_loop = np.array(["L1", "L1", "L1", "L1", "L1"]) + ties_site_id = np.array(["A"]) + # every observation of site "C" is clipped + mask = np.array([True, True, False, True, True]) + + with pytest.warns(GSolveSolverWarning, match="Sites were completely removed"): + result = _check_post_clip_data_are_ok( + mask=mask, + obs_site_id=obs_site_id, + ties_site_id=ties_site_id, + obs_loop=obs_loop, + use_loops=False, + ) + + assert result is False + + def test_all_ties_removed_raises_value_error(self) -> None: + """Removing every reference/tie site raises ValueError.""" + obs_site_id = np.array(["A", "B", "A", "B"]) + obs_loop = np.array(["L1", "L1", "L1", "L1"]) + ties_site_id = np.array(["A"]) + # all observations of tie site "A" are clipped + mask = np.array([False, True, False, True]) + + with pytest.raises(ValueError, match="All reference sites were completely"): + _check_post_clip_data_are_ok( + mask=mask, + obs_site_id=obs_site_id, + ties_site_id=ties_site_id, + obs_loop=obs_loop, + use_loops=False, + ) + + def test_some_ties_removed_warns_and_returns_false(self) -> None: + """Removing some (but not all) tie sites warns and returns False.""" + obs_site_id = np.array(["A", "B", "C", "A", "B", "C"]) + obs_loop = np.array(["L1", "L1", "L1", "L1", "L1", "L1"]) + ties_site_id = np.array(["A", "B"]) + # tie site "B" is fully clipped, tie site "A" survives + mask = np.array([True, False, True, True, False, True]) + + with pytest.warns(GSolveSolverWarning, match="reference sites were completely"): + result = _check_post_clip_data_are_ok( + mask=mask, + obs_site_id=obs_site_id, + ties_site_id=ties_site_id, + obs_loop=obs_loop, + use_loops=False, + ) + + assert result is False + + def test_dropped_loop_warns_and_returns_false(self) -> None: + """With use_loops and >1 loop, a fully removed loop warns.""" + obs_site_id = np.array(["A", "B", "A", "B", "A", "B"]) + obs_loop = np.array(["L1", "L1", "L2", "L2", "L3", "L3"]) + ties_site_id = np.array(["A", "B"]) + # loop "L3" is entirely clipped while two loops remain + mask = np.array([True, True, True, True, False, False]) + + with pytest.warns(GSolveSolverWarning, match="Loops completely removed"): + result = _check_post_clip_data_are_ok( + mask=mask, + obs_site_id=obs_site_id, + ties_site_id=ties_site_id, + obs_loop=obs_loop, + use_loops=True, + ) + + assert result is False + + def test_loop_loses_common_sites_warns_and_returns_false(self) -> None: + """A surviving loop left with no sites common to the rest warns.""" + # Pre-clip, loops L1 and L2 share sites A and B (network is connected). + obs_site_id = np.array(["A", "B", "C", "A", "B", "D"]) + obs_loop = np.array(["L1", "L1", "L1", "L2", "L2", "L2"]) + ties_site_id = np.array(["A", "B"]) + # Clip A and B from L2, leaving L2 with only site D -> no common sites. + mask = np.array([True, True, True, False, False, True]) + + with pytest.warns(GSolveSolverWarning, match="no sites in common"): + result = _check_post_clip_data_are_ok( + mask=mask, + obs_site_id=obs_site_id, + ties_site_id=ties_site_id, + obs_loop=obs_loop, + use_loops=True, + ) + + assert result is False + + def test_single_loop_skips_loop_checks(self) -> None: + """With only one loop, loop-specific checks are skipped even if use_loops.""" + obs_site_id = np.array(["A", "B", "A", "B"]) + obs_loop = np.array(["L1", "L1", "L1", "L1"]) + ties_site_id = np.array(["A", "B"]) + mask = np.array([True, True, True, True]) + + with warnings.catch_warnings(): + warnings.simplefilter("error", GSolveSolverWarning) + result = _check_post_clip_data_are_ok( + mask=mask, + obs_site_id=obs_site_id, + ties_site_id=ties_site_id, + obs_loop=obs_loop, + use_loops=True, + ) + + assert result is True From d4c69afa282f777d3ab53d392215c3ffeca0b0e7 Mon Sep 17 00:00:00 2001 From: Adrian Benson Date: Wed, 30 Sep 2026 01:42:18 +1300 Subject: [PATCH 32/36] Don't spell check git stuff --- cspell.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cspell.json b/cspell.json index 491a245..39f0a1f 100644 --- a/cspell.json +++ b/cspell.json @@ -15,5 +15,9 @@ "data-science-tools", "git", "en-au" + ], + "ignorePaths": [ + ".git/**", + ".gitignore" ] } \ No newline at end of file From 8f6a81a6824fd3c67e42897162e819e59b1a45f2 Mon Sep 17 00:00:00 2001 From: Adrian Benson Date: Fri, 2 Oct 2026 16:36:44 +1300 Subject: [PATCH 33/36] Percentile clip check method with unit tests --- src/gsolve/gsolve_algorithms.py | 60 ++++++++++++++++++++------------- src/gsolve/gsolve_outputs.py | 12 ++++--- tests/test_gsolve_algorithms.py | 48 +++++++++++++++++++++----- 3 files changed, 84 insertions(+), 36 deletions(-) diff --git a/src/gsolve/gsolve_algorithms.py b/src/gsolve/gsolve_algorithms.py index 7bffb5f..c1e605c 100644 --- a/src/gsolve/gsolve_algorithms.py +++ b/src/gsolve/gsolve_algorithms.py @@ -50,6 +50,10 @@ def _solver_warning(message: str): ) +def _solver_clip_warning(message: str): + _solver_warning(f"After percentile clipping: {message}") + + def call_gsolve_lstsq( obs: pd.DataFrame, ref_sites: pd.DataFrame, @@ -478,19 +482,16 @@ def _check_post_clip_data_are_ok( obs_loop: np.ndarray[tuple[int]], use_loops: bool, ) -> bool: + """Check if percentile clipping does damage.""" obs_site_id_remain = obs_site_id[mask] obs_loop_remain = obs_loop[mask] errs = 0 # warn about site removal if len(dropped_sites := np.setdiff1d(obs_site_id, obs_site_id_remain)) > 0: - _solver_warning( - f"Sites were completely removed after percentile clipping: {dropped_sites}" - ) + _solver_clip_warning(f"sites were completely removed: {dropped_sites}") errs += 1 - check_loops = use_loops and not (obs_loop[0] == obs_loop).all() - # ensure all tie sites made it dropped_ties = np.intersect1d(ties_site_id, obs_site_id_remain) if len(dropped_ties) == 0: @@ -499,44 +500,55 @@ def _check_post_clip_data_are_ok( f"percentile clipping: {dropped_ties}" ) raise ValueError(msg) + if len(dropped_ties) < len(ties_site_id): - _solver_warning( - f"{len(dropped_ties)} of {len(ties_site_id)} reference sites were completely" - f" removed after percentile clipping: {dropped_ties})" + _solver_clip_warning( + f"{len(dropped_ties)} of {len(ties_site_id)} reference sites " + f"were completely: {dropped_ties})" ) errs += 1 # if using loops and there was actually more than 1 loop # - check that loops were not completely removed - # - check that loops have common stations - # - maybe? check that still have intra loop repeats + # - check that loops have common stations or include a ref site + # if use_loops and have > 1 loops, then check_loops + check_loops = use_loops and not (obs_loop[0] == obs_loop).all() if check_loops: if len(dropped_loops := np.setdiff1d(obs_loop, obs_loop_remain)) > 0: - _solver_warning( - f"Loops completely removed after percentile clipping: {dropped_loops}" - ) + _solver_clip_warning(f"some loops were completely removed: {dropped_loops}") errs += 1 - # is this necessary? + # check if loops are now isolated from rest of survey for loop_id in np.unique(obs_loop_remain): m_pre = obs_loop == loop_id - in_other_loops_pre_clip = np.intersect1d( - obs_site_id[m_pre], obs_site_id[~m_pre] - ) - if len(in_other_loops_pre_clip) == 0: - continue # nothing will have changed - m_post = obs_loop_remain == loop_id - in_other_loops_post_clip = np.intersect1d( + + in_other_loops_pre = np.intersect1d(obs_site_id[m_pre], obs_site_id[~m_pre]) + in_other_loops_pre = len(in_other_loops_pre) > 0 + in_other_loops_post = np.intersect1d( obs_site_id_remain[m_post], obs_site_id_remain[~m_post] ) + in_other_loops_post = len(in_other_loops_post) > 0 - if len(in_other_loops_post_clip) == 0: + has_ref_gravity_pre = np.isin(ties_site_id, obs_site_id[m_pre]).any() + has_ref_gravity_post = np.isin( + ties_site_id, obs_site_id_remain[m_post] + ).any() + + isolated_loop_pre = not in_other_loops_pre and not has_ref_gravity_pre + isolated_loop_post = not in_other_loops_post and not has_ref_gravity_post + + if isolated_loop_pre: + # Already isolated so, nothing will have changed + continue + if isolated_loop_post: _solver_warning( - f"After percentile clipping, loop '{loop_id}' has no sites in common " - "with the rest of survey" + f"loop '{loop_id}' has no sites in common with the rest of survey" ) errs += 1 + if has_ref_gravity_pre and not has_ref_gravity_post: + _solver_warning(f"all reference sites removed from loop '{loop_id}'") + errs += 1 return errs == 0 diff --git a/src/gsolve/gsolve_outputs.py b/src/gsolve/gsolve_outputs.py index 02ef1cd..202412b 100644 --- a/src/gsolve/gsolve_outputs.py +++ b/src/gsolve/gsolve_outputs.py @@ -352,13 +352,14 @@ def calibration_factor(self) -> float: """Convenience property to access the calculated calibration factor.""" return float(np.asarray(self.params.calculated_calibration_factor).item()) - def plot_residual_drift( + def plot_residual_drift( # ruff: ignore[too-many-positional-arguments] self, loop: str | float, plot_drift: bool = True, unit: _PlotGravityUnit = "mGal", filename: FilePath | None = None, show: bool = True, + ax: plt.Axes | None = None, ) -> plt.Axes: """ Plot the residuals and drift curve. @@ -409,12 +410,15 @@ def plot_residual_drift( msg = f"unrecognized unit '{unit}'. Must be 'mGal' or 'uGal'" raise ValueError(msg) + if ax is None: + fig = plt.figure() + ax = fig.add_subplot(111) + elif isinstance(ax, plt.Axes): + fig = ax.get_figure() + x = df[x_col].to_numpy() y = df[y_col].to_numpy() - fig = plt.figure() - ax = fig.add_subplot(111) - if plot_drift: drift_y = drift * x y += drift_y diff --git a/tests/test_gsolve_algorithms.py b/tests/test_gsolve_algorithms.py index a1fc2e3..3bdb290 100644 --- a/tests/test_gsolve_algorithms.py +++ b/tests/test_gsolve_algorithms.py @@ -394,7 +394,7 @@ def test_dropped_site_warns_and_returns_false(self) -> None: # every observation of site "C" is clipped mask = np.array([True, True, False, True, True]) - with pytest.warns(GSolveSolverWarning, match="Sites were completely removed"): + with pytest.warns(GSolveSolverWarning, match="sites were completely removed"): result = _check_post_clip_data_are_ok( mask=mask, obs_site_id=obs_site_id, @@ -449,7 +449,9 @@ def test_dropped_loop_warns_and_returns_false(self) -> None: # loop "L3" is entirely clipped while two loops remain mask = np.array([True, True, True, True, False, False]) - with pytest.warns(GSolveSolverWarning, match="Loops completely removed"): + with pytest.warns( + GSolveSolverWarning, match="some loops were completely removed" + ): result = _check_post_clip_data_are_ok( mask=mask, obs_site_id=obs_site_id, @@ -461,15 +463,45 @@ def test_dropped_loop_warns_and_returns_false(self) -> None: assert result is False def test_loop_loses_common_sites_warns_and_returns_false(self) -> None: - """A surviving loop left with no sites common to the rest warns.""" - # Pre-clip, loops L1 and L2 share sites A and B (network is connected). + """A surviving loop stripped of its reference anchor warns. + + When a connected loop loses its only reference site after clipping + (while still sharing a site with the rest of the survey), the checker + emits the "no sites in common" warning. In the current implementation + this is coupled with the reference-removal warning below. + """ + # L1 = {A, B, C}, L2 = {A, B, D}; the only tie site "A" is in both loops. obs_site_id = np.array(["A", "B", "C", "A", "B", "D"]) obs_loop = np.array(["L1", "L1", "L1", "L2", "L2", "L2"]) - ties_site_id = np.array(["A", "B"]) - # Clip A and B from L2, leaving L2 with only site D -> no common sites. - mask = np.array([True, True, True, False, False, True]) + ties_site_id = np.array(["A"]) + # Clip tie "A" from L2 only; "A" survives globally via L1. + mask = np.array([True, True, True, False, True, True]) - with pytest.warns(GSolveSolverWarning, match="no sites in common"): + with pytest.warns( + GSolveSolverWarning, match="all reference sites removed from loop" + ): + result = _check_post_clip_data_are_ok( + mask=mask, + obs_site_id=obs_site_id, + ties_site_id=ties_site_id, + obs_loop=obs_loop, + use_loops=True, + ) + + assert result is False + + def test_all_reference_sites_removed_from_loop_warns(self) -> None: + """A loop that loses all its reference sites warns and returns False.""" + # L1 = {A, B, C}, L2 = {A, B, D}; the only tie site "A" is in both loops. + obs_site_id = np.array(["A", "B", "C", "A", "B", "D"]) + obs_loop = np.array(["L1", "L1", "L1", "L2", "L2", "L2"]) + ties_site_id = np.array(["A"]) + # Clip tie "A" from L2 only; "A" survives globally via L1. + mask = np.array([True, True, True, False, True, True]) + + with pytest.warns( + GSolveSolverWarning, match="all reference sites removed from loop" + ): result = _check_post_clip_data_are_ok( mask=mask, obs_site_id=obs_site_id, From 17b110cdfa7fbace54f79f6b6b46d30c3045d1e9 Mon Sep 17 00:00:00 2001 From: Adrian Benson Date: Sat, 3 Oct 2026 03:06:54 +1300 Subject: [PATCH 34/36] Add Warner class, add ax option to plot args --- src/gsolve/core/utils.py | 70 +++++++++++++++++++++++++++++++++ src/gsolve/gsolve_algorithms.py | 38 ++++++------------ src/gsolve/gsolve_outputs.py | 53 +++++++++++++++++++------ 3 files changed, 121 insertions(+), 40 deletions(-) diff --git a/src/gsolve/core/utils.py b/src/gsolve/core/utils.py index f7898c5..0536413 100644 --- a/src/gsolve/core/utils.py +++ b/src/gsolve/core/utils.py @@ -22,7 +22,9 @@ import itertools import sys +import warnings from collections.abc import Sequence +from pathlib import Path from typing import Any, Literal, TypeAliasType, get_args, get_origin, overload import numpy as np @@ -1158,3 +1160,71 @@ def convert_single_timestamp_arg( raise TypeError(msg) return t_ + + +class GSolveSimpleWarner: + """Simple class to make python warnings friendlier. + + For cases where the issue is with the user's code or data. + + - Ensures that a warning's trace points to the users code. + - Maintains a count and cache of warnings issued + + + Parameters + ---------- + prefix : str, optional + Prepend prefix to all messages. + default_category : Warning, default is UserWarning + The category to assign warnings. + """ + + prefix: str | None + default_category: Warning | None + _count: int + skip_file_prefixes: tuple[str, ...] + _messages: list[str] + + def __init__( + self, prefix: str | None = None, default_category: Warning = UserWarning + ): + self.prefix = prefix + self._count = 0 + self.default_category = default_category + self.skip_file_prefixes = (str(Path(__file__).parents[1]),) + self._messages = [] + + def warn( + self, message: str, show_prefix: bool = True, category: Warning | None = None + ): + """ + Issue a python warning for user errors. + + Parameters + ---------- + message : str + The warning message to issue. + show_prefix : bool, default True + Prepend message with the ``default_prefix`` set at initialisation. + category : Warning | None, optional + If specified, use ``category`` for the warning category, otherwise use + the category specified at initialisation (default=UserWarning). + """ + if not self.prefix is None and show_prefix: + msg = f"{self.prefix} {message}" + else: + msg = f"{message}" + + if category is None: + category = self.default_category + + warnings.warn( + message=msg, category=category, skip_file_prefixes=self.skip_file_prefixes + ) + self._count += 1 + self._messages.append([msg, category]) + + @property + def count(self) -> int: + """The number of warnings issued by this object.""" + return self._count diff --git a/src/gsolve/gsolve_algorithms.py b/src/gsolve/gsolve_algorithms.py index c1e605c..547b133 100644 --- a/src/gsolve/gsolve_algorithms.py +++ b/src/gsolve/gsolve_algorithms.py @@ -17,19 +17,17 @@ # Copyright (c) 2025 Earth Sciences New Zealand. """Functions and Classes for performing network adjustment of gravity data.""" -import pathlib -import warnings from typing import Any import numpy as np import pandas as pd from gsolve.core._typing import GSolveSolverMethod, GSolveSolverReturn +from gsolve.core.utils import GSolveSimpleWarner from gsolve.gsolve_outputs import GSolveResults __all__ = ["GSolveSolverMethod", "call_gsolve_calibration", "call_gsolve_lstsq"] -# make warnings show caller location _GSOLVE_SOLVER_METHODS: dict[int, str] = { 1: "Unconstrained least squares", @@ -42,18 +40,6 @@ class GSolveSolverWarning(UserWarning): """Raised when gsolve may produce unreliable results.""" -def _solver_warning(message: str): - warnings.warn( - message, - category=GSolveSolverWarning, - skip_file_prefixes=(str(pathlib.Path(__file__).parent),), - ) - - -def _solver_clip_warning(message: str): - _solver_warning(f"After percentile clipping: {message}") - - def call_gsolve_lstsq( obs: pd.DataFrame, ref_sites: pd.DataFrame, @@ -483,14 +469,15 @@ def _check_post_clip_data_are_ok( use_loops: bool, ) -> bool: """Check if percentile clipping does damage.""" + warner = GSolveSimpleWarner( + prefix="After percentile clipping:", default_category=GSolveSolverWarning + ) obs_site_id_remain = obs_site_id[mask] obs_loop_remain = obs_loop[mask] - errs = 0 # warn about site removal if len(dropped_sites := np.setdiff1d(obs_site_id, obs_site_id_remain)) > 0: - _solver_clip_warning(f"sites were completely removed: {dropped_sites}") - errs += 1 + warner.warn(f"sites were completely removed: {dropped_sites}") # ensure all tie sites made it dropped_ties = np.intersect1d(ties_site_id, obs_site_id_remain) @@ -502,11 +489,10 @@ def _check_post_clip_data_are_ok( raise ValueError(msg) if len(dropped_ties) < len(ties_site_id): - _solver_clip_warning( + warner.warn( f"{len(dropped_ties)} of {len(ties_site_id)} reference sites " f"were completely: {dropped_ties})" ) - errs += 1 # if using loops and there was actually more than 1 loop # - check that loops were not completely removed @@ -516,8 +502,7 @@ def _check_post_clip_data_are_ok( check_loops = use_loops and not (obs_loop[0] == obs_loop).all() if check_loops: if len(dropped_loops := np.setdiff1d(obs_loop, obs_loop_remain)) > 0: - _solver_clip_warning(f"some loops were completely removed: {dropped_loops}") - errs += 1 + warner.warn(f"some loops were completely removed: {dropped_loops}") # check if loops are now isolated from rest of survey for loop_id in np.unique(obs_loop_remain): @@ -543,12 +528,11 @@ def _check_post_clip_data_are_ok( # Already isolated so, nothing will have changed continue if isolated_loop_post: - _solver_warning( + warner.warn( f"loop '{loop_id}' has no sites in common with the rest of survey" ) - errs += 1 + if has_ref_gravity_pre and not has_ref_gravity_post: - _solver_warning(f"all reference sites removed from loop '{loop_id}'") - errs += 1 + warner.warn(f"all reference sites removed from loop '{loop_id}'") - return errs == 0 + return warner.count == 0 diff --git a/src/gsolve/gsolve_outputs.py b/src/gsolve/gsolve_outputs.py index 202412b..31276ca 100644 --- a/src/gsolve/gsolve_outputs.py +++ b/src/gsolve/gsolve_outputs.py @@ -233,6 +233,7 @@ def plot_residual_cdf( unit: _PlotGravityUnit = "mGal", filename: FilePath | None = None, show: bool = True, + ax: plt.Axes | None = None, ) -> plt.Axes: """ Plot the empirical cumulative density function of residuals. @@ -250,6 +251,8 @@ def plot_residual_cdf( to the end of the filename (before suffix). show: bool, default True Show the plot in a new window. + ax : matplotlib.axes.Axes, optional + Plot data to ``ax`` if specified, otherwise instantiate a new Axes object. Returns ------- @@ -257,6 +260,15 @@ def plot_residual_cdf( The plot axes instance. """ + if ax is None: + fig = plt.figure() + ax = fig.add_subplot(111) + elif isinstance(ax, plt.Axes): + fig = ax.get_figure() + else: + msg = f"invalid type for ax: {type(ax).__name__}" + raise TypeError(msg) + m = self.obs_solution.active.eq(True) & self.obs_solution.residual.notna() df = self.obs_solution.loc[m].copy() @@ -296,8 +308,6 @@ def plot_residual_cdf( raise ValueError(msg) df = df.loc[df["loop"].isin(loops)] - fig = plt.figure() - ax = fig.add_subplot(111) for l, loop_df in df.groupby("loop"): residuals = loop_df["residual"].to_numpy() @@ -360,6 +370,7 @@ def plot_residual_drift( # ruff: ignore[too-many-positional-arguments] filename: FilePath | None = None, show: bool = True, ax: plt.Axes | None = None, + **kwargs, ) -> plt.Axes: """ Plot the residuals and drift curve. @@ -371,20 +382,36 @@ def plot_residual_drift( # ruff: ignore[too-many-positional-arguments] plot_drift : bool, default True If True, plot the drift curve along with the residuals + drift. If False, plot only the residuals as stem plot. - unit: {'uGal', 'mGal'}, default 'mGal' + unit : {'uGal', 'mGal'}, default 'mGal' If 'uGal', plot residuals in microGal's. If 'mGal', plot residuals in milliGal's. - filename: str, default None + filename : str, default None If not None, save plot to ``filename``. The specified loop id is appended to the end of the filename (before suffix). - show: bool, default True + show : bool, default True Show the plot in a new window. + ax : matplotlib.axes.Axes, optional + Plot data to ``ax`` if specified, otherwise instantiate a new Axes object. + **kwargs : + Options to pass to the underlying matplotlib method. + + - ``plt.scatter`` if ``plot_drift=True`` + - ``plt.stem`` otherwise Returns ------- matplotlib.axes.Axes The plot axes instance. """ + if ax is None: + fig = plt.figure() + ax = fig.add_subplot(111) + elif isinstance(ax, plt.Axes): + fig = ax.get_figure() + else: + msg = f"invalid type for ax: {type(ax).__name__}" + raise TypeError(msg) + loop = str(loop) x_col: str = "timedelta" y_col: str = "residual" @@ -410,24 +437,22 @@ def plot_residual_drift( # ruff: ignore[too-many-positional-arguments] msg = f"unrecognized unit '{unit}'. Must be 'mGal' or 'uGal'" raise ValueError(msg) - if ax is None: - fig = plt.figure() - ax = fig.add_subplot(111) - elif isinstance(ax, plt.Axes): - fig = ax.get_figure() - x = df[x_col].to_numpy() y = df[y_col].to_numpy() if plot_drift: drift_y = drift * x y += drift_y - ax.scatter(x, y, marker=".", label="residuals") + _ = kwargs.setdefault("label", "residuals") + _ = kwargs.setdefault("marker", ".") + + ax.scatter(x, y, **kwargs) ax.plot( x, drift_y, c="orange", label=f"drift curve ({drift:{precision}} {unit_label}/hr)", + **kwargs, ) ax.set_ylabel(f"{y_col} + drift ({unit_label})") ax.set_title( @@ -436,7 +461,9 @@ def plot_residual_drift( # ruff: ignore[too-many-positional-arguments] f"Percentile clipping = {self.params.percentile_clipping:.1f}" ) else: - ax.stem(x, y, label="residuals", markerfmt=".") + _ = kwargs.setdefault("label", "residuals") + _ = kwargs.setdefault("markerfmt", ".") + ax.stem(x, y, **kwargs) ax.set_ylabel(f"{y_col} ({unit_label})") ax.legend(loc="best") From c536f0bd5557b869c8151badb551e7d6306b92b2 Mon Sep 17 00:00:00 2001 From: Adrian Benson Date: Tue, 6 Oct 2026 11:10:11 +1300 Subject: [PATCH 35/36] Use Warner class everywhere --- src/gsolve/core/data.py | 15 +++++----- src/gsolve/core/utils.py | 7 +++-- src/gsolve/gsolve_algorithms.py | 4 +-- src/gsolve/observations.py | 23 +++++++++------ src/gsolve/reductions/terrain_corrections.py | 30 +++++++++----------- src/gsolve/scintrex.py | 9 +++--- src/gsolve/tide/earth_tide.py | 3 +- src/gsolve/tide/ocean_load.py | 11 ++++--- 8 files changed, 54 insertions(+), 48 deletions(-) diff --git a/src/gsolve/core/data.py b/src/gsolve/core/data.py index 435332c..f3f04f3 100644 --- a/src/gsolve/core/data.py +++ b/src/gsolve/core/data.py @@ -21,7 +21,6 @@ import abc import copy import dataclasses -import warnings from collections.abc import Callable from copy import deepcopy from types import MappingProxyType @@ -42,6 +41,7 @@ write_excel_worksheet, ) from gsolve.core.utils import ( + GSolveUserWarning, merge_datetime_columns, normalize_field_names, prepare_writable_df, @@ -505,18 +505,19 @@ def set_column( def _data_ok(self, warn: bool = True) -> bool: """Test whether data are complete according to specifications in ``obj._known_fields``.""" - rval = True + all_ok = True + warner = GSolveUserWarning() for f in self.required_fields(): if f not in self.data.columns: if warn: - warnings.warn(f"Missing required field: '{f}'") - rval = False + warner.warn(f"Missing required field: '{f}'") + all_ok = False if self.data[f].isna().any() or self.data[f].eq("").any(): if warn: - warnings.warn(f"Required field has empty records: '{f}'") - rval = False + warner.warn(f"Required field has empty records: '{f}'") + all_ok = False - return rval + return all_ok @classmethod def from_dataframe( diff --git a/src/gsolve/core/utils.py b/src/gsolve/core/utils.py index 0536413..48ee0df 100644 --- a/src/gsolve/core/utils.py +++ b/src/gsolve/core/utils.py @@ -51,6 +51,7 @@ __all__ = [ "DEFAULT_TIMESTAMP_COLUMNS", "GSolveDataWarning", + "GSolveUserWarning", "check_duplicate_index", "columns_to_timestamp", "expand_datetime_column", @@ -928,12 +929,12 @@ def _display(self, msg: str) -> None: def print_msgs(self) -> None: """Print all stored warning messages.""" for msg in self.messages: - print(f"{self.prefix}: {msg}") # ruff: ignore[print] + print(f"{self.prefix}: {msg}", file=sys.stderr) # ruff: ignore[print] def final_msg(self) -> None: """Print closing summary message.""" if self.count > 0: - self._display(f"{self.count} problem(s) encountered") + self._display(f"{self.count} problem(s) encountered", file=sys.stderr) def generate_loop_intervals( @@ -1162,7 +1163,7 @@ def convert_single_timestamp_arg( return t_ -class GSolveSimpleWarner: +class GSolveUserWarning: """Simple class to make python warnings friendlier. For cases where the issue is with the user's code or data. diff --git a/src/gsolve/gsolve_algorithms.py b/src/gsolve/gsolve_algorithms.py index 547b133..25efbfc 100644 --- a/src/gsolve/gsolve_algorithms.py +++ b/src/gsolve/gsolve_algorithms.py @@ -23,7 +23,7 @@ import pandas as pd from gsolve.core._typing import GSolveSolverMethod, GSolveSolverReturn -from gsolve.core.utils import GSolveSimpleWarner +from gsolve.core.utils import GSolveUserWarning from gsolve.gsolve_outputs import GSolveResults __all__ = ["GSolveSolverMethod", "call_gsolve_calibration", "call_gsolve_lstsq"] @@ -469,7 +469,7 @@ def _check_post_clip_data_are_ok( use_loops: bool, ) -> bool: """Check if percentile clipping does damage.""" - warner = GSolveSimpleWarner( + warner = GSolveUserWarning( prefix="After percentile clipping:", default_category=GSolveSolverWarning ) obs_site_id_remain = obs_site_id[mask] diff --git a/src/gsolve/observations.py b/src/gsolve/observations.py index 67e700b..44910cb 100644 --- a/src/gsolve/observations.py +++ b/src/gsolve/observations.py @@ -49,6 +49,7 @@ from gsolve.core.excel_io import write_excel_worksheet from gsolve.core.utils import ( GSolveDataWarning, + GSolveUserWarning, is_list_like, prepare_writable_df, to_naive_utc_datetime, @@ -347,10 +348,11 @@ def __init__( self.data.loc[:, ["meter_reading", "meter_reading_mgal"]].isna().all(axis=1) ) if has_nan_readings.any(): - _warnings.warn( + GSolveUserWarning().warn( "neither 'meter_reading' or 'meter_reading_mgal' present for " "1 or more records" ) + _ = self._data_ok(warn=True) self.set_timedelta_unit(timedelta_unit, set_tdelta=False) self.set_fixed_time_datum(fixed_time_datum, set_tdelta=False) @@ -481,10 +483,12 @@ def set_obs_id( if duplicated_obs_id == "error": msg_0 = f"{msg}" raise ValueError(msg_0) + + warner = GSolveUserWarning() if duplicated_obs_id == "keep": - _warnings.warn(f"keeping {msg}") + warner.warn(f"keeping {msg}") elif duplicated_obs_id == "rename": - _warnings.warn(f"renaming {msg}") + warner.warn(f"renaming {msg}") new_idx = self._index_deduplicator(new_idx) self.data = self.data.set_index(new_idx) @@ -1530,6 +1534,7 @@ def merge( The new GravityObservations object. """ + warner = GSolveUserWarning() if not isinstance(other, type(self)): msg = ( f"invalid type for other: " @@ -1569,15 +1574,15 @@ def merge( raise ValueError(msg) if if_duplicate_loops == "keep": - _warnings.warn(f"keeping {msg}") + warner.warn(f"keeping {msg}") elif if_duplicate_loops == "drop": - _warnings.warn(f"dropping {msg}") + warner.warn(f"dropping {msg}") m = ~other.data["loop"].isin(duplicated_loops) other.data = other.data.loc[m, :] elif if_duplicate_loops == "rename": - _warnings.warn(f"{msg}: adding suffix '{rename_suffix}' to loop id's") + warner.warn(f"{msg}: adding suffix '{rename_suffix}' to loop id's") for l in duplicated_loops: m = other.data["loop"].eq(l) other.data.loc[m, "loop"] += f"_{rename_suffix}" @@ -1592,11 +1597,11 @@ def merge( raise ValueError(msg) if if_duplicate_obs_ids == "drop": - _warnings.warn(f"dropping {msg}") + warner.warn(f"dropping {msg}") other.data = other.data.loc[~is_duplicated_obs_id, :] elif if_duplicate_obs_ids == "rename": - _warnings.warn(f"adding suffix '{rename_suffix}' to {msg}") + warner.warn(f"adding suffix '{rename_suffix}' to {msg}") rename_dict = { i: f"{i}_{rename_suffix}" for i in other.data.index[is_duplicated_obs_id] @@ -1604,7 +1609,7 @@ def merge( other.data = other.data.rename(index=rename_dict) if if_duplicate_obs_ids == "regenerate": - _warnings.warn(f"{msg}: will regenerate 'obs_id for all data") + warner.warn(f"{msg}: will regenerate 'obs_id for all data") # defer regeneration until after concat regen_obs_ids = True diff --git a/src/gsolve/reductions/terrain_corrections.py b/src/gsolve/reductions/terrain_corrections.py index e748bb5..90ca672 100644 --- a/src/gsolve/reductions/terrain_corrections.py +++ b/src/gsolve/reductions/terrain_corrections.py @@ -45,6 +45,7 @@ from gsolve.core.data import DataFieldSpecification, GSolveParameters, GSolveTable from gsolve.core.excel_io import read_excel_worksheet, write_excel_worksheet from gsolve.core.utils import ( + GSolveUserWarning, is_filepath_like, is_in_literal, is_list_like, @@ -548,11 +549,7 @@ def _sanity_check(self, if_errors: Literal["warn", "error"] = "error") -> None: an exception. If ``'warn'``, issue a warning and continue checking. """ throw_error: bool = if_errors == "error" - # error_count: int = 0 - - def warn_(m: str) -> None: - warnings.warn(m, category=UserWarning) - # error_count += 1 + warner = GSolveUserWarning() if ( np.isnan(self.min_dist) @@ -567,7 +564,7 @@ def warn_(m: str) -> None: ) if throw_error: raise ValueError(msg) - warn_(msg) + warner.warn(msg) # check distance msk type is valid if not is_in_literal(self.distance_mask_type, TCorrDistanceMaskType): @@ -577,7 +574,7 @@ def warn_(m: str) -> None: ) if throw_error: raise ValueError(msg) - warn_(msg) + warner.warn(msg) # check dem_source if not _is_dataarray(self.dem_source) and not is_filepath_like(self.dem_source): @@ -587,21 +584,21 @@ def warn_(m: str) -> None: ) if throw_error: raise ValueError(msg) - warn_(msg) + warner.warn(msg) if _is_dataarray(self.dem_source): if not self.dem_source.tcorr.is_valid_dem: msg = "invalid dem_source DataArray: must be a 2D array of floats" if throw_error: raise ValueError(msg) - warn_(msg) + warner.warn(msg) elif is_filepath_like(self.dem_source): if not self.dem_source: msg = "invalid dem_source file-path like" if throw_error: raise ValueError(msg) - warn_(msg) + warner.warn(msg) else: msg = ( f"invalid dem_source: must be an xarray.DataArray or file-path like" @@ -609,7 +606,7 @@ def warn_(m: str) -> None: ) if throw_error: raise ValueError(msg) - warn_(msg) + warner.warn(msg) # check density_dataset_source if _is_dataarray(self.density_dataset_source): @@ -620,7 +617,7 @@ def warn_(m: str) -> None: ) if throw_error: raise ValueError(msg) - warn_(msg) + warner.warn(msg) elif is_filepath_like(self.density_dataset_source): if not self.density_dataset_source: @@ -928,7 +925,7 @@ def compute( ) else: x, y = None, None - warnings.warn( + GSolveUserWarning().warn( "TerrainCorrector parameters specify variable easting " "and/or northing fields. Easting and Northing will not be " "written to TerrainCorrectionData output." @@ -1746,17 +1743,18 @@ def get_corrections( err_msg = ( f"no terrain correction data found for {len(site_id_missing)} of " - f"{len(site_id_idx)} site_id's " + f"{len(site_id_idx)} site_id's" ) + warner = GSolveUserWarning(prefix=err_msg + ", ") if if_missing == "raise": raise ValueError(err_msg) if if_missing == "drop": - warnings.warn(f"{err_msg}, dropping from output") + warnings.warn("dropping from output") return tcorrs.loc[site_id_found, cols] - warnings.warn(f"{err_msg}, filling with {fill_value}") + warner.warn(f"filling with {fill_value}") rval = tcorrs.loc[site_id_found, cols].copy() rval_fill = pd.DataFrame(index=site_id_missing, columns=cols, data=fill_value) return pd.concat([rval, rval_fill]).loc[site_id_idx] diff --git a/src/gsolve/scintrex.py b/src/gsolve/scintrex.py index a13fa78..e613da8 100644 --- a/src/gsolve/scintrex.py +++ b/src/gsolve/scintrex.py @@ -19,7 +19,6 @@ import abc import copy import pathlib -import warnings from collections.abc import Callable, Mapping, Sequence from types import MappingProxyType from typing import Literal, Self @@ -35,6 +34,7 @@ TimedeltaScalar, ) from gsolve.core.utils import ( + GSolveUserWarning, generate_loop_intervals, generate_loop_names, is_datetime_array, @@ -305,8 +305,9 @@ def _set_data(self, data: pd.DataFrame, on_error: str = "raise") -> None: raise TypeError(msg) from err_cant_replace else: if on_error == "warn": - msg = f"bad data encountered in column '{c}', setting to nan" - warnings.warn(msg) + GSolveUserWarning().warn( + f"bad data encountered in column '{c}', setting to nan" + ) if ( "datetime" not in df.columns @@ -412,7 +413,7 @@ def from_file( metadata_units[k] = u if not file_id_found: - warnings.warn( + GSolveUserWarning().warn( f"Expected file type identifier '{cls._file_id_header}'" f"not found in file {cg6_file}." ) diff --git a/src/gsolve/tide/earth_tide.py b/src/gsolve/tide/earth_tide.py index 2b588d9..656f731 100644 --- a/src/gsolve/tide/earth_tide.py +++ b/src/gsolve/tide/earth_tide.py @@ -40,6 +40,7 @@ ) from gsolve.core.utils import ( GSolveDataWarning, + GSolveUserWarning, convert_single_timestamp_arg, dms2rad, to_1d_ndarray, @@ -755,7 +756,7 @@ def sort_and_validate(self, gap_threshold: float | None = None) -> None: .to_numpy() ) if gaps.any(): - warnings.warn( + GSolveUserWarning().warn( message=( f"some frequency {gaps.sum()} intervals separated by" f"greater than {gap_threshold} " diff --git a/src/gsolve/tide/ocean_load.py b/src/gsolve/tide/ocean_load.py index 6b0d072..b22d90d 100644 --- a/src/gsolve/tide/ocean_load.py +++ b/src/gsolve/tide/ocean_load.py @@ -18,7 +18,6 @@ """Methods and classes for reading and applying ocean load corrections to gravity data.""" -import warnings from pathlib import Path from typing import Any, Literal, Protocol, runtime_checkable @@ -34,7 +33,7 @@ FloatArray, SiteIDArray, ) -from gsolve.core.utils import to_1d_ndarray, to_naive_utc_datetime +from gsolve.core.utils import GSolveUserWarning, to_1d_ndarray, to_naive_utc_datetime def _read_csv_with_fallback(file_path: FilePath, **kwargs) -> pd.DataFrame: @@ -213,7 +212,7 @@ def ocean_load_correction( ) if if_not_matched == "error": raise ValueError(msg) - warnings.warn(msg, UserWarning) + GSolveUserWarning().warn(msg) if any(present_mask): rval.loc[present_mask] = self.data.loc[ @@ -359,7 +358,7 @@ class since it provides corrections for a single station. f"({self.starttime} <-> {self.endtime})" ) if if_not_matched == "warn": - warnings.warn(msg, UserWarning) + GSolveUserWarning().warn(msg) else: raise ValueError(msg) @@ -418,7 +417,7 @@ def _validate_timeseries_data(df: pd.DataFrame) -> None: # warn if non-uniform sampling interval/rate sample_intervals = (df.index[1:] - df.index[:-1]).total_seconds() if not np.allclose(sample_intervals[1:], sample_intervals[1]): - warnings.warn("timeseries has non-uniform sampling interval/rate.", UserWarning) + GSolveUserWarning().warn("timeseries has non-uniform sampling interval/rate.") def qtp_to_corrector( @@ -766,7 +765,7 @@ def ocean_load_correction( ) if if_not_matched == "error": raise ValueError(msg) - warnings.warn(msg, UserWarning) + GSolveUserWarning().warn(msg) uniq_site_id = [s for s in uniq_site_id if s not in bad_site_ids] # set up the computers for each station From 41ecfdbe34a899d8abd9ac73dc574a3247bea1a0 Mon Sep 17 00:00:00 2001 From: Adrian Benson Date: Tue, 6 Oct 2026 11:19:33 +1300 Subject: [PATCH 36/36] bug: rm file- form _display() --- src/gsolve/core/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gsolve/core/utils.py b/src/gsolve/core/utils.py index 48ee0df..2e19823 100644 --- a/src/gsolve/core/utils.py +++ b/src/gsolve/core/utils.py @@ -934,7 +934,7 @@ def print_msgs(self) -> None: def final_msg(self) -> None: """Print closing summary message.""" if self.count > 0: - self._display(f"{self.count} problem(s) encountered", file=sys.stderr) + self._display(f"{self.count} problem(s) encountered") def generate_loop_intervals(