core-lens 0.1.dev129__tar.gz → 0.1.dev132__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.dev129 → core_lens-0.1.dev132}/PKG-INFO +1 -1
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/src/core_lens/_version.py +2 -2
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/src/core_lens/aoi.py +25 -2
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/src/core_lens/base/result.py +4 -2
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/src/core_lens/base/view.py +14 -24
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/src/core_lens/export/formats.py +20 -32
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/src/core_lens/schema/detection.py +36 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/src/core_lens/schema/profile.py +6 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/src/core_lens/utils/spatial.py +28 -17
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/tests/unit/test_export.py +5 -2
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/.github/ISSUE_TEMPLATE/blank-proposal.yaml +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/.github/ISSUE_TEMPLATE/bug-report.yaml +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/.github/ISSUE_TEMPLATE/feature-request.yaml +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/.github/pull_request_template.md +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/.github/workflows/ci.yml +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/.github/workflows/gh-pages.yml +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/.github/workflows/pre-release.yml +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/.github/workflows/release.yml +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/.gitignore +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/.gitmessage +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/.pre-commit-config.yaml +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/.python-version +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/CONTRIBUTING.md +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/LICENSE +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/README.md +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/SKILLS.md +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/benchmarks/README.md +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/benchmarks/bench_aoi.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/benchmarks/bench_export.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/benchmarks/bench_polars_utils.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/benchmarks/bench_result.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/benchmarks/bench_schema.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/benchmarks/bench_season.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/benchmarks/bench_spatial.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/benchmarks/bench_view.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/benchmarks/run_all.sh +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/docs/Makefile +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/docs/make.bat +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/docs/source/concepts.md +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/docs/source/conf.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/docs/source/export.md +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/docs/source/index.rst +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/docs/source/intro.md +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/docs/source/logging.md +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/docs/source/plots.md +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/docs/source/plugins.md +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/docs/source/queries.md +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/docs/source/quickstart.md +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/docs/source/stats.md +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/examples/demo_mws.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/examples/demo_tehsil.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/hooks/mypy.sh +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/hooks/no-parquet-outside-fixtures.sh +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/hooks/pytest.sh +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/pyproject.toml +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/src/core_lens/__init__.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/src/core_lens/__main__.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/src/core_lens/base/__init__.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/src/core_lens/base/entity.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/src/core_lens/base/namespaces/__init__.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/src/core_lens/base/namespaces/plot.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/src/core_lens/base/namespaces/stats.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/src/core_lens/entities/__init__.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/src/core_lens/entities/mws.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/src/core_lens/entities/tehsil.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/src/core_lens/entities/waterbody.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/src/core_lens/export/__init__.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/src/core_lens/py.typed +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/src/core_lens/schema/__init__.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/src/core_lens/utils/__init__.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/src/core_lens/utils/paths.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/src/core_lens/utils/polars_utils.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/src/core_lens/utils/season.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/tests/fixtures/generate_fixtures.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/tests/unit/conftest.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/tests/unit/test_aoi.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/tests/unit/test_entities.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/tests/unit/test_entity.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/tests/unit/test_main.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/tests/unit/test_plot.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/tests/unit/test_polars_utils.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/tests/unit/test_profile.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/tests/unit/test_result.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/tests/unit/test_schema_detection.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/tests/unit/test_schema_profile.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/tests/unit/test_season.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/tests/unit/test_season_config.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/tests/unit/test_spatial.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/tests/unit/test_stats.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/tests/unit/test_view.py +0 -0
- {core_lens-0.1.dev129 → core_lens-0.1.dev132}/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.dev132
|
|
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.dev132'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 'dev132')
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -22,7 +22,7 @@ if TYPE_CHECKING:
|
|
|
22
22
|
from core_lens.base.view import View
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
@dataclass
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
26
|
class SeasonConfig:
|
|
27
27
|
"""Date-range definitions for the three Indian crop seasons.
|
|
28
28
|
|
|
@@ -96,6 +96,29 @@ def _md_in_range(md: str, start: str, end: str) -> bool:
|
|
|
96
96
|
return md >= start or md <= end
|
|
97
97
|
|
|
98
98
|
|
|
99
|
+
# Singleton for the default SeasonConfig — constructed once per process.
|
|
100
|
+
# SeasonConfig.__post_init__ runs 6× datetime.strptime calls for validation;
|
|
101
|
+
# sharing the default instance eliminates that cost for every AoI() call
|
|
102
|
+
# that does not supply a custom season config.
|
|
103
|
+
_DEFAULT_SEASON_CONFIG: SeasonConfig | None = None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _default_season_config() -> SeasonConfig:
|
|
107
|
+
"""Return the shared default :class:`SeasonConfig` instance.
|
|
108
|
+
|
|
109
|
+
Constructed once per process and reused thereafter. Callers that need
|
|
110
|
+
a custom config should construct their own :class:`SeasonConfig` and
|
|
111
|
+
pass it to :class:`AoI`.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
SeasonConfig: The shared default instance.
|
|
115
|
+
"""
|
|
116
|
+
global _DEFAULT_SEASON_CONFIG
|
|
117
|
+
if _DEFAULT_SEASON_CONFIG is None:
|
|
118
|
+
_DEFAULT_SEASON_CONFIG = SeasonConfig()
|
|
119
|
+
return _DEFAULT_SEASON_CONFIG
|
|
120
|
+
|
|
121
|
+
|
|
99
122
|
# Class-level registry: entity name → entity **class**.
|
|
100
123
|
# Shared across all AoI instances in a process. Explicit registration is
|
|
101
124
|
# required; there is no auto-discovery (design §6.1).
|
|
@@ -191,7 +214,7 @@ class AoI:
|
|
|
191
214
|
self.data_root = pathlib.Path(data_root_str).resolve()
|
|
192
215
|
|
|
193
216
|
self._storage_options: dict[str, Any] = storage_options or {}
|
|
194
|
-
self.seasons: SeasonConfig = seasons or
|
|
217
|
+
self.seasons: SeasonConfig = seasons or _default_season_config()
|
|
195
218
|
logger.info("Initializing AoI with data_root={}", self.data_root)
|
|
196
219
|
|
|
197
220
|
n_modes = sum(
|
|
@@ -100,10 +100,12 @@ class Result:
|
|
|
100
100
|
"Call .with_geometry() first to join the static geometry column before extracting GeoDataFrame."
|
|
101
101
|
)
|
|
102
102
|
import geopandas as gpd
|
|
103
|
-
import shapely
|
|
103
|
+
import shapely
|
|
104
104
|
|
|
105
105
|
geometry_col = self.entity.geometry_col
|
|
106
|
-
|
|
106
|
+
# Vectorised C-level decode — shapely.from_wkb operates on the whole
|
|
107
|
+
# numpy array at once, avoiding a Python-loop per row.
|
|
108
|
+
geometries = shapely.from_wkb(self.data[geometry_col].to_numpy())
|
|
107
109
|
geo_series = gpd.GeoSeries(geometries, crs="EPSG:4326")
|
|
108
110
|
return gpd.GeoDataFrame(
|
|
109
111
|
self.data.drop(geometry_col).to_pandas(),
|
|
@@ -407,28 +407,18 @@ class View:
|
|
|
407
407
|
if time_col is not None:
|
|
408
408
|
from core_lens.utils.season import resolve_time_filter
|
|
409
409
|
|
|
410
|
-
# Fall back to
|
|
411
|
-
from core_lens.aoi import
|
|
412
|
-
|
|
413
|
-
season_cfg = self._season_config or
|
|
414
|
-
|
|
415
|
-
#
|
|
416
|
-
#
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
).collect_schema()
|
|
423
|
-
_time_dtype = pq_schema.get(time_col)
|
|
424
|
-
_is_year_col = _time_dtype is not None and _time_dtype in (
|
|
425
|
-
pl.Int32,
|
|
426
|
-
pl.Int64,
|
|
427
|
-
pl.UInt32,
|
|
428
|
-
pl.UInt16,
|
|
429
|
-
)
|
|
430
|
-
except Exception:
|
|
431
|
-
_is_year_col = False
|
|
410
|
+
# Fall back to the default SeasonConfig if none was supplied on this View.
|
|
411
|
+
from core_lens.aoi import _default_season_config
|
|
412
|
+
|
|
413
|
+
season_cfg = self._season_config or _default_season_config()
|
|
414
|
+
|
|
415
|
+
# Read is_year_col from the cached schema profile — populated at
|
|
416
|
+
# detect() time from the Parquet schema, so no extra I/O here.
|
|
417
|
+
_is_year_col: bool | None = (
|
|
418
|
+
profile.annual_is_year_col
|
|
419
|
+
if resolution == Resolution.ANNUAL
|
|
420
|
+
else profile.fortnightly_is_year_col
|
|
421
|
+
)
|
|
432
422
|
|
|
433
423
|
# Pass as a private hint inside the filter dict (non-mutating copy).
|
|
434
424
|
_tf = dict(self.time_filter)
|
|
@@ -454,9 +444,9 @@ class View:
|
|
|
454
444
|
and profile.fortnightly_time_col is not None
|
|
455
445
|
):
|
|
456
446
|
from core_lens.utils.season import add_temporal_columns
|
|
457
|
-
from core_lens.aoi import
|
|
447
|
+
from core_lens.aoi import _default_season_config
|
|
458
448
|
|
|
459
|
-
season_cfg = self._season_config or
|
|
449
|
+
season_cfg = self._season_config or _default_season_config()
|
|
460
450
|
data = add_temporal_columns(data, profile.fortnightly_time_col, season_cfg)
|
|
461
451
|
|
|
462
452
|
if self.join_spec is not None:
|
|
@@ -162,12 +162,17 @@ def geoparquet(result: "Result", path: str | pathlib.Path, **kwargs: Any) -> Non
|
|
|
162
162
|
def geojson(result: "Result", path: str | pathlib.Path, **kwargs: Any) -> None:
|
|
163
163
|
"""Export the result data to a GeoJSON file.
|
|
164
164
|
|
|
165
|
-
This function uses
|
|
165
|
+
This function uses GeoPandas to write the data frame. It prefers the
|
|
166
|
+
``pyogrio`` engine (faster, C-backed) when available and falls back to
|
|
167
|
+
``fiona`` automatically. The ``engine`` kwarg can override this:
|
|
168
|
+
``engine="fiona"`` or ``engine="pyogrio"``.
|
|
166
169
|
|
|
167
170
|
Args:
|
|
168
171
|
result (Result): The Result object to export.
|
|
169
172
|
path (str | pathlib.Path): Destination path for the GeoJSON file.
|
|
170
|
-
**kwargs (Any): Additional options
|
|
173
|
+
**kwargs (Any): Additional options forwarded to
|
|
174
|
+
``GeoDataFrame.to_file()`` (e.g. ``driver="GeoJSON"``,
|
|
175
|
+
``engine="pyogrio"``).
|
|
171
176
|
|
|
172
177
|
Example:
|
|
173
178
|
>>> from core_lens.export import geojson
|
|
@@ -182,37 +187,20 @@ def geojson(result: "Result", path: str | pathlib.Path, **kwargs: Any) -> None:
|
|
|
182
187
|
"Call .with_geometry() first to join the static geometry column before exporting to geojson."
|
|
183
188
|
)
|
|
184
189
|
|
|
185
|
-
|
|
186
|
-
geom_col = result.entity.geometry_col
|
|
187
|
-
|
|
188
|
-
cols = []
|
|
189
|
-
for c in df.columns:
|
|
190
|
-
if c == geom_col:
|
|
191
|
-
cols.append(f'ST_GeomFromWKB("{c}") AS "{c}"')
|
|
192
|
-
else:
|
|
193
|
-
cols.append(f'"{c}"')
|
|
194
|
-
|
|
195
|
-
select_clause = ", ".join(cols)
|
|
196
|
-
|
|
197
|
-
conn = _get_duckdb_conn()
|
|
190
|
+
gdf = result.gdf()
|
|
198
191
|
|
|
199
|
-
|
|
192
|
+
# Resolve write engine: prefer pyogrio (C-backed, no GDAL Python overhead),
|
|
193
|
+
# fall back to fiona if not installed.
|
|
194
|
+
engine = kwargs.pop("engine", None)
|
|
195
|
+
if engine is None:
|
|
196
|
+
try:
|
|
197
|
+
import pyogrio as _ # type: ignore[import-untyped] # noqa: F401
|
|
200
198
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
options.append(f"{k.upper()} {'TRUE' if v else 'FALSE'}")
|
|
205
|
-
elif isinstance(v, str):
|
|
206
|
-
options.append(f"{k.upper()} '{v}'")
|
|
207
|
-
else:
|
|
208
|
-
options.append(f"{k.upper()} {v}")
|
|
199
|
+
engine = "pyogrio"
|
|
200
|
+
except ModuleNotFoundError:
|
|
201
|
+
engine = "fiona"
|
|
209
202
|
|
|
210
|
-
|
|
203
|
+
# Always write as GeoJSON unless the caller overrides driver.
|
|
204
|
+
kwargs.setdefault("driver", "GeoJSON")
|
|
211
205
|
|
|
212
|
-
|
|
213
|
-
COPY (
|
|
214
|
-
SELECT {select_clause}
|
|
215
|
-
FROM df
|
|
216
|
-
) TO '{path_str}' ({options_str});
|
|
217
|
-
"""
|
|
218
|
-
conn.execute(query)
|
|
206
|
+
gdf.to_file(path, engine=engine, **kwargs)
|
|
@@ -104,6 +104,7 @@ def detect(
|
|
|
104
104
|
|
|
105
105
|
annual_time_col: str | None = None
|
|
106
106
|
extra_annual_cols: list[str] = []
|
|
107
|
+
annual_is_year_col: bool | None = None
|
|
107
108
|
if annual_path:
|
|
108
109
|
annual_schema = _read_schema(
|
|
109
110
|
annual_path, label="annual", storage_options=_so or None
|
|
@@ -113,9 +114,11 @@ def detect(
|
|
|
113
114
|
{annual_time_col} if annual_time_col else set()
|
|
114
115
|
)
|
|
115
116
|
extra_annual_cols = [c for c in annual_schema if c not in reserved_annual]
|
|
117
|
+
annual_is_year_col = _is_year_col_from_schema(annual_schema, annual_time_col)
|
|
116
118
|
|
|
117
119
|
fortnightly_time_col: str | None = None
|
|
118
120
|
extra_fortnightly_cols: list[str] = []
|
|
121
|
+
fortnightly_is_year_col: bool | None = None
|
|
119
122
|
if fortnightly_path:
|
|
120
123
|
fortnightly_schema = _read_schema(
|
|
121
124
|
fortnightly_path, label="fortnightly", storage_options=_so or None
|
|
@@ -125,6 +128,9 @@ def detect(
|
|
|
125
128
|
{fortnightly_time_col} if fortnightly_time_col else set()
|
|
126
129
|
)
|
|
127
130
|
extra_fortnightly_cols = [c for c in fortnightly_schema if c not in reserved_fn]
|
|
131
|
+
fortnightly_is_year_col = _is_year_col_from_schema(
|
|
132
|
+
fortnightly_schema, fortnightly_time_col
|
|
133
|
+
)
|
|
128
134
|
|
|
129
135
|
return SchemaProfile(
|
|
130
136
|
key_cols=key_cols,
|
|
@@ -136,6 +142,8 @@ def detect(
|
|
|
136
142
|
extra_static_cols=extra_static_cols,
|
|
137
143
|
extra_annual_cols=extra_annual_cols,
|
|
138
144
|
extra_fortnightly_cols=extra_fortnightly_cols,
|
|
145
|
+
annual_is_year_col=annual_is_year_col,
|
|
146
|
+
fortnightly_is_year_col=fortnightly_is_year_col,
|
|
139
147
|
)
|
|
140
148
|
|
|
141
149
|
|
|
@@ -237,3 +245,31 @@ def _infer_time_col(schema: pl.Schema, path: str) -> str | None:
|
|
|
237
245
|
return col
|
|
238
246
|
|
|
239
247
|
return None
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
_YEAR_COL_DTYPES = (pl.Int32, pl.Int64, pl.UInt32, pl.UInt16)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _is_year_col_from_schema(
|
|
254
|
+
schema: pl.Schema,
|
|
255
|
+
time_col: str | None,
|
|
256
|
+
) -> bool | None:
|
|
257
|
+
"""Return whether *time_col* is an integer-year column, or ``None`` if absent.
|
|
258
|
+
|
|
259
|
+
``True`` — integer dtype (``Int32`` / ``Int64`` / ``UInt32`` / ``UInt16``).
|
|
260
|
+
``False`` — ``Date`` / ``Datetime`` column.
|
|
261
|
+
``None`` — *time_col* is ``None`` or not found in *schema*.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
schema (pl.Schema): The Parquet schema returned by ``collect_schema()``.
|
|
265
|
+
time_col (str | None): The name of the time column, or ``None``.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
bool | None: ``True`` for integer-year, ``False`` for Date/Datetime, ``None`` if absent.
|
|
269
|
+
"""
|
|
270
|
+
if time_col is None:
|
|
271
|
+
return None
|
|
272
|
+
dtype = schema.get(time_col)
|
|
273
|
+
if dtype is None:
|
|
274
|
+
return None
|
|
275
|
+
return dtype in _YEAR_COL_DTYPES
|
|
@@ -69,6 +69,12 @@ class SchemaProfile(BaseModel):
|
|
|
69
69
|
extra_static_cols: list[str] = []
|
|
70
70
|
extra_annual_cols: list[str] = []
|
|
71
71
|
extra_fortnightly_cols: list[str] = []
|
|
72
|
+
# Whether the time column is an integer-year column (True), a Date/Datetime
|
|
73
|
+
# column (False), or absent/unknown (None). Determined at detect() time from
|
|
74
|
+
# the Parquet schema — eliminates a redundant collect_schema() call in
|
|
75
|
+
# View._materialise for every .annual / .fortnightly access.
|
|
76
|
+
annual_is_year_col: bool | None = None
|
|
77
|
+
fortnightly_is_year_col: bool | None = None
|
|
72
78
|
|
|
73
79
|
@classmethod
|
|
74
80
|
def from_file(cls, path: str | pathlib.Path) -> "SchemaProfile":
|
|
@@ -247,28 +247,39 @@ def exact_spatial_filter(
|
|
|
247
247
|
else:
|
|
248
248
|
geoms = shapely.from_wkt(geom_array)
|
|
249
249
|
|
|
250
|
+
import numpy as np
|
|
251
|
+
|
|
250
252
|
if relationship == "centroid":
|
|
251
253
|
# Centroid mode: entity centroid must lie within the AoI geometry.
|
|
252
|
-
# STRtree
|
|
253
|
-
#
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
254
|
+
# STRtree is overkill for a single-query containment test — building
|
|
255
|
+
# the tree costs O(n log n) with no payoff when there is only one query
|
|
256
|
+
# geometry. Vectorised shapely ops stay in C and avoid the overhead.
|
|
257
|
+
centroids = shapely.centroid(geoms) # vectorised, C-level
|
|
258
|
+
mask = shapely.contains(aoi_geometry, centroids) # vectorised bool array
|
|
259
|
+
hit_indices = np.where(mask)[0].tolist()
|
|
257
260
|
|
|
258
261
|
else: # area mode
|
|
259
|
-
# Area mode: intersection area / entity area
|
|
262
|
+
# Area mode: intersection area / entity area >= threshold.
|
|
263
|
+
# Vectorise: compute all areas and intersections in C via numpy arrays.
|
|
260
264
|
tree = shapely.STRtree(geoms)
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
if
|
|
271
|
-
hit_indices
|
|
265
|
+
intersect_idx = np.asarray(
|
|
266
|
+
tree.query(aoi_geometry, predicate="intersects"), dtype=np.intp
|
|
267
|
+
)
|
|
268
|
+
if len(intersect_idx) == 0:
|
|
269
|
+
hit_indices = []
|
|
270
|
+
else:
|
|
271
|
+
candidate_geoms = geoms[intersect_idx]
|
|
272
|
+
entity_areas = shapely.area(candidate_geoms) # vectorised
|
|
273
|
+
valid = entity_areas > 0
|
|
274
|
+
if not valid.any():
|
|
275
|
+
hit_indices = []
|
|
276
|
+
else:
|
|
277
|
+
inter_geoms = shapely.intersection(
|
|
278
|
+
candidate_geoms[valid], aoi_geometry
|
|
279
|
+
) # vectorised
|
|
280
|
+
inter_areas = shapely.area(inter_geoms) # vectorised
|
|
281
|
+
ratios = inter_areas / entity_areas[valid]
|
|
282
|
+
hit_indices = intersect_idx[valid][ratios >= threshold].tolist()
|
|
272
283
|
|
|
273
284
|
del geoms # free all decoded Shapely geometries before assembling result
|
|
274
285
|
|
|
@@ -152,8 +152,10 @@ def test_geoparquet_options_partition(
|
|
|
152
152
|
|
|
153
153
|
|
|
154
154
|
def test_geojson_options(sample_result: Result, tmp_path: pathlib.Path) -> None:
|
|
155
|
+
"""Unknown kwargs are forwarded to the write engine; unrecognised ones raise
|
|
156
|
+
ValueError (pyogrio) rather than silently being ignored. The test simply
|
|
157
|
+
verifies the call path is exercised without an unexpected exception type."""
|
|
155
158
|
result_with_geom = sample_result.with_geometry()
|
|
156
|
-
import duckdb
|
|
157
159
|
|
|
158
160
|
out_path = tmp_path / "out_opts.json"
|
|
159
161
|
try:
|
|
@@ -165,5 +167,6 @@ def test_geojson_options(sample_result: Result, tmp_path: pathlib.Path) -> None:
|
|
|
165
167
|
flag=False,
|
|
166
168
|
number=10,
|
|
167
169
|
)
|
|
168
|
-
except
|
|
170
|
+
except ValueError:
|
|
171
|
+
# pyogrio rejects unrecognised driver options — that's expected.
|
|
169
172
|
pass
|
|
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
|
|
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
|