core-lens 0.1.dev136__py3-none-any.whl → 0.1.dev140__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.
- core_lens/_version.py +2 -2
- core_lens/aoi.py +17 -0
- core_lens/base/entity.py +18 -3
- core_lens/base/namespaces/plot.py +12 -5
- core_lens/base/namespaces/stats.py +15 -1
- core_lens/base/result.py +9 -0
- core_lens/base/view.py +8 -0
- core_lens/entities/mws.py +25 -10
- core_lens/entities/tehsil.py +20 -8
- core_lens/entities/waterbody.py +20 -8
- core_lens/export/formats.py +5 -0
- core_lens/schema/detection.py +10 -0
- core_lens/schema/profile.py +2 -0
- core_lens/utils/paths.py +4 -0
- core_lens/utils/polars_utils.py +3 -0
- core_lens/utils/season.py +7 -0
- core_lens/utils/spatial.py +25 -4
- {core_lens-0.1.dev136.dist-info → core_lens-0.1.dev140.dist-info}/METADATA +1 -1
- core_lens-0.1.dev140.dist-info/RECORD +30 -0
- core_lens-0.1.dev136.dist-info/RECORD +0 -30
- {core_lens-0.1.dev136.dist-info → core_lens-0.1.dev140.dist-info}/WHEEL +0 -0
- {core_lens-0.1.dev136.dist-info → core_lens-0.1.dev140.dist-info}/licenses/LICENSE +0 -0
core_lens/_version.py
CHANGED
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.1.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 1, '
|
|
21
|
+
__version__ = version = '0.1.dev140'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 'dev140')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
core_lens/aoi.py
CHANGED
|
@@ -47,6 +47,7 @@ class SeasonConfig:
|
|
|
47
47
|
kharif: Kharif (monsoon) season range as ``(start_MM-DD, end_MM-DD)``.
|
|
48
48
|
rabi: Rabi (winter) season range.
|
|
49
49
|
zaid: Zaid (summer) season range.
|
|
50
|
+
|
|
50
51
|
"""
|
|
51
52
|
|
|
52
53
|
kharif: tuple[str, str] = ("07-01", "10-31")
|
|
@@ -76,6 +77,7 @@ class SeasonConfig:
|
|
|
76
77
|
|
|
77
78
|
Returns:
|
|
78
79
|
str: ``"kharif"``, ``"rabi"``, or ``"zaid"``.
|
|
80
|
+
|
|
79
81
|
"""
|
|
80
82
|
md = f"{d.month:02d}-{d.day:02d}"
|
|
81
83
|
for name in ("kharif", "rabi", "zaid"):
|
|
@@ -115,6 +117,7 @@ def _default_season_config() -> SeasonConfig:
|
|
|
115
117
|
|
|
116
118
|
Returns:
|
|
117
119
|
SeasonConfig: The shared default instance.
|
|
120
|
+
|
|
118
121
|
"""
|
|
119
122
|
global _DEFAULT_SEASON_CONFIG
|
|
120
123
|
if _DEFAULT_SEASON_CONFIG is None:
|
|
@@ -167,6 +170,7 @@ def _cached_resolve_boundary(
|
|
|
167
170
|
|
|
168
171
|
Returns:
|
|
169
172
|
tuple: ``(geometry_wkb, entity_name, key_rows)``.
|
|
173
|
+
|
|
170
174
|
"""
|
|
171
175
|
from core_lens.base.entity import _entity_name as _ename
|
|
172
176
|
|
|
@@ -284,6 +288,7 @@ class AoI:
|
|
|
284
288
|
data_root: Resolved path to the data directory.
|
|
285
289
|
geometry: Shapely polygon representing the AoI boundary.
|
|
286
290
|
seasons: :class:`SeasonConfig` in effect for this AoI.
|
|
291
|
+
|
|
287
292
|
"""
|
|
288
293
|
|
|
289
294
|
def __init__(
|
|
@@ -331,6 +336,7 @@ class AoI:
|
|
|
331
336
|
one boundary mode is used simultaneously.
|
|
332
337
|
:class:`~core_lens.base.EntityValidationError`: If a boundary entity referenced in
|
|
333
338
|
``entity_kwargs`` is not registered.
|
|
339
|
+
|
|
334
340
|
"""
|
|
335
341
|
# Preserve cloud URIs as-is; resolve local paths to absolute.
|
|
336
342
|
data_root_str = str(data_root)
|
|
@@ -395,6 +401,7 @@ class AoI:
|
|
|
395
401
|
|
|
396
402
|
Returns:
|
|
397
403
|
str: ``"kharif"``, ``"rabi"``, or ``"zaid"``.
|
|
404
|
+
|
|
398
405
|
"""
|
|
399
406
|
return self.seasons.season_for(date.today())
|
|
400
407
|
|
|
@@ -404,6 +411,7 @@ class AoI:
|
|
|
404
411
|
|
|
405
412
|
Returns:
|
|
406
413
|
int: Current year as an integer.
|
|
414
|
+
|
|
407
415
|
"""
|
|
408
416
|
return date.today().year
|
|
409
417
|
|
|
@@ -416,6 +424,7 @@ class AoI:
|
|
|
416
424
|
|
|
417
425
|
Returns:
|
|
418
426
|
Any: A Lonboard Map object.
|
|
427
|
+
|
|
419
428
|
"""
|
|
420
429
|
import lonboard
|
|
421
430
|
import geopandas as gpd
|
|
@@ -476,6 +485,7 @@ class AoI:
|
|
|
476
485
|
Raises:
|
|
477
486
|
KeyError: If *name* is not in :data:`_REGISTRY`.
|
|
478
487
|
:class:`~core_lens.base.EntityValidationError`: If validation fails.
|
|
488
|
+
|
|
479
489
|
"""
|
|
480
490
|
if name not in self._entity_instances:
|
|
481
491
|
entity_cls = _REGISTRY[name]
|
|
@@ -506,6 +516,7 @@ class AoI:
|
|
|
506
516
|
Raises:
|
|
507
517
|
:class:`~core_lens.base.EntityValidationError`: If any registered
|
|
508
518
|
entity fails validation.
|
|
519
|
+
|
|
509
520
|
"""
|
|
510
521
|
for name in _REGISTRY:
|
|
511
522
|
self._get_entity(name)
|
|
@@ -562,6 +573,7 @@ class AoI:
|
|
|
562
573
|
Raises:
|
|
563
574
|
:class:`~core_lens.base.EntityValidationError`: If no registered entity can satisfy the filters.
|
|
564
575
|
ValueError: If the filters match zero rows.
|
|
576
|
+
|
|
565
577
|
"""
|
|
566
578
|
logger.debug("Resolving named boundary using kwargs: {}", entity_kwargs)
|
|
567
579
|
|
|
@@ -625,6 +637,7 @@ class AoI:
|
|
|
625
637
|
Raises:
|
|
626
638
|
:class:`~core_lens.base.EntityValidationError`: If any validation check
|
|
627
639
|
fails (absolute-path entities only at register time).
|
|
640
|
+
|
|
628
641
|
"""
|
|
629
642
|
import pathlib as _pathlib
|
|
630
643
|
|
|
@@ -647,6 +660,7 @@ class AoI:
|
|
|
647
660
|
|
|
648
661
|
Args:
|
|
649
662
|
entity_cls (type[BaseEntity]): The entity class to remove.
|
|
663
|
+
|
|
650
664
|
"""
|
|
651
665
|
name = _entity_name(entity_cls)
|
|
652
666
|
logger.info("Deregistering entity class: {}", name)
|
|
@@ -658,6 +672,7 @@ class AoI:
|
|
|
658
672
|
|
|
659
673
|
Returns:
|
|
660
674
|
list[str]: A sorted list of entity name strings.
|
|
675
|
+
|
|
661
676
|
"""
|
|
662
677
|
return sorted(_REGISTRY)
|
|
663
678
|
|
|
@@ -695,6 +710,7 @@ def _validate_entity_paths(entity: BaseEntity, name: str) -> None:
|
|
|
695
710
|
|
|
696
711
|
Raises:
|
|
697
712
|
EntityValidationError: If any path does not exist.
|
|
713
|
+
|
|
698
714
|
"""
|
|
699
715
|
# --- Static path existence check ----------------------------------------
|
|
700
716
|
try:
|
|
@@ -760,6 +776,7 @@ def _validate_entity(entity: BaseEntity, name: str) -> None:
|
|
|
760
776
|
|
|
761
777
|
Raises:
|
|
762
778
|
EntityValidationError: If any validation check fails.
|
|
779
|
+
|
|
763
780
|
"""
|
|
764
781
|
# Fast path-only check first.
|
|
765
782
|
_validate_entity_paths(entity, name)
|
core_lens/base/entity.py
CHANGED
|
@@ -65,6 +65,7 @@ def _so_key(storage_options: dict[str, Any]) -> tuple[tuple[str, Any], ...]:
|
|
|
65
65
|
|
|
66
66
|
Returns:
|
|
67
67
|
tuple[tuple[str, Any], ...]: A sorted tuple of ``(key, value)`` pairs.
|
|
68
|
+
|
|
68
69
|
"""
|
|
69
70
|
return tuple(sorted(storage_options.items()))
|
|
70
71
|
|
|
@@ -85,6 +86,7 @@ def _cached_detect(
|
|
|
85
86
|
|
|
86
87
|
Returns:
|
|
87
88
|
SchemaProfile: Detected schema profile for the given entity paths.
|
|
89
|
+
|
|
88
90
|
"""
|
|
89
91
|
from core_lens.schema.detection import detect
|
|
90
92
|
|
|
@@ -114,6 +116,7 @@ def _cached_build_index(
|
|
|
114
116
|
|
|
115
117
|
Returns:
|
|
116
118
|
pl.DataFrame: The bounding-box index DataFrame.
|
|
119
|
+
|
|
117
120
|
"""
|
|
118
121
|
logger.debug("Building bbox index for {} (cache miss)", static_path)
|
|
119
122
|
return build_bbox_index(
|
|
@@ -127,7 +130,7 @@ def _cached_build_index(
|
|
|
127
130
|
|
|
128
131
|
|
|
129
132
|
class BaseEntity(ABC):
|
|
130
|
-
"""Abstract base class for every entity in the core_lens plugin system.
|
|
133
|
+
r"""Abstract base class for every entity in the core_lens plugin system.
|
|
131
134
|
|
|
132
135
|
An entity represents a geospatial primitive (e.g. microwatershed, village,
|
|
133
136
|
district) backed by one or more Parquet/GeoParquet files. Entities are
|
|
@@ -195,6 +198,7 @@ class BaseEntity(ABC):
|
|
|
195
198
|
``polars.scan_parquet``. For S3 the common keys are
|
|
196
199
|
``"region"``, ``"access_key"``, and ``"secret_key"``.
|
|
197
200
|
``None`` uses ambient credentials (env-vars / ``~/.aws/``).
|
|
201
|
+
|
|
198
202
|
"""
|
|
199
203
|
# Store as str so cloud URIs (s3://…) are never coerced through pathlib.
|
|
200
204
|
self._data_root: str | None = str(data_root) if data_root is not None else None
|
|
@@ -219,6 +223,7 @@ class BaseEntity(ABC):
|
|
|
219
223
|
|
|
220
224
|
Raises:
|
|
221
225
|
FileNotFoundError: For *local* paths that do not exist after resolution.
|
|
226
|
+
|
|
222
227
|
"""
|
|
223
228
|
# Fully-qualified cloud URI — return as-is.
|
|
224
229
|
if is_cloud_uri(path):
|
|
@@ -255,13 +260,14 @@ class BaseEntity(ABC):
|
|
|
255
260
|
@property
|
|
256
261
|
@abstractmethod
|
|
257
262
|
def key_cols(self) -> list[str]:
|
|
258
|
-
"""Columns that uniquely identify one instance of this entity.
|
|
263
|
+
r"""Columns that uniquely identify one instance of this entity.
|
|
259
264
|
|
|
260
265
|
For built-in entities this is always a single-element list (e.g.
|
|
261
266
|
``[\"mws_id\"]``), but the contract allows composite keys for plugins.
|
|
262
267
|
|
|
263
268
|
Returns:
|
|
264
269
|
list[str]: A list of column name strings present in the static file.
|
|
270
|
+
|
|
265
271
|
"""
|
|
266
272
|
|
|
267
273
|
@property
|
|
@@ -274,6 +280,7 @@ class BaseEntity(ABC):
|
|
|
274
280
|
|
|
275
281
|
Returns:
|
|
276
282
|
str: The column name as a string.
|
|
283
|
+
|
|
277
284
|
"""
|
|
278
285
|
|
|
279
286
|
@property
|
|
@@ -287,6 +294,7 @@ class BaseEntity(ABC):
|
|
|
287
294
|
|
|
288
295
|
Returns:
|
|
289
296
|
str: A path string.
|
|
297
|
+
|
|
290
298
|
"""
|
|
291
299
|
|
|
292
300
|
@property
|
|
@@ -299,6 +307,7 @@ class BaseEntity(ABC):
|
|
|
299
307
|
|
|
300
308
|
Returns:
|
|
301
309
|
str | None: A path string, or ``None`` if the entity has no annual data.
|
|
310
|
+
|
|
302
311
|
"""
|
|
303
312
|
return None
|
|
304
313
|
|
|
@@ -312,6 +321,7 @@ class BaseEntity(ABC):
|
|
|
312
321
|
|
|
313
322
|
Returns:
|
|
314
323
|
str | None: A path string, or ``None`` if the entity has no fortnightly data.
|
|
324
|
+
|
|
315
325
|
"""
|
|
316
326
|
return None
|
|
317
327
|
|
|
@@ -336,6 +346,7 @@ class BaseEntity(ABC):
|
|
|
336
346
|
|
|
337
347
|
Returns:
|
|
338
348
|
SchemaProfile: A fully-validated :class:`~core_lens.schema.profile.SchemaProfile`.
|
|
349
|
+
|
|
339
350
|
"""
|
|
340
351
|
if not hasattr(self, "_schema_profile"):
|
|
341
352
|
_so = self._storage_options or {}
|
|
@@ -372,6 +383,7 @@ class BaseEntity(ABC):
|
|
|
372
383
|
|
|
373
384
|
Returns:
|
|
374
385
|
pl.DataFrame: DataFrame with columns ``(*key_cols, minx, miny, maxx, maxy)``.
|
|
386
|
+
|
|
375
387
|
"""
|
|
376
388
|
if not hasattr(self, "_cached_index"):
|
|
377
389
|
profile = self.schema_profile # already cached — no extra I/O
|
|
@@ -409,6 +421,7 @@ class BaseEntity(ABC):
|
|
|
409
421
|
Raises:
|
|
410
422
|
ValueError: If a kwarg cannot be resolved as either an attribute
|
|
411
423
|
column or a registered entity name.
|
|
424
|
+
|
|
412
425
|
"""
|
|
413
426
|
from core_lens.aoi import _REGISTRY
|
|
414
427
|
from core_lens.base.view import View
|
|
@@ -544,6 +557,7 @@ class BaseEntity(ABC):
|
|
|
544
557
|
|
|
545
558
|
Raises:
|
|
546
559
|
ValueError: If neither ``geometry`` nor ``bbox`` is provided.
|
|
560
|
+
|
|
547
561
|
"""
|
|
548
562
|
import shapely.geometry as sgeom
|
|
549
563
|
|
|
@@ -584,7 +598,7 @@ class BaseEntity(ABC):
|
|
|
584
598
|
return View(keys=keys, entity=self, entity_name=entity_name)
|
|
585
599
|
|
|
586
600
|
def spatial_join(self, other: "BaseEntity", agg: dict[str, str]) -> "View":
|
|
587
|
-
"""Return a lazy :class:`~core_lens.base.view.View` with a cross-entity join pending.
|
|
601
|
+
r"""Return a lazy :class:`~core_lens.base.view.View` with a cross-entity join pending.
|
|
588
602
|
|
|
589
603
|
The join is recorded in the View's ``join_spec`` and computed only at
|
|
590
604
|
materialisation time (``.static``, ``.annual``, or ``.fortnightly``).
|
|
@@ -601,6 +615,7 @@ class BaseEntity(ABC):
|
|
|
601
615
|
Returns:
|
|
602
616
|
View: A lazy :class:`~core_lens.base.view.View` with the join spec
|
|
603
617
|
recorded for deferred execution.
|
|
618
|
+
|
|
604
619
|
"""
|
|
605
620
|
from core_lens.base.view import View
|
|
606
621
|
|
|
@@ -17,6 +17,7 @@ class SubplotOn(Enum):
|
|
|
17
17
|
MONTH: Split data by month.
|
|
18
18
|
SEASON: Split data by meteorological season.
|
|
19
19
|
SEASON_YEAR: Split data by season and year.
|
|
20
|
+
|
|
20
21
|
"""
|
|
21
22
|
|
|
22
23
|
YEAR = "year"
|
|
@@ -71,6 +72,7 @@ def _wkb_to_arrow_table(
|
|
|
71
72
|
|
|
72
73
|
Returns:
|
|
73
74
|
An ``arro3.core.Table`` whose geometry column is GeoArrow-typed.
|
|
75
|
+
|
|
74
76
|
"""
|
|
75
77
|
import geoarrow.rust.core as ga
|
|
76
78
|
import polars as pl
|
|
@@ -182,6 +184,7 @@ def _color_for(index: int) -> str:
|
|
|
182
184
|
|
|
183
185
|
Returns:
|
|
184
186
|
The hex color string.
|
|
187
|
+
|
|
185
188
|
"""
|
|
186
189
|
return _PALETTE[index % len(_PALETTE)]
|
|
187
190
|
|
|
@@ -193,6 +196,7 @@ def _apply_theme(fig: Any, result: "Result", title: str) -> None:
|
|
|
193
196
|
fig: The Bokeh figure.
|
|
194
197
|
result: The parent Result object.
|
|
195
198
|
title: The title text.
|
|
199
|
+
|
|
196
200
|
"""
|
|
197
201
|
entity_name = getattr(result.entity, "__name__", "Unknown")
|
|
198
202
|
if hasattr(result, "entity_name"):
|
|
@@ -202,7 +206,6 @@ def _apply_theme(fig: Any, result: "Result", title: str) -> None:
|
|
|
202
206
|
fig.title.text_font = "Inter, sans-serif"
|
|
203
207
|
fig.title.text_font_size = "14pt"
|
|
204
208
|
|
|
205
|
-
# Subtitle-style annotation in the bottom-right corner.
|
|
206
209
|
from bokeh.models.annotations import Label
|
|
207
210
|
|
|
208
211
|
subtitle = Label(
|
|
@@ -227,6 +230,7 @@ class PlotNamespace:
|
|
|
227
230
|
|
|
228
231
|
Attributes:
|
|
229
232
|
result: The parent Result object.
|
|
233
|
+
|
|
230
234
|
"""
|
|
231
235
|
|
|
232
236
|
def __init__(self, result: "Result") -> None:
|
|
@@ -253,6 +257,7 @@ class PlotNamespace:
|
|
|
253
257
|
ValueError: If ``column`` or ``subplot_on`` column not found in data.
|
|
254
258
|
NotImplementedError: Lonboard does not support native subplot grids;
|
|
255
259
|
only single-value ``subplot_on`` is rendered when specified.
|
|
260
|
+
|
|
256
261
|
"""
|
|
257
262
|
import lonboard
|
|
258
263
|
from lonboard.colormap import apply_continuous_cmap
|
|
@@ -347,6 +352,7 @@ class PlotNamespace:
|
|
|
347
352
|
|
|
348
353
|
Returns:
|
|
349
354
|
Any: A Bokeh Tabs object (or a single Figure when ``aggregate=True``).
|
|
355
|
+
|
|
350
356
|
"""
|
|
351
357
|
import polars as pl
|
|
352
358
|
import polars.selectors as cs
|
|
@@ -474,7 +480,6 @@ class PlotNamespace:
|
|
|
474
480
|
_apply_theme(fig, self.result, title)
|
|
475
481
|
return fig
|
|
476
482
|
|
|
477
|
-
# --- Aggregated view --------------------------------------------------
|
|
478
483
|
group_cols = [x, subplot_col] if subplot_col else [x]
|
|
479
484
|
agg_pdf = (
|
|
480
485
|
df.group_by(group_cols)
|
|
@@ -486,7 +491,6 @@ class PlotNamespace:
|
|
|
486
491
|
if aggregate:
|
|
487
492
|
return _make_fig(agg_pdf, "Timeseries (Aggregated Mean)")
|
|
488
493
|
|
|
489
|
-
# --- Per-entity view --------------------------------------------------
|
|
490
494
|
entity_df = df.sort(x)
|
|
491
495
|
unique_entities = entity_df[key_col].unique()
|
|
492
496
|
if len(unique_entities) > top_n:
|
|
@@ -519,6 +523,7 @@ class PlotNamespace:
|
|
|
519
523
|
|
|
520
524
|
Returns:
|
|
521
525
|
Any: A Bokeh Figure or Tabs object.
|
|
526
|
+
|
|
522
527
|
"""
|
|
523
528
|
import polars as pl
|
|
524
529
|
import polars.selectors as cs
|
|
@@ -595,6 +600,7 @@ class PlotNamespace:
|
|
|
595
600
|
|
|
596
601
|
Returns:
|
|
597
602
|
Any: A Bokeh Figure or Tabs object.
|
|
603
|
+
|
|
598
604
|
"""
|
|
599
605
|
import numpy as np
|
|
600
606
|
import pandas as pd
|
|
@@ -665,6 +671,7 @@ class PlotNamespace:
|
|
|
665
671
|
|
|
666
672
|
Returns:
|
|
667
673
|
Any: A Bokeh Figure object.
|
|
674
|
+
|
|
668
675
|
"""
|
|
669
676
|
import polars.selectors as cs
|
|
670
677
|
from bokeh.models import BasicTicker, ColumnDataSource, LinearColorMapper
|
|
@@ -684,7 +691,6 @@ class PlotNamespace:
|
|
|
684
691
|
corr = df.to_pandas().corr()
|
|
685
692
|
cols = list(corr.columns)
|
|
686
693
|
|
|
687
|
-
# Build long-form data for rect glyphs.
|
|
688
694
|
xs: list[str] = []
|
|
689
695
|
ys: list[str] = []
|
|
690
696
|
vals: list[float] = []
|
|
@@ -744,6 +750,7 @@ class PlotNamespace:
|
|
|
744
750
|
|
|
745
751
|
Returns:
|
|
746
752
|
Any: A Bokeh Figure object.
|
|
753
|
+
|
|
747
754
|
"""
|
|
748
755
|
from bokeh.models import (
|
|
749
756
|
BasicTicker,
|
|
@@ -769,7 +776,6 @@ class PlotNamespace:
|
|
|
769
776
|
x_vals = [str(v) for v in pdf.columns.tolist()]
|
|
770
777
|
y_vals = [str(v) for v in pdf.index.tolist()]
|
|
771
778
|
|
|
772
|
-
# Long-form
|
|
773
779
|
xs2: list[str] = []
|
|
774
780
|
ys2: list[str] = []
|
|
775
781
|
vals2: list[float] = []
|
|
@@ -819,6 +825,7 @@ class PlotNamespace:
|
|
|
819
825
|
|
|
820
826
|
Returns:
|
|
821
827
|
Any: A Bokeh ``gridplot`` object.
|
|
828
|
+
|
|
822
829
|
"""
|
|
823
830
|
import polars.selectors as cs
|
|
824
831
|
from bokeh.layouts import gridplot
|
|
@@ -29,6 +29,7 @@ def _sf(x: object) -> float:
|
|
|
29
29
|
|
|
30
30
|
Returns:
|
|
31
31
|
float: The numeric value as a float, or NaN if the input is None.
|
|
32
|
+
|
|
32
33
|
"""
|
|
33
34
|
if x is None:
|
|
34
35
|
return float("nan")
|
|
@@ -42,6 +43,7 @@ class CorrelateMethod(Enum):
|
|
|
42
43
|
PEARSON: Pearson correlation coefficient.
|
|
43
44
|
SPEARMAN: Spearman rank correlation.
|
|
44
45
|
KENDALL: Kendall Tau correlation.
|
|
46
|
+
|
|
45
47
|
"""
|
|
46
48
|
|
|
47
49
|
PEARSON = "pearson"
|
|
@@ -58,6 +60,7 @@ class TestMethod(Enum):
|
|
|
58
60
|
WILCOXON: Wilcoxon signed-rank test.
|
|
59
61
|
KS: Kolmogorov-Smirnov test.
|
|
60
62
|
CHI_SQUARE: Chi-square test.
|
|
63
|
+
|
|
61
64
|
"""
|
|
62
65
|
|
|
63
66
|
__test__ = False
|
|
@@ -75,6 +78,7 @@ class ChangeMethod(Enum):
|
|
|
75
78
|
ABSOLUTE: Absolute difference between periods.
|
|
76
79
|
PERCENTAGE: Percentage change between periods.
|
|
77
80
|
TREND: Linear trend over time.
|
|
81
|
+
|
|
78
82
|
"""
|
|
79
83
|
|
|
80
84
|
ABSOLUTE = "absolute"
|
|
@@ -90,6 +94,7 @@ class AnomalyCrossMethod(Enum):
|
|
|
90
94
|
IQR: Interquartile range method.
|
|
91
95
|
PERCENTILE: Percentile-based method.
|
|
92
96
|
THRESHOLD: Fixed threshold method.
|
|
97
|
+
|
|
93
98
|
"""
|
|
94
99
|
|
|
95
100
|
ZSCORE = "zscore"
|
|
@@ -105,6 +110,7 @@ class AnomalyTsMethod(Enum):
|
|
|
105
110
|
STL: Seasonal-Trend decomposition using LOESS.
|
|
106
111
|
CUSUM: Cumulative sum control chart.
|
|
107
112
|
MAD: Median Absolute Deviation.
|
|
113
|
+
|
|
108
114
|
"""
|
|
109
115
|
|
|
110
116
|
STL = "stl"
|
|
@@ -120,6 +126,7 @@ class SimilarityMethod(Enum):
|
|
|
120
126
|
COSINE: Cosine similarity.
|
|
121
127
|
MAHALANOBIS: Mahalanobis distance.
|
|
122
128
|
MANHATTAN: Manhattan distance.
|
|
129
|
+
|
|
123
130
|
"""
|
|
124
131
|
|
|
125
132
|
EUCLIDEAN = "euclidean"
|
|
@@ -167,7 +174,8 @@ class StatsNamespace:
|
|
|
167
174
|
columns: list[str] | None = None,
|
|
168
175
|
by: str = "column",
|
|
169
176
|
) -> "Result":
|
|
170
|
-
"""Per-column or per-entity descriptive statistics.
|
|
177
|
+
r"""Per-column or per-entity descriptive statistics.
|
|
178
|
+
|
|
171
179
|
Uses polars' in-built methods for mean, std, min, max, quantiles etc.
|
|
172
180
|
|
|
173
181
|
Args:
|
|
@@ -180,6 +188,7 @@ class StatsNamespace:
|
|
|
180
188
|
|
|
181
189
|
Raises:
|
|
182
190
|
ValueError: If ``by`` is not ``\"column\"`` or ``\"entity\"``.
|
|
191
|
+
|
|
183
192
|
"""
|
|
184
193
|
df = self._r.data
|
|
185
194
|
cols = columns if columns is not None else self._numeric_cols(df)
|
|
@@ -239,6 +248,7 @@ class StatsNamespace:
|
|
|
239
248
|
Raises:
|
|
240
249
|
CorrelationError: If fewer than 2 columns supplied.
|
|
241
250
|
ValueError: If ``method`` is not recognised.
|
|
251
|
+
|
|
242
252
|
"""
|
|
243
253
|
if len(columns) < 2:
|
|
244
254
|
raise CorrelationError(
|
|
@@ -313,6 +323,7 @@ class StatsNamespace:
|
|
|
313
323
|
Raises:
|
|
314
324
|
ValueError: If none of ``groups``, ``periods``, ``against`` supplied,
|
|
315
325
|
or if ``method`` is not recognised.
|
|
326
|
+
|
|
316
327
|
"""
|
|
317
328
|
import scipy.stats as sp
|
|
318
329
|
|
|
@@ -460,6 +471,7 @@ class StatsNamespace:
|
|
|
460
471
|
|
|
461
472
|
Raises:
|
|
462
473
|
ValueError: If ``method`` is not recognised or year column absent.
|
|
474
|
+
|
|
463
475
|
"""
|
|
464
476
|
if not isinstance(method, ChangeMethod):
|
|
465
477
|
raise ValueError(
|
|
@@ -566,6 +578,7 @@ class StatsNamespace:
|
|
|
566
578
|
|
|
567
579
|
Raises:
|
|
568
580
|
ValueError: If ``mode``, ``method``, or observation count invalid.
|
|
581
|
+
|
|
569
582
|
"""
|
|
570
583
|
df = self._r.data
|
|
571
584
|
key = self._r.key_cols[0]
|
|
@@ -835,6 +848,7 @@ class StatsNamespace:
|
|
|
835
848
|
Raises:
|
|
836
849
|
ValueError: If ``method`` is invalid, no columns can be resolved,
|
|
837
850
|
or ``target`` is not found.
|
|
851
|
+
|
|
838
852
|
"""
|
|
839
853
|
if not isinstance(method, SimilarityMethod):
|
|
840
854
|
raise ValueError(
|
core_lens/base/result.py
CHANGED
|
@@ -53,6 +53,7 @@ class Result:
|
|
|
53
53
|
entity: Reference to the parent entity, retained so
|
|
54
54
|
:meth:`with_geometry` can locate the static file without
|
|
55
55
|
requiring callers to pass it again.
|
|
56
|
+
|
|
56
57
|
"""
|
|
57
58
|
|
|
58
59
|
def __init__(
|
|
@@ -79,6 +80,7 @@ class Result:
|
|
|
79
80
|
|
|
80
81
|
Returns:
|
|
81
82
|
pl.DataFrame: The materialised data frame.
|
|
83
|
+
|
|
82
84
|
"""
|
|
83
85
|
return self.data
|
|
84
86
|
|
|
@@ -93,6 +95,7 @@ class Result:
|
|
|
93
95
|
|
|
94
96
|
Raises:
|
|
95
97
|
TypeError: If :attr:`has_geometry` is ``False``.
|
|
98
|
+
|
|
96
99
|
"""
|
|
97
100
|
if not self.has_geometry:
|
|
98
101
|
raise TypeError(
|
|
@@ -121,6 +124,7 @@ class Result:
|
|
|
121
124
|
|
|
122
125
|
Returns:
|
|
123
126
|
pl.LazyFrame: A lazy frame backed by :attr:`data`.
|
|
127
|
+
|
|
124
128
|
"""
|
|
125
129
|
return self.data.lazy()
|
|
126
130
|
|
|
@@ -139,6 +143,7 @@ class Result:
|
|
|
139
143
|
Result: A new :class:`Result` with the geometry column merged in and
|
|
140
144
|
``has_geometry=True``. If ``has_geometry`` is already ``True``,
|
|
141
145
|
returns ``self`` unchanged.
|
|
146
|
+
|
|
142
147
|
"""
|
|
143
148
|
if self.has_geometry:
|
|
144
149
|
return self
|
|
@@ -172,6 +177,7 @@ class Result:
|
|
|
172
177
|
|
|
173
178
|
Returns:
|
|
174
179
|
Result: A new :class:`Result` with ``name`` appended to :attr:`data`.
|
|
180
|
+
|
|
175
181
|
"""
|
|
176
182
|
new_data = self.data.with_columns(expr.alias(name))
|
|
177
183
|
return self._replace(data=new_data)
|
|
@@ -219,6 +225,7 @@ class Result:
|
|
|
219
225
|
Raises:
|
|
220
226
|
ValueError: If ``by`` is incompatible with :attr:`resolution`, or
|
|
221
227
|
if ``by`` is not a recognised grouping name.
|
|
228
|
+
|
|
222
229
|
"""
|
|
223
230
|
if by is not None and by not in _VALID_BY:
|
|
224
231
|
raise ValueError(
|
|
@@ -263,6 +270,7 @@ class Result:
|
|
|
263
270
|
|
|
264
271
|
Returns:
|
|
265
272
|
StatsNamespace: The statistical analysis namespace.
|
|
273
|
+
|
|
266
274
|
"""
|
|
267
275
|
from core_lens.base.namespaces.stats import StatsNamespace
|
|
268
276
|
|
|
@@ -281,6 +289,7 @@ class Result:
|
|
|
281
289
|
|
|
282
290
|
Returns:
|
|
283
291
|
PlotNamespace: The visualisation namespace.
|
|
292
|
+
|
|
284
293
|
"""
|
|
285
294
|
from core_lens.base.namespaces.plot import PlotNamespace
|
|
286
295
|
|
core_lens/base/view.py
CHANGED
|
@@ -61,6 +61,7 @@ class View:
|
|
|
61
61
|
join_spec: A dict encoding a deferred :meth:`~BaseEntity.spatial_join`
|
|
62
62
|
request, or ``None``. Evaluated during materialisation after the
|
|
63
63
|
primary scan is complete.
|
|
64
|
+
|
|
64
65
|
"""
|
|
65
66
|
|
|
66
67
|
def __init__(
|
|
@@ -94,6 +95,7 @@ class View:
|
|
|
94
95
|
|
|
95
96
|
Returns:
|
|
96
97
|
View: A new lazy :class:`View` with the narrowed keys.
|
|
98
|
+
|
|
97
99
|
"""
|
|
98
100
|
static = self.entity._resolve(self.entity.static_path)
|
|
99
101
|
|
|
@@ -141,6 +143,7 @@ class View:
|
|
|
141
143
|
|
|
142
144
|
Raises:
|
|
143
145
|
ValueError: If neither ``geometry`` nor ``bbox`` is provided.
|
|
146
|
+
|
|
144
147
|
"""
|
|
145
148
|
import shapely.geometry as sgeom
|
|
146
149
|
from core_lens.utils.spatial import (
|
|
@@ -198,6 +201,7 @@ class View:
|
|
|
198
201
|
|
|
199
202
|
Returns:
|
|
200
203
|
View: A new lazy :class:`View` with the join spec recorded.
|
|
204
|
+
|
|
201
205
|
"""
|
|
202
206
|
if self.join_spec is not None:
|
|
203
207
|
logger.error(
|
|
@@ -260,6 +264,7 @@ class View:
|
|
|
260
264
|
ValueError: If the arguments are inconsistent (e.g. mixing date
|
|
261
265
|
range and season arguments, omitting required args, or
|
|
262
266
|
supplying ``year`` with ``season="current"``).
|
|
267
|
+
|
|
263
268
|
"""
|
|
264
269
|
if season is not None and (start is not None or end is not None):
|
|
265
270
|
logger.error(
|
|
@@ -329,6 +334,7 @@ class View:
|
|
|
329
334
|
Raises:
|
|
330
335
|
AttributeError: If the entity has no ``static_path`` (should not
|
|
331
336
|
happen in practice since ``static_path`` is mandatory).
|
|
337
|
+
|
|
332
338
|
"""
|
|
333
339
|
return self._materialise(Resolution.STATIC)
|
|
334
340
|
|
|
@@ -342,6 +348,7 @@ class View:
|
|
|
342
348
|
|
|
343
349
|
Raises:
|
|
344
350
|
AttributeError: If the entity has no ``annual_path``.
|
|
351
|
+
|
|
345
352
|
"""
|
|
346
353
|
return self._materialise(Resolution.ANNUAL)
|
|
347
354
|
|
|
@@ -355,6 +362,7 @@ class View:
|
|
|
355
362
|
|
|
356
363
|
Raises:
|
|
357
364
|
AttributeError: If the entity has no ``fortnightly_path``.
|
|
365
|
+
|
|
358
366
|
"""
|
|
359
367
|
return self._materialise(Resolution.FORTNIGHTLY)
|
|
360
368
|
|
core_lens/entities/mws.py
CHANGED
|
@@ -14,35 +14,50 @@ class MWSEntity(BaseEntity):
|
|
|
14
14
|
|
|
15
15
|
@property
|
|
16
16
|
def key_cols(self) -> list[str]:
|
|
17
|
-
"""
|
|
18
|
-
|
|
17
|
+
"""Get the property value.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
list[str]: The key columns for the entity.
|
|
21
|
+
|
|
19
22
|
"""
|
|
20
23
|
return ["mws_id"]
|
|
21
24
|
|
|
22
25
|
@property
|
|
23
26
|
def geometry_col(self) -> str:
|
|
24
|
-
"""
|
|
25
|
-
|
|
27
|
+
"""Get the property value.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
str: The geometry column name.
|
|
31
|
+
|
|
26
32
|
"""
|
|
27
33
|
return "geometry"
|
|
28
34
|
|
|
29
35
|
@property
|
|
30
36
|
def static_path(self) -> str:
|
|
31
|
-
"""
|
|
32
|
-
|
|
37
|
+
"""Get the property value.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
str: The relative path to the static data.
|
|
41
|
+
|
|
33
42
|
"""
|
|
34
43
|
return "mws/static"
|
|
35
44
|
|
|
36
45
|
@property
|
|
37
46
|
def annual_path(self) -> str | None:
|
|
38
|
-
"""
|
|
39
|
-
|
|
47
|
+
"""Get the property value.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
str | None: The relative path to the annual data.
|
|
51
|
+
|
|
40
52
|
"""
|
|
41
53
|
return "mws/annual"
|
|
42
54
|
|
|
43
55
|
@property
|
|
44
56
|
def fortnightly_path(self) -> str | None:
|
|
45
|
-
"""
|
|
46
|
-
|
|
57
|
+
"""Get the property value.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
str | None: The relative path to the fortnightly data.
|
|
61
|
+
|
|
47
62
|
"""
|
|
48
63
|
return "mws/fortnightly"
|
core_lens/entities/tehsil.py
CHANGED
|
@@ -15,29 +15,41 @@ class TehsilEntity(BaseEntity):
|
|
|
15
15
|
|
|
16
16
|
@property
|
|
17
17
|
def key_cols(self) -> list[str]:
|
|
18
|
-
"""
|
|
19
|
-
|
|
18
|
+
"""Get the property value.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
list[str]: The key columns for the entity.
|
|
22
|
+
|
|
20
23
|
"""
|
|
21
24
|
return ["id"]
|
|
22
25
|
|
|
23
26
|
@property
|
|
24
27
|
def geometry_col(self) -> str:
|
|
25
|
-
"""
|
|
26
|
-
|
|
28
|
+
"""Get the property value.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
str: The geometry column name.
|
|
32
|
+
|
|
27
33
|
"""
|
|
28
34
|
return "geom"
|
|
29
35
|
|
|
30
36
|
@property
|
|
31
37
|
def static_path(self) -> str:
|
|
32
|
-
"""
|
|
33
|
-
|
|
38
|
+
"""Get the property value.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
str: The relative path to the static data.
|
|
42
|
+
|
|
34
43
|
"""
|
|
35
44
|
return "tehsil/static/tehsil.parquet"
|
|
36
45
|
|
|
37
46
|
@property
|
|
38
47
|
def schema_profile(self) -> SchemaProfile:
|
|
39
|
-
"""
|
|
40
|
-
|
|
48
|
+
"""Get the property value.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
SchemaProfile: The hardcoded schema profile.
|
|
52
|
+
|
|
41
53
|
"""
|
|
42
54
|
if not hasattr(self, "_schema_profile"):
|
|
43
55
|
self._schema_profile = SchemaProfile(
|
core_lens/entities/waterbody.py
CHANGED
|
@@ -13,28 +13,40 @@ class WaterbodyEntity(BaseEntity):
|
|
|
13
13
|
|
|
14
14
|
@property
|
|
15
15
|
def key_cols(self) -> list[str]:
|
|
16
|
-
"""
|
|
17
|
-
|
|
16
|
+
"""Get the property value.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
list[str]: The key columns for the entity.
|
|
20
|
+
|
|
18
21
|
"""
|
|
19
22
|
return ["wb_id"]
|
|
20
23
|
|
|
21
24
|
@property
|
|
22
25
|
def geometry_col(self) -> str:
|
|
23
|
-
"""
|
|
24
|
-
|
|
26
|
+
"""Get the property value.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
str: The geometry column name.
|
|
30
|
+
|
|
25
31
|
"""
|
|
26
32
|
return "geometry"
|
|
27
33
|
|
|
28
34
|
@property
|
|
29
35
|
def static_path(self) -> str:
|
|
30
|
-
"""
|
|
31
|
-
|
|
36
|
+
"""Get the property value.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
str: The relative path to the static data.
|
|
40
|
+
|
|
32
41
|
"""
|
|
33
42
|
return "waterbodies/static"
|
|
34
43
|
|
|
35
44
|
@property
|
|
36
45
|
def annual_path(self) -> str | None:
|
|
37
|
-
"""
|
|
38
|
-
|
|
46
|
+
"""Get the property value.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
str | None: The relative path to the annual data.
|
|
50
|
+
|
|
39
51
|
"""
|
|
40
52
|
return "waterbodies/annual"
|
core_lens/export/formats.py
CHANGED
|
@@ -33,6 +33,7 @@ def parquet(result: "Result", path: str | pathlib.Path, **kwargs: Any) -> None:
|
|
|
33
33
|
Example:
|
|
34
34
|
>>> from core_lens.export import parquet
|
|
35
35
|
>>> parquet(result, "output.parquet", compression="zstd", compression_level=3)
|
|
36
|
+
|
|
36
37
|
"""
|
|
37
38
|
if result.has_geometry:
|
|
38
39
|
raise TypeError(
|
|
@@ -57,6 +58,7 @@ def json(result: "Result", path: str | pathlib.Path, **kwargs: Any) -> None:
|
|
|
57
58
|
Example:
|
|
58
59
|
>>> from core_lens.export import json
|
|
59
60
|
>>> json(result, "output.json", pretty=True)
|
|
61
|
+
|
|
60
62
|
"""
|
|
61
63
|
if result.has_geometry:
|
|
62
64
|
raise TypeError(
|
|
@@ -81,6 +83,7 @@ def csv(result: "Result", path: str | pathlib.Path, **kwargs: Any) -> None:
|
|
|
81
83
|
Example:
|
|
82
84
|
>>> from core_lens.export import csv
|
|
83
85
|
>>> csv(result, "output.csv", separator=",")
|
|
86
|
+
|
|
84
87
|
"""
|
|
85
88
|
if result.has_geometry:
|
|
86
89
|
raise TypeError(
|
|
@@ -110,6 +113,7 @@ def geoparquet(result: "Result", path: str | pathlib.Path, **kwargs: Any) -> Non
|
|
|
110
113
|
|
|
111
114
|
Raises:
|
|
112
115
|
TypeError: If the Result object does not have geometry.
|
|
116
|
+
|
|
113
117
|
"""
|
|
114
118
|
if not result.has_geometry:
|
|
115
119
|
raise TypeError(
|
|
@@ -180,6 +184,7 @@ def geojson(result: "Result", path: str | pathlib.Path, **kwargs: Any) -> None:
|
|
|
180
184
|
|
|
181
185
|
Raises:
|
|
182
186
|
TypeError: If the Result object does not have geometry.
|
|
187
|
+
|
|
183
188
|
"""
|
|
184
189
|
if not result.has_geometry:
|
|
185
190
|
raise TypeError(
|
core_lens/schema/detection.py
CHANGED
|
@@ -81,6 +81,7 @@ def detect(
|
|
|
81
81
|
Raises:
|
|
82
82
|
SchemaDetectionError: If a required column is absent, the geometry
|
|
83
83
|
type cannot be inferred, or any other detection step fails.
|
|
84
|
+
|
|
84
85
|
"""
|
|
85
86
|
_so = storage_options or {}
|
|
86
87
|
static_schema = _read_schema(
|
|
@@ -178,6 +179,14 @@ def _infer_bbox_cols(schema: pl.Schema) -> tuple[str, str, str, str] | None:
|
|
|
178
179
|
for pattern in _BBOX_PATTERNS:
|
|
179
180
|
if all(col in schema for col in pattern):
|
|
180
181
|
return pattern
|
|
182
|
+
|
|
183
|
+
for col_name, dtype in schema.items():
|
|
184
|
+
if isinstance(dtype, pl.Struct):
|
|
185
|
+
struct_fields = [f.name for f in dtype.fields]
|
|
186
|
+
for pattern in _BBOX_PATTERNS:
|
|
187
|
+
if all(f in struct_fields for f in pattern):
|
|
188
|
+
return tuple(f"{col_name}.{f}" for f in pattern) # type: ignore
|
|
189
|
+
|
|
181
190
|
return None
|
|
182
191
|
|
|
183
192
|
|
|
@@ -266,6 +275,7 @@ def _is_year_col_from_schema(
|
|
|
266
275
|
|
|
267
276
|
Returns:
|
|
268
277
|
bool | None: ``True`` for integer-year, ``False`` for Date/Datetime, ``None`` if absent.
|
|
278
|
+
|
|
269
279
|
"""
|
|
270
280
|
if time_col is None:
|
|
271
281
|
return None
|
core_lens/schema/profile.py
CHANGED
|
@@ -56,6 +56,7 @@ class SchemaProfile(BaseModel):
|
|
|
56
56
|
is used for validation and documentation only.
|
|
57
57
|
extra_annual_cols: Additional attribute columns in the annual file.
|
|
58
58
|
extra_fortnightly_cols: Additional attribute columns in the fortnightly file.
|
|
59
|
+
|
|
59
60
|
"""
|
|
60
61
|
|
|
61
62
|
model_config = {"frozen": True}
|
|
@@ -89,6 +90,7 @@ class SchemaProfile(BaseModel):
|
|
|
89
90
|
Raises:
|
|
90
91
|
ValueError: If the file is not valid JSON.
|
|
91
92
|
FileNotFoundError: If the file does not exist.
|
|
93
|
+
|
|
92
94
|
"""
|
|
93
95
|
import json
|
|
94
96
|
|
core_lens/utils/paths.py
CHANGED
|
@@ -34,6 +34,7 @@ def is_cloud_uri(uri: str) -> bool:
|
|
|
34
34
|
|
|
35
35
|
Returns:
|
|
36
36
|
bool: ``True`` for URIs starting with a recognised cloud scheme.
|
|
37
|
+
|
|
37
38
|
"""
|
|
38
39
|
return any(uri.startswith(scheme) for scheme in _CLOUD_SCHEMES)
|
|
39
40
|
|
|
@@ -50,6 +51,7 @@ def resolve_fs_and_path(uri: str) -> tuple[pafs.FileSystem, str]:
|
|
|
50
51
|
Returns:
|
|
51
52
|
tuple[pyarrow.fs.FileSystem, str]: The resolved filesystem and the
|
|
52
53
|
normalised path within that filesystem.
|
|
54
|
+
|
|
53
55
|
"""
|
|
54
56
|
if is_cloud_uri(uri):
|
|
55
57
|
fs, path = pafs.FileSystem.from_uri(uri)
|
|
@@ -72,6 +74,7 @@ def path_exists(uri: str) -> bool:
|
|
|
72
74
|
|
|
73
75
|
Returns:
|
|
74
76
|
bool: ``True`` if the path exists (file or directory).
|
|
77
|
+
|
|
75
78
|
"""
|
|
76
79
|
try:
|
|
77
80
|
fs, path = resolve_fs_and_path(uri)
|
|
@@ -95,6 +98,7 @@ def join_uri(root: str, rel: str) -> str:
|
|
|
95
98
|
|
|
96
99
|
Returns:
|
|
97
100
|
str: The joined URI or absolute path string.
|
|
101
|
+
|
|
98
102
|
"""
|
|
99
103
|
if is_cloud_uri(root):
|
|
100
104
|
return root.rstrip("/") + "/" + rel.lstrip("/")
|
core_lens/utils/polars_utils.py
CHANGED
|
@@ -17,6 +17,7 @@ def _gpu_available() -> bool:
|
|
|
17
17
|
|
|
18
18
|
Returns:
|
|
19
19
|
bool: True if GPU is available.
|
|
20
|
+
|
|
20
21
|
"""
|
|
21
22
|
global _GPU_AVAILABLE
|
|
22
23
|
if _GPU_AVAILABLE is None:
|
|
@@ -56,6 +57,7 @@ def collect_lf(lf: pl.LazyFrame) -> pl.DataFrame:
|
|
|
56
57
|
|
|
57
58
|
Returns:
|
|
58
59
|
pl.DataFrame: A materialised ``pl.DataFrame``.
|
|
60
|
+
|
|
59
61
|
"""
|
|
60
62
|
global _GPU_AVAILABLE
|
|
61
63
|
if _gpu_available():
|
|
@@ -107,6 +109,7 @@ def scan_with_key_filter(
|
|
|
107
109
|
|
|
108
110
|
Returns:
|
|
109
111
|
pl.LazyFrame: A ``pl.LazyFrame`` ready to be ``.collect()``-ed.
|
|
112
|
+
|
|
110
113
|
"""
|
|
111
114
|
_so = storage_options or {}
|
|
112
115
|
lf = pl.scan_parquet(path, hive_partitioning=True, storage_options=_so or None)
|
core_lens/utils/season.py
CHANGED
|
@@ -42,6 +42,7 @@ def resolve_time_filter(
|
|
|
42
42
|
|
|
43
43
|
Raises:
|
|
44
44
|
ValueError: If ``time_filter`` has an unrecognised structure.
|
|
45
|
+
|
|
45
46
|
"""
|
|
46
47
|
if "start" in time_filter and "end" in time_filter:
|
|
47
48
|
start = time_filter["start"]
|
|
@@ -93,6 +94,7 @@ def _date_range_expr(
|
|
|
93
94
|
|
|
94
95
|
Returns:
|
|
95
96
|
pl.Expr: A Polars expression.
|
|
97
|
+
|
|
96
98
|
"""
|
|
97
99
|
start_date = datetime.date.fromisoformat(start)
|
|
98
100
|
end_date = datetime.date.fromisoformat(end)
|
|
@@ -141,6 +143,7 @@ def _season_expr(
|
|
|
141
143
|
|
|
142
144
|
Returns:
|
|
143
145
|
pl.Expr: A Polars expression.
|
|
146
|
+
|
|
144
147
|
"""
|
|
145
148
|
start_md, end_md = getattr(season_config, season_name)
|
|
146
149
|
year_crossing = start_md > end_md
|
|
@@ -182,6 +185,7 @@ def _parse_md(md: str) -> tuple[int, int]:
|
|
|
182
185
|
|
|
183
186
|
Returns:
|
|
184
187
|
tuple[int, int]: A tuple containing ``(month, day)`` as integers.
|
|
188
|
+
|
|
185
189
|
"""
|
|
186
190
|
month, day = md.split("-")
|
|
187
191
|
return int(month), int(day)
|
|
@@ -198,6 +202,7 @@ def _year_bounds(year: int | tuple[int, int] | None) -> tuple[int, int]:
|
|
|
198
202
|
|
|
199
203
|
Returns:
|
|
200
204
|
tuple[int, int]: A tuple containing ``(from_year, to_year)``.
|
|
205
|
+
|
|
201
206
|
"""
|
|
202
207
|
# When year is None use a wide range covering past and future data.
|
|
203
208
|
if year is None:
|
|
@@ -238,6 +243,7 @@ def add_temporal_columns(
|
|
|
238
243
|
|
|
239
244
|
Returns:
|
|
240
245
|
pl.DataFrame: A new DataFrame with the temporal grouping columns appended.
|
|
246
|
+
|
|
241
247
|
"""
|
|
242
248
|
existing = set(df.columns)
|
|
243
249
|
|
|
@@ -285,6 +291,7 @@ def add_temporal_columns(
|
|
|
285
291
|
|
|
286
292
|
Returns:
|
|
287
293
|
pl.Expr: A Polars boolean expression handling year rollover.
|
|
294
|
+
|
|
288
295
|
"""
|
|
289
296
|
if start <= end:
|
|
290
297
|
# is_between with bare strings → column-name lookup; use pl.lit().
|
core_lens/utils/spatial.py
CHANGED
|
@@ -45,6 +45,7 @@ def _bbox_sidecar_path(static_path: str) -> pathlib.Path | None:
|
|
|
45
45
|
|
|
46
46
|
Returns:
|
|
47
47
|
pathlib.Path | None: Sidecar path, or ``None`` if ``static_path`` is a cloud URI.
|
|
48
|
+
|
|
48
49
|
"""
|
|
49
50
|
if is_cloud_uri(static_path):
|
|
50
51
|
return None
|
|
@@ -74,6 +75,7 @@ def _read_bbox_sidecar(sidecar: pathlib.Path, static_path: str) -> pl.DataFrame
|
|
|
74
75
|
Returns:
|
|
75
76
|
pl.DataFrame | None: The cached index, or ``None`` if the sidecar is
|
|
76
77
|
absent or stale.
|
|
78
|
+
|
|
77
79
|
"""
|
|
78
80
|
if not sidecar.exists():
|
|
79
81
|
return None
|
|
@@ -100,6 +102,7 @@ def _write_bbox_sidecar(df: pl.DataFrame, sidecar: pathlib.Path) -> None:
|
|
|
100
102
|
Args:
|
|
101
103
|
df (pl.DataFrame): The bbox index DataFrame to persist.
|
|
102
104
|
sidecar (pathlib.Path): Destination sidecar path.
|
|
105
|
+
|
|
103
106
|
"""
|
|
104
107
|
try:
|
|
105
108
|
sidecar.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -120,6 +123,7 @@ def resolve_path(path: str) -> str:
|
|
|
120
123
|
|
|
121
124
|
Raises:
|
|
122
125
|
FileNotFoundError: If the resolved local path does not exist.
|
|
126
|
+
|
|
123
127
|
"""
|
|
124
128
|
if is_cloud_uri(path):
|
|
125
129
|
return path
|
|
@@ -169,18 +173,32 @@ def build_bbox_index(
|
|
|
169
173
|
|
|
170
174
|
Returns:
|
|
171
175
|
pl.DataFrame: A ``pl.DataFrame`` with columns ``(*key_cols, minx, miny, maxx, maxy)``.
|
|
176
|
+
|
|
172
177
|
"""
|
|
173
178
|
_so = storage_options or {}
|
|
174
179
|
if bbox_cols is not None:
|
|
175
180
|
logger.debug("Using pre-computed bbox columns: {}", bbox_cols)
|
|
176
|
-
|
|
181
|
+
base_bbox_cols = list(dict.fromkeys(c.split(".")[0] for c in bbox_cols))
|
|
182
|
+
cols_to_read = key_cols + base_bbox_cols
|
|
177
183
|
df = pl.read_parquet(
|
|
178
184
|
static_path, columns=cols_to_read, storage_options=_so or None
|
|
179
185
|
)
|
|
180
186
|
minx_col, miny_col, maxx_col, maxy_col = bbox_cols
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
187
|
+
|
|
188
|
+
for alias, col_path in zip(
|
|
189
|
+
["minx", "miny", "maxx", "maxy"],
|
|
190
|
+
[minx_col, miny_col, maxx_col, maxy_col],
|
|
191
|
+
):
|
|
192
|
+
parts = col_path.split(".")
|
|
193
|
+
if len(parts) == 1:
|
|
194
|
+
df = df.rename({col_path: alias})
|
|
195
|
+
else:
|
|
196
|
+
expr = pl.col(parts[0])
|
|
197
|
+
for p in parts[1:]:
|
|
198
|
+
expr = expr.struct.field(p)
|
|
199
|
+
df = df.with_columns(expr.alias(alias))
|
|
200
|
+
|
|
201
|
+
return df.select(key_cols + ["minx", "miny", "maxx", "maxy"])
|
|
184
202
|
|
|
185
203
|
cols_to_read = key_cols + [geometry_col]
|
|
186
204
|
if geometry_type == "latlon":
|
|
@@ -271,6 +289,7 @@ def bbox_intersects_geometry(
|
|
|
271
289
|
|
|
272
290
|
Returns:
|
|
273
291
|
pl.DataFrame: The subset of ``index_df`` whose rows overlap the geometry bounds.
|
|
292
|
+
|
|
274
293
|
"""
|
|
275
294
|
gminx, gminy, gmaxx, gmaxy = geometry.bounds # type: ignore[attr-defined]
|
|
276
295
|
return index_df.filter(
|
|
@@ -320,6 +339,7 @@ def exact_spatial_filter(
|
|
|
320
339
|
|
|
321
340
|
Raises:
|
|
322
341
|
ValueError: If ``relationship`` is not one of the valid options.
|
|
342
|
+
|
|
323
343
|
"""
|
|
324
344
|
logger.debug(
|
|
325
345
|
"Refining {} candidates with exact spatial filter (relationship='{}')",
|
|
@@ -423,6 +443,7 @@ def execute_spatial_join(
|
|
|
423
443
|
Returns:
|
|
424
444
|
pl.DataFrame: ``primary_df`` with additional columns
|
|
425
445
|
``{other_entity_name}_{col}`` appended for each ``agg`` entry.
|
|
446
|
+
|
|
426
447
|
"""
|
|
427
448
|
logger.info(
|
|
428
449
|
"Starting execute_spatial_join for other_entity_name={}", other_entity_name
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: core-lens
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.dev140
|
|
4
4
|
Summary: Query, analyse, and visualise CoreStack's microwatershed and Earth science data through a clean, composable Python API.
|
|
5
5
|
Project-URL: Homepage, https://github.com/ApoorvaKashyap/core-lens
|
|
6
6
|
Project-URL: Issues, https://github.com/ApoorvaKashyap/core-lens/issues
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
core_lens/__init__.py,sha256=orecTblbuvC112H3VSOVu60ABsP-fZ1q9Xq0mk5N_as,307
|
|
2
|
+
core_lens/__main__.py,sha256=ydzrJGjWcTIIsw9jqftIKvd-XMPSzD-uIY6RoBRvKGc,218
|
|
3
|
+
core_lens/_version.py,sha256=L2XU-ObkCHZUrKUrnKSt1WjwGMtJ04XzlG1c2qTbR3M,532
|
|
4
|
+
core_lens/aoi.py,sha256=FRwC_XMYxFj9ad58vLy9Q7l0hkUzBfrWkBcSxPfSYU0,31765
|
|
5
|
+
core_lens/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
core_lens/base/__init__.py,sha256=UyuirsO1YtbXmGVgPFcQcGsnVq0skH5IlhdVOmS2yGE,476
|
|
7
|
+
core_lens/base/entity.py,sha256=n-3fL5VY744u-wXYtBg3_oIHH_Z_mW_kHybCS2EFCnw,25224
|
|
8
|
+
core_lens/base/result.py,sha256=-I4eWChO1FxJGwmGHfauMcAaivZjrXnCXbb7EXAcop4,11934
|
|
9
|
+
core_lens/base/view.py,sha256=23s3IRRP4VQxVAbT0BIJ_TbH5UAbT512MZ_xJMlUf1o,19739
|
|
10
|
+
core_lens/base/namespaces/__init__.py,sha256=ktQAfiL-tjJFqFzw6FudepZz-pfDodIJUTCCOWshupc,210
|
|
11
|
+
core_lens/base/namespaces/plot.py,sha256=tY7-kkEnMOkkOsIaJy1Q2ItBtzdGqBx1DBoHTN3qrf4,30663
|
|
12
|
+
core_lens/base/namespaces/stats.py,sha256=9lfjTXpJU5TjvO9P0aqppUN9-l2rpP0VNTQ55fz0T0g,38922
|
|
13
|
+
core_lens/entities/__init__.py,sha256=66IshOBiTObYf9h3FD92lXQaJ0W-_DGFT9vudDeuVdU,253
|
|
14
|
+
core_lens/entities/mws.py,sha256=SvlDcTnqUwMzj_jMOZPcIGqq2D_b12z0wUOq0uCbrpM,1373
|
|
15
|
+
core_lens/entities/tehsil.py,sha256=7HbUoJwuSsBnrZQtrxLMw1-yuB18Y3_ckcA6JnTAE3o,1757
|
|
16
|
+
core_lens/entities/waterbody.py,sha256=4_2S-uGKCtOAfl2NCBSNMu4N5yyzM8z7H1Do1rPTgIY,1083
|
|
17
|
+
core_lens/export/__init__.py,sha256=vMF9Qb3uo9eciOh0OXI3BpZhCbufxccPt0YjaDtcl7M,178
|
|
18
|
+
core_lens/export/formats.py,sha256=-CTHltcDy0UPzEKZNrtnWnqsoj2t65imUxYTOz7l0oE,7454
|
|
19
|
+
core_lens/schema/__init__.py,sha256=fNCBoGh_Imr2KVpzERn_EoDmaREoac-gYkT3oJBGfcs,133
|
|
20
|
+
core_lens/schema/detection.py,sha256=DlIQR8OqYMtW49JEQFipch1Kjcr8uKUG-jNMYKwj9Ng,10127
|
|
21
|
+
core_lens/schema/profile.py,sha256=iYpBEjBzQVTl9_vQ8FtBZ_MmZOhU8cylNVFxZL2RVls,5095
|
|
22
|
+
core_lens/utils/__init__.py,sha256=jOWDmI8XtZ0NWosH-zbeOHHxHale4Dyz2MiS9MGP9sE,36
|
|
23
|
+
core_lens/utils/paths.py,sha256=-ARFoVwxs5Ht0pHi1PXApDwgRkptXPZ7nqFZy_mPZmA,3455
|
|
24
|
+
core_lens/utils/polars_utils.py,sha256=PL27FB9LlA5HVcW965x1SS7PzlUj-6DiY1I38ERWnVQ,4712
|
|
25
|
+
core_lens/utils/season.py,sha256=X-Z4im3q-O6gN6yRX6awC-7dy77CRGMPSggfEh5ZdD4,11969
|
|
26
|
+
core_lens/utils/spatial.py,sha256=AnGKa4SCOUJRkX2Wzp8DZgz5hjdGti65PlpzRQrHPJY,23418
|
|
27
|
+
core_lens-0.1.dev140.dist-info/METADATA,sha256=5olAVFUWY94nf9YnCKUqUEj5SIroaKZbZg0ctZ2gL4I,6175
|
|
28
|
+
core_lens-0.1.dev140.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
29
|
+
core_lens-0.1.dev140.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
30
|
+
core_lens-0.1.dev140.dist-info/RECORD,,
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
core_lens/__init__.py,sha256=orecTblbuvC112H3VSOVu60ABsP-fZ1q9Xq0mk5N_as,307
|
|
2
|
-
core_lens/__main__.py,sha256=ydzrJGjWcTIIsw9jqftIKvd-XMPSzD-uIY6RoBRvKGc,218
|
|
3
|
-
core_lens/_version.py,sha256=XOs-eSiptQsximat6U5vD_upBt2K_Ml1u8bvjm-d6TY,532
|
|
4
|
-
core_lens/aoi.py,sha256=o-g41E8CPm-kAp_dD-T7KtbD6x1lQgRmvCQnubm2HAo,31748
|
|
5
|
-
core_lens/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
-
core_lens/base/__init__.py,sha256=UyuirsO1YtbXmGVgPFcQcGsnVq0skH5IlhdVOmS2yGE,476
|
|
7
|
-
core_lens/base/entity.py,sha256=KvU9cb34M8F_YUAu8qHVtsTtVyZmmWcyU_j0SYOmI9Y,25206
|
|
8
|
-
core_lens/base/result.py,sha256=YZ5ShAX7l_DUHTBppa0wflPvm16K0LMtJRHTMkluGtU,11925
|
|
9
|
-
core_lens/base/view.py,sha256=HxD2ZPgt3THWA6r4M6OfAnYAScJo_NODtBXGFZijfb4,19731
|
|
10
|
-
core_lens/base/namespaces/__init__.py,sha256=ktQAfiL-tjJFqFzw6FudepZz-pfDodIJUTCCOWshupc,210
|
|
11
|
-
core_lens/base/namespaces/plot.py,sha256=nDg3SH41OPCLaxRNpEMMb1z4ZLchKVFKvYsYfw2gbTk,30941
|
|
12
|
-
core_lens/base/namespaces/stats.py,sha256=x6SIoO9Hfs1MoSKSOkZNKHF5VTnskW_OIk5cMkY0G4U,38907
|
|
13
|
-
core_lens/entities/__init__.py,sha256=66IshOBiTObYf9h3FD92lXQaJ0W-_DGFT9vudDeuVdU,253
|
|
14
|
-
core_lens/entities/mws.py,sha256=ZKox2zG-j5GF9Jld3vabfRmirWapQp62v6jRrY5GjYY,1183
|
|
15
|
-
core_lens/entities/tehsil.py,sha256=qULw32bMX0R9eMBCz5Y2M3r_40v2KhbUCFpZllhUQ7Q,1605
|
|
16
|
-
core_lens/entities/waterbody.py,sha256=7MnoB7siUzkeKyoIwZY6d98FEAzV0e4uqFYdCml5pNk,931
|
|
17
|
-
core_lens/export/__init__.py,sha256=vMF9Qb3uo9eciOh0OXI3BpZhCbufxccPt0YjaDtcl7M,178
|
|
18
|
-
core_lens/export/formats.py,sha256=yDS_PgSn4fGRR4402gVWvpT0dq57X02bog95yedk2l4,7449
|
|
19
|
-
core_lens/schema/__init__.py,sha256=fNCBoGh_Imr2KVpzERn_EoDmaREoac-gYkT3oJBGfcs,133
|
|
20
|
-
core_lens/schema/detection.py,sha256=zcUM9DaxAzKbX-_4vM6aST3MNkZmxi2Yu4EyZuTFbR0,9791
|
|
21
|
-
core_lens/schema/profile.py,sha256=5Pbfv_4jtzFJnRQA5Z680hxywpiV7882_NJGoT327hc,5093
|
|
22
|
-
core_lens/utils/__init__.py,sha256=jOWDmI8XtZ0NWosH-zbeOHHxHale4Dyz2MiS9MGP9sE,36
|
|
23
|
-
core_lens/utils/paths.py,sha256=-b6_NwwnyjyYD6i7Cwze64LhzhHNRlvrKBTqR4rjojA,3451
|
|
24
|
-
core_lens/utils/polars_utils.py,sha256=dcUO4Sn64swF343kbFKz5UYldPSZ6kfNFJ14iUt505c,4709
|
|
25
|
-
core_lens/utils/season.py,sha256=5H8QRjbu5-kQlVnqE_qGQtt_JcX2CuNJm-Wyhgps5ro,11962
|
|
26
|
-
core_lens/utils/spatial.py,sha256=onhJwyap4OIKvjNEOCFtb4zkQ6ZYqki6lVF_YJ4spH4,22912
|
|
27
|
-
core_lens-0.1.dev136.dist-info/METADATA,sha256=l72l8jxxKO06hEQEdRGVkHiT5U4THoPRz-9vkWUf9B8,6175
|
|
28
|
-
core_lens-0.1.dev136.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
29
|
-
core_lens-0.1.dev136.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
30
|
-
core_lens-0.1.dev136.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|