gagely 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
gagely/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ try:
2
+ from ._version import version as __version__
3
+ except ImportError:
4
+ try:
5
+ from importlib.metadata import version
6
+
7
+ __version__ = version("gagely")
8
+ except Exception:
9
+ __version__ = "0.0.0"
10
+
11
+ __all__ = ["__version__"]
gagely/_dss_utils.py ADDED
@@ -0,0 +1,106 @@
1
+ """Shared DSS interval-inference and gap-fill helpers.
2
+
3
+ Used by both ``gagely.usgs`` and ``gagely.contrail`` so that interval
4
+ detection lives in one place.
5
+ """
6
+
7
+ import logging
8
+ from datetime import datetime
9
+ from typing import List, Optional, Tuple
10
+
11
+ import pandas as pd
12
+ from pydsstools.core import DssPathName, UNDEFINED
13
+
14
+ __all__: List[str] = []
15
+
16
+
17
+ def _infer_interval_seconds(times: pd.Series) -> Optional[int]:
18
+ """Return the modal sampling interval in seconds across all of *times*.
19
+
20
+ Computes consecutive diffs over the full sorted series and returns the
21
+ most common diff in seconds. Logs a warning if more than one unique
22
+ diff is found.
23
+
24
+ Returns ``None`` if fewer than 2 samples are available.
25
+ """
26
+ if len(times) < 2:
27
+ return None
28
+ diffs = times.sort_values().diff().dropna()
29
+ if diffs.empty:
30
+ return None
31
+ counts = diffs.value_counts()
32
+ modal_diff = counts.index[0]
33
+ modal_seconds = int(modal_diff.total_seconds())
34
+ if len(counts) > 1:
35
+ anomalies = [
36
+ (int(d.total_seconds()), int(c))
37
+ for d, c in counts.iloc[1:4].items()
38
+ ]
39
+ logging.warning(
40
+ "Non-uniform intervals: modal=%ds (%d/%d rows); top anomalies %s",
41
+ modal_seconds, int(counts.iloc[0]), int(counts.sum()), anomalies,
42
+ )
43
+ return modal_seconds
44
+
45
+
46
+ def _infer_interval_epart(times: pd.Series) -> Optional[str]:
47
+ """Return the DSS E-part string for the modal interval in *times*, or ``None``."""
48
+ seconds = _infer_interval_seconds(times)
49
+ if seconds is None:
50
+ return None
51
+ return DssPathName.interval_to_epart(seconds)
52
+
53
+
54
+ _FILL_PROFILES = ("undefined", "ffill", "bfill", "interpolate")
55
+
56
+
57
+ def _densify_timeseries(
58
+ times: List[datetime],
59
+ values: List[float],
60
+ interval_seconds: int,
61
+ profile: str = "undefined",
62
+ ) -> Tuple[List[datetime], List[float]]:
63
+ """Reindex a (times, values) pair onto a regular *interval_seconds* grid.
64
+
65
+ Gap slots are filled according to *profile*:
66
+
67
+ - ``"undefined"`` (default): stamp ``UNDEFINED`` (-9999.0).
68
+ - ``"ffill"``: forward-fill from the last known value; leading gaps → UNDEFINED.
69
+ - ``"bfill"``: backward-fill from the next known value; trailing gaps → UNDEFINED.
70
+ - ``"interpolate"``: linear interpolation between bounding values; edge gaps → UNDEFINED.
71
+
72
+ The grid is bounded by ``min(times)`` and ``max(times)``; the caller's
73
+ requested window is not padded.
74
+
75
+ Returns a new ``(times, values)`` pair sorted by time.
76
+ """
77
+ if profile not in _FILL_PROFILES:
78
+ raise ValueError(f"profile must be one of {_FILL_PROFILES!r}, got {profile!r}")
79
+
80
+ s = pd.Series(values, index=pd.to_datetime(times), name="value").sort_index()
81
+ s = s[~s.index.duplicated(keep="first")]
82
+
83
+ diffs = s.index.to_series().diff().dropna()
84
+ if (diffs == pd.Timedelta(seconds=interval_seconds)).all():
85
+ return s.index.to_pydatetime().tolist(), s.tolist()
86
+
87
+ target = pd.date_range(
88
+ start=s.index.min(), end=s.index.max(), freq=f"{interval_seconds}s"
89
+ )
90
+
91
+ if profile == "undefined":
92
+ densified = s.reindex(target, fill_value=UNDEFINED)
93
+ elif profile == "ffill":
94
+ densified = s.reindex(target).ffill().fillna(UNDEFINED)
95
+ elif profile == "bfill":
96
+ densified = s.reindex(target).bfill().fillna(UNDEFINED)
97
+ else: # interpolate
98
+ densified = s.reindex(target).interpolate(method="time").fillna(UNDEFINED)
99
+
100
+ n_filled = len(densified) - len(s)
101
+ if n_filled > 0:
102
+ logging.debug(
103
+ "_densify_timeseries: filled %d / %d slots (profile=%r)",
104
+ n_filled, len(densified), profile,
105
+ )
106
+ return densified.index.to_pydatetime().tolist(), densified.tolist()
gagely/_version.py ADDED
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.1.0'
22
+ __version_tuple__ = version_tuple = (0, 1, 0)
23
+
24
+ __commit_id__ = commit_id = None