core-lens 0.1.dev112__tar.gz → 0.1.dev114__tar.gz
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-0.1.dev112 → core_lens-0.1.dev114}/PKG-INFO +1 -1
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/src/core_lens/_version.py +2 -2
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/src/core_lens/base/namespaces/plot.py +13 -9
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/src/core_lens/base/view.py +21 -1
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/src/core_lens/utils/season.py +68 -28
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/src/core_lens/utils/spatial.py +2 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/.github/pull_request_template.md +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/.github/workflows/ci.yml +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/.github/workflows/gh-pages.yml +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/.github/workflows/pre-release.yml +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/.github/workflows/release.yml +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/.gitignore +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/.gitmessage +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/.pre-commit-config.yaml +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/.python-version +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/CONTRIBUTING.md +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/LICENSE +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/README.md +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/SKILLS.md +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/docs/Makefile +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/docs/make.bat +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/docs/source/concepts.md +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/docs/source/conf.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/docs/source/index.rst +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/docs/source/intro.md +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/docs/source/plots.md +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/docs/source/plugins.md +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/docs/source/queries.md +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/docs/source/quickstart.md +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/docs/source/stats.md +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/examples/demo_mws.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/examples/demo_tehsil.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/hooks/mypy.sh +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/hooks/no-parquet-outside-fixtures.sh +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/hooks/pytest.sh +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/pyproject.toml +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/src/core_lens/__init__.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/src/core_lens/__main__.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/src/core_lens/aoi.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/src/core_lens/base/__init__.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/src/core_lens/base/entity.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/src/core_lens/base/namespaces/__init__.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/src/core_lens/base/namespaces/stats.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/src/core_lens/base/result.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/src/core_lens/entities/__init__.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/src/core_lens/entities/mws.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/src/core_lens/entities/tehsil.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/src/core_lens/entities/waterbody.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/src/core_lens/export/__init__.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/src/core_lens/export/formats.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/src/core_lens/py.typed +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/src/core_lens/schema/__init__.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/src/core_lens/schema/detection.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/src/core_lens/schema/profile.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/src/core_lens/utils/__init__.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/src/core_lens/utils/polars_utils.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/tests/fixtures/generate_fixtures.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/tests/unit/conftest.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/tests/unit/test_aoi.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/tests/unit/test_entities.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/tests/unit/test_entity.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/tests/unit/test_export.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/tests/unit/test_main.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/tests/unit/test_plot.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/tests/unit/test_polars_utils.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/tests/unit/test_profile.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/tests/unit/test_result.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/tests/unit/test_schema_detection.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/tests/unit/test_schema_profile.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/tests/unit/test_season.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/tests/unit/test_season_config.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/tests/unit/test_spatial.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/tests/unit/test_stats.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/tests/unit/test_view.py +0 -0
- {core_lens-0.1.dev112 → core_lens-0.1.dev114}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: core-lens
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.dev114
|
|
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
|
|
@@ -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.dev114'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 'dev114')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -78,15 +78,16 @@ def _wkb_to_arrow_table(
|
|
|
78
78
|
keep = [geom_col] + (extra_cols or [])
|
|
79
79
|
df = df.select([c for c in keep if c in df.columns])
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
81
|
+
# WKB type detection: slice byte 1 (skip endian flag) — single byte covers
|
|
82
|
+
# all standard types 1-7. bin.slice returns BinaryView in this Polars build
|
|
83
|
+
# which cannot be cast to UInt8 directly, so we extract the int value via
|
|
84
|
+
# map_elements on the already-sliced 1-byte series (no endian logic, no
|
|
85
|
+
# int.from_bytes on 4 bytes — still much cheaper than the original path).
|
|
86
|
+
geom_type_series = (
|
|
87
|
+
df[geom_col]
|
|
88
|
+
.bin.slice(1, 1)
|
|
89
|
+
.map_elements(lambda b: b[0] if b else 0, return_dtype=pl.UInt8)
|
|
90
|
+
)
|
|
90
91
|
|
|
91
92
|
# --- Fast path: pure Polygon / MultiPolygon rows (no Shapely) ---
|
|
92
93
|
poly_mask = geom_type_series.is_in([3, 6])
|
|
@@ -134,6 +135,9 @@ def _wkb_to_arrow_table(
|
|
|
134
135
|
return ac.Table.from_arrow(arrow_table)
|
|
135
136
|
|
|
136
137
|
arrow_table = df_poly.to_arrow()
|
|
138
|
+
del (
|
|
139
|
+
df_poly
|
|
140
|
+
) # free Polars WKB buffer before PyArrow combine_chunks() allocates its copy
|
|
137
141
|
wkb_col = arrow_table.column(geom_col).combine_chunks()
|
|
138
142
|
geo_col = ga.from_wkb(wkb_col)
|
|
139
143
|
|
|
@@ -376,7 +376,27 @@ class View:
|
|
|
376
376
|
from core_lens.aoi import SeasonConfig
|
|
377
377
|
|
|
378
378
|
season_cfg = self._season_config or SeasonConfig()
|
|
379
|
-
|
|
379
|
+
|
|
380
|
+
# Detect whether the time column is an integer year column so
|
|
381
|
+
# _date_range_expr can branch at expression-build time (avoids
|
|
382
|
+
# the cast-to-string + runtime when/then triple-copy).
|
|
383
|
+
try:
|
|
384
|
+
pq_schema = pl.read_parquet_schema(abs_path)
|
|
385
|
+
_time_dtype = pq_schema.get(time_col)
|
|
386
|
+
_is_year_col = _time_dtype is not None and _time_dtype in (
|
|
387
|
+
pl.Int32,
|
|
388
|
+
pl.Int64,
|
|
389
|
+
pl.UInt32,
|
|
390
|
+
pl.UInt16,
|
|
391
|
+
)
|
|
392
|
+
except Exception:
|
|
393
|
+
_is_year_col = False
|
|
394
|
+
|
|
395
|
+
# Pass as a private hint inside the filter dict (non-mutating copy).
|
|
396
|
+
_tf = dict(self.time_filter)
|
|
397
|
+
if "start" in _tf and "end" in _tf:
|
|
398
|
+
_tf["_is_year_col"] = _is_year_col
|
|
399
|
+
time_expr = resolve_time_filter(_tf, time_col, season_cfg)
|
|
380
400
|
|
|
381
401
|
lf = scan_with_key_filter(
|
|
382
402
|
path=abs_path,
|
|
@@ -47,7 +47,12 @@ def resolve_time_filter(
|
|
|
47
47
|
end = time_filter["end"]
|
|
48
48
|
assert isinstance(start, str)
|
|
49
49
|
assert isinstance(end, str)
|
|
50
|
-
|
|
50
|
+
# _is_year_col is injected by _materialise when the parquet schema is
|
|
51
|
+
# available. When absent (e.g. direct test calls) pass None so that
|
|
52
|
+
# _date_range_expr uses its runtime when/then fallback.
|
|
53
|
+
_raw = time_filter.get("_is_year_col")
|
|
54
|
+
is_year_col: bool | None = bool(_raw) if _raw is not None else None
|
|
55
|
+
return _date_range_expr(time_col, start, end, is_year_col=is_year_col)
|
|
51
56
|
|
|
52
57
|
if "season" in time_filter:
|
|
53
58
|
season_name = time_filter["season"]
|
|
@@ -64,16 +69,25 @@ def resolve_time_filter(
|
|
|
64
69
|
)
|
|
65
70
|
|
|
66
71
|
|
|
67
|
-
def _date_range_expr(
|
|
72
|
+
def _date_range_expr(
|
|
73
|
+
time_col: str,
|
|
74
|
+
start: str,
|
|
75
|
+
end: str,
|
|
76
|
+
is_year_col: bool | None = None,
|
|
77
|
+
) -> pl.Expr:
|
|
68
78
|
"""Return a Polars expression for an inclusive ISO-8601 date range.
|
|
69
79
|
|
|
70
|
-
|
|
71
|
-
|
|
80
|
+
When ``is_year_col`` is known (``True`` / ``False``) branches at *build
|
|
81
|
+
time* — no runtime copies of the column. When ``None`` (caller has no
|
|
82
|
+
schema info) falls back to a ``when/then`` that handles both integer-year
|
|
83
|
+
and Date columns at runtime.
|
|
72
84
|
|
|
73
85
|
Args:
|
|
74
86
|
time_col: Name of the time column.
|
|
75
87
|
start: ISO-8601 start date string (``"YYYY-MM-DD"``).
|
|
76
88
|
end: ISO-8601 end date string (``"YYYY-MM-DD"``).
|
|
89
|
+
is_year_col: ``True`` → integer calendar-year column; ``False`` →
|
|
90
|
+
``pl.Date`` column; ``None`` → unknown, use runtime detection.
|
|
77
91
|
|
|
78
92
|
Returns:
|
|
79
93
|
A Polars expression.
|
|
@@ -82,25 +96,25 @@ def _date_range_expr(time_col: str, start: str, end: str) -> pl.Expr:
|
|
|
82
96
|
end_date = datetime.date.fromisoformat(end)
|
|
83
97
|
col = pl.col(time_col)
|
|
84
98
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
# For date columns, cast to string yields "2020-01-01".
|
|
89
|
-
# So we compare strings directly! Lexicographical string comparison works perfectly for ISO-8601 dates and 4-digit years.
|
|
90
|
-
# For year integers, "2020" >= "2020-01-01" is False.
|
|
91
|
-
# Wait, "2020" < "2020-01-01". So if it's an integer, "2020" will not be between "2020-01-01" and "2023-12-31" because "2020" is less than "2020-01-01".
|
|
92
|
-
# To fix this, we can extract the year for the integer comparison using a regex or length check.
|
|
93
|
-
# Better: check the length of the casted string. If 4, it's a year.
|
|
94
|
-
s_col = col.cast(pl.String)
|
|
95
|
-
is_year_int = s_col.str.len_bytes() == 4
|
|
99
|
+
if is_year_col is True:
|
|
100
|
+
# Integer year column — compare year integers directly, no string cast.
|
|
101
|
+
return col.is_between(start_date.year, end_date.year)
|
|
96
102
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
start_date
|
|
100
|
-
)
|
|
101
|
-
date_expr = s_col.is_between(pl.lit(start), pl.lit(end))
|
|
103
|
+
if is_year_col is False:
|
|
104
|
+
# Date column — compare against date literals directly.
|
|
105
|
+
return col.is_between(pl.lit(start_date), pl.lit(end_date))
|
|
102
106
|
|
|
103
|
-
|
|
107
|
+
# is_year_col is None — dtype unknown (e.g. direct test call without schema).
|
|
108
|
+
# Use a runtime when/then that handles both representations.
|
|
109
|
+
s_col = col.cast(pl.String)
|
|
110
|
+
is_year_like = s_col.str.len_bytes() == 4
|
|
111
|
+
return (
|
|
112
|
+
pl.when(is_year_like)
|
|
113
|
+
.then(
|
|
114
|
+
col.cast(pl.Int32, strict=False).is_between(start_date.year, end_date.year)
|
|
115
|
+
)
|
|
116
|
+
.otherwise(col.is_between(pl.lit(start_date), pl.lit(end_date)))
|
|
117
|
+
)
|
|
104
118
|
|
|
105
119
|
|
|
106
120
|
def _season_expr(
|
|
@@ -227,17 +241,43 @@ def add_temporal_columns(
|
|
|
227
241
|
df = df.with_columns(exprs)
|
|
228
242
|
|
|
229
243
|
# --- season / season_year -----------------------------------------------
|
|
230
|
-
#
|
|
231
|
-
#
|
|
244
|
+
# Vectorized via Polars when/then on MM-DD string — no Python loop,
|
|
245
|
+
# no .to_list(), no Python date objects per row.
|
|
232
246
|
if "season" not in existing or "season_year" not in existing:
|
|
233
|
-
|
|
247
|
+
date_col = pl.col(time_col)
|
|
248
|
+
md = (
|
|
249
|
+
date_col.dt.month().cast(pl.String).str.pad_start(2, "0")
|
|
250
|
+
+ pl.lit("-")
|
|
251
|
+
+ date_col.dt.day().cast(pl.String).str.pad_start(2, "0")
|
|
252
|
+
)
|
|
253
|
+
k_start, k_end = season_config.kharif
|
|
254
|
+
r_start, r_end = season_config.rabi
|
|
255
|
+
z_start, z_end = season_config.zaid
|
|
256
|
+
|
|
257
|
+
def _md_between(expr: pl.Expr, start: str, end: str) -> pl.Expr:
|
|
258
|
+
"""True when *expr* (MM-DD string) falls within [start, end], handling year rollover."""
|
|
259
|
+
if start <= end:
|
|
260
|
+
# is_between with bare strings → column-name lookup; use pl.lit().
|
|
261
|
+
return expr.is_between(pl.lit(start), pl.lit(end))
|
|
262
|
+
# Year-crossing: e.g. rabi 11-01 → 03-31
|
|
263
|
+
return (expr >= pl.lit(start)) | (expr <= pl.lit(end))
|
|
264
|
+
|
|
265
|
+
season_expr = (
|
|
266
|
+
pl.when(_md_between(md, k_start, k_end))
|
|
267
|
+
.then(pl.lit("kharif"))
|
|
268
|
+
.when(_md_between(md, r_start, r_end))
|
|
269
|
+
.then(pl.lit("rabi"))
|
|
270
|
+
.otherwise(pl.lit("zaid"))
|
|
271
|
+
.alias("season")
|
|
272
|
+
)
|
|
273
|
+
|
|
234
274
|
if "season" not in existing:
|
|
235
|
-
df = df.with_columns(
|
|
275
|
+
df = df.with_columns(season_expr)
|
|
236
276
|
if "season_year" not in existing:
|
|
237
|
-
years = df["year"].to_list()
|
|
238
|
-
season_year_labels = [f"{s}_{y}" for s, y in zip(season_labels, years)]
|
|
239
277
|
df = df.with_columns(
|
|
240
|
-
pl.
|
|
278
|
+
(pl.col("season") + pl.lit("_") + pl.col("year").cast(pl.String)).alias(
|
|
279
|
+
"season_year"
|
|
280
|
+
)
|
|
241
281
|
)
|
|
242
282
|
|
|
243
283
|
return df
|
|
@@ -230,6 +230,8 @@ def exact_spatial_filter(
|
|
|
230
230
|
if inter_area / entity_area >= threshold:
|
|
231
231
|
hit_indices.append(i)
|
|
232
232
|
|
|
233
|
+
del geoms # free all decoded Shapely geometries before assembling result
|
|
234
|
+
|
|
233
235
|
matched: pl.DataFrame = full_df[hit_indices].select(key_cols)
|
|
234
236
|
return matched
|
|
235
237
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|