core-lens 0.1.dev130__tar.gz → 0.1.dev133__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.dev130 → core_lens-0.1.dev133}/PKG-INFO +1 -1
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/src/core_lens/_version.py +2 -2
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/src/core_lens/aoi.py +25 -2
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/src/core_lens/base/entity.py +128 -15
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/src/core_lens/base/view.py +14 -24
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/src/core_lens/schema/detection.py +36 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/src/core_lens/schema/profile.py +6 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/src/core_lens/utils/spatial.py +28 -17
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/.github/ISSUE_TEMPLATE/blank-proposal.yaml +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/.github/ISSUE_TEMPLATE/bug-report.yaml +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/.github/ISSUE_TEMPLATE/feature-request.yaml +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/.github/pull_request_template.md +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/.github/workflows/ci.yml +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/.github/workflows/gh-pages.yml +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/.github/workflows/pre-release.yml +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/.github/workflows/release.yml +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/.gitignore +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/.gitmessage +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/.pre-commit-config.yaml +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/.python-version +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/CONTRIBUTING.md +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/LICENSE +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/README.md +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/SKILLS.md +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/benchmarks/README.md +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/benchmarks/bench_aoi.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/benchmarks/bench_export.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/benchmarks/bench_polars_utils.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/benchmarks/bench_result.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/benchmarks/bench_schema.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/benchmarks/bench_season.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/benchmarks/bench_spatial.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/benchmarks/bench_view.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/benchmarks/run_all.sh +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/docs/Makefile +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/docs/make.bat +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/docs/source/concepts.md +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/docs/source/conf.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/docs/source/export.md +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/docs/source/index.rst +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/docs/source/intro.md +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/docs/source/logging.md +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/docs/source/plots.md +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/docs/source/plugins.md +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/docs/source/queries.md +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/docs/source/quickstart.md +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/docs/source/stats.md +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/examples/demo_mws.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/examples/demo_tehsil.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/hooks/mypy.sh +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/hooks/no-parquet-outside-fixtures.sh +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/hooks/pytest.sh +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/pyproject.toml +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/src/core_lens/__init__.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/src/core_lens/__main__.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/src/core_lens/base/__init__.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/src/core_lens/base/namespaces/__init__.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/src/core_lens/base/namespaces/plot.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/src/core_lens/base/namespaces/stats.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/src/core_lens/base/result.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/src/core_lens/entities/__init__.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/src/core_lens/entities/mws.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/src/core_lens/entities/tehsil.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/src/core_lens/entities/waterbody.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/src/core_lens/export/__init__.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/src/core_lens/export/formats.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/src/core_lens/py.typed +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/src/core_lens/schema/__init__.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/src/core_lens/utils/__init__.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/src/core_lens/utils/paths.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/src/core_lens/utils/polars_utils.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/src/core_lens/utils/season.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/tests/fixtures/generate_fixtures.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/tests/unit/conftest.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/tests/unit/test_aoi.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/tests/unit/test_entities.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/tests/unit/test_entity.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/tests/unit/test_export.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/tests/unit/test_main.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/tests/unit/test_plot.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/tests/unit/test_polars_utils.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/tests/unit/test_profile.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/tests/unit/test_result.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/tests/unit/test_schema_detection.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/tests/unit/test_schema_profile.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/tests/unit/test_season.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/tests/unit/test_season_config.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/tests/unit/test_spatial.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/tests/unit/test_stats.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/tests/unit/test_view.py +0 -0
- {core_lens-0.1.dev130 → core_lens-0.1.dev133}/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.dev133
|
|
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.dev133'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 'dev133')
|
|
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(
|
|
@@ -11,6 +11,7 @@ Plugin authors import from the public surface::
|
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
|
+
import functools
|
|
14
15
|
import pathlib
|
|
15
16
|
from abc import ABC, abstractmethod
|
|
16
17
|
from typing import TYPE_CHECKING, Any
|
|
@@ -35,6 +36,96 @@ if TYPE_CHECKING:
|
|
|
35
36
|
from core_lens.schema.profile import SchemaProfile
|
|
36
37
|
|
|
37
38
|
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
# Process-level caches — shared across all BaseEntity instances in a process.
|
|
41
|
+
#
|
|
42
|
+
# Both caches are backed by functools.cache (lru_cache with maxsize=None).
|
|
43
|
+
# maxsize=None means unbounded — intentional: evicting an index or schema
|
|
44
|
+
# profile forces an expensive rebuild on the next access.
|
|
45
|
+
#
|
|
46
|
+
# _cached_detect eliminates repeated detect() calls (OPT-2).
|
|
47
|
+
# _cached_build_index eliminates repeated build_bbox_index() calls (OPT-1).
|
|
48
|
+
#
|
|
49
|
+
# Cache diagnostics: _cached_detect.cache_info() / _cached_build_index.cache_info()
|
|
50
|
+
# Cache reset (tests): _cached_detect.cache_clear() / _cached_build_index.cache_clear()
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _so_key(storage_options: dict[str, Any]) -> tuple[tuple[str, Any], ...]:
|
|
55
|
+
"""Convert a ``storage_options`` dict to a hashable, order-stable tuple.
|
|
56
|
+
|
|
57
|
+
All args passed to :func:`_cached_detect` and :func:`_cached_build_index`
|
|
58
|
+
must be hashable (``functools.cache`` requirement). ``storage_options``
|
|
59
|
+
is a plain dict, so we convert it here before passing to the cached
|
|
60
|
+
functions. Different cloud credentials for the same path produce
|
|
61
|
+
different cache keys.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
storage_options (dict[str, Any]): Cloud credential / configuration dict.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
tuple[tuple[str, Any], ...]: A sorted tuple of ``(key, value)`` pairs.
|
|
68
|
+
"""
|
|
69
|
+
return tuple(sorted(storage_options.items()))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@functools.cache
|
|
73
|
+
def _cached_detect(
|
|
74
|
+
static_path: str,
|
|
75
|
+
key_cols: tuple[str, ...],
|
|
76
|
+
geometry_col: str,
|
|
77
|
+
annual_path: str | None,
|
|
78
|
+
fortnightly_path: str | None,
|
|
79
|
+
storage_options_key: tuple[tuple[str, Any], ...],
|
|
80
|
+
) -> "SchemaProfile":
|
|
81
|
+
"""Cached wrapper around :func:`~core_lens.schema.detection.detect`.
|
|
82
|
+
|
|
83
|
+
All arguments must be hashable so they can form the cache key. Pass
|
|
84
|
+
``storage_options`` through :func:`_so_key` before calling this function.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
SchemaProfile: Detected schema profile for the given entity paths.
|
|
88
|
+
"""
|
|
89
|
+
from core_lens.schema.detection import detect
|
|
90
|
+
|
|
91
|
+
return detect(
|
|
92
|
+
static_path=static_path,
|
|
93
|
+
key_cols=list(key_cols),
|
|
94
|
+
geometry_col=geometry_col,
|
|
95
|
+
annual_path=annual_path,
|
|
96
|
+
fortnightly_path=fortnightly_path,
|
|
97
|
+
storage_options=dict(storage_options_key) or None,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@functools.cache
|
|
102
|
+
def _cached_build_index(
|
|
103
|
+
static_path: str,
|
|
104
|
+
key_cols: tuple[str, ...],
|
|
105
|
+
bbox_cols: tuple[str, str, str, str] | None,
|
|
106
|
+
geometry_col: str,
|
|
107
|
+
geometry_type: str,
|
|
108
|
+
storage_options_key: tuple[tuple[str, Any], ...],
|
|
109
|
+
) -> pl.DataFrame:
|
|
110
|
+
"""Cached wrapper around :func:`~core_lens.utils.spatial.build_bbox_index`.
|
|
111
|
+
|
|
112
|
+
All arguments must be hashable so they can form the cache key. Pass
|
|
113
|
+
``storage_options`` through :func:`_so_key` before calling this function.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
pl.DataFrame: The bounding-box index DataFrame.
|
|
117
|
+
"""
|
|
118
|
+
logger.debug("Building bbox index for {} (cache miss)", static_path)
|
|
119
|
+
return build_bbox_index(
|
|
120
|
+
static_path=static_path,
|
|
121
|
+
key_cols=list(key_cols),
|
|
122
|
+
bbox_cols=bbox_cols,
|
|
123
|
+
geometry_col=geometry_col,
|
|
124
|
+
geometry_type=geometry_type,
|
|
125
|
+
storage_options=dict(storage_options_key) or None,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
38
129
|
class BaseEntity(ABC):
|
|
39
130
|
"""Abstract base class for every entity in the core_lens plugin system.
|
|
40
131
|
|
|
@@ -228,19 +319,29 @@ class BaseEntity(ABC):
|
|
|
228
319
|
def schema_profile(self) -> "SchemaProfile":
|
|
229
320
|
"""Validated schema descriptor for this entity's data files.
|
|
230
321
|
|
|
231
|
-
Auto-detected from Parquet file metadata on first access
|
|
232
|
-
|
|
233
|
-
|
|
322
|
+
Auto-detected from Parquet file metadata on first access. The result
|
|
323
|
+
is cached at two levels:
|
|
324
|
+
|
|
325
|
+
1. **Instance level** — ``_schema_profile`` attribute set on ``self``
|
|
326
|
+
so that repeated property access within the same instance is a bare
|
|
327
|
+
attribute lookup with no dict overhead.
|
|
328
|
+
2. **Process level** — :func:`_cached_detect` (backed by
|
|
329
|
+
``functools.cache``) keyed on the resolved paths and entity
|
|
330
|
+
configuration. Different ``AoI()`` calls that point at the same
|
|
331
|
+
data directory share one ``SchemaProfile`` object, avoiding
|
|
332
|
+
redundant ``collect_schema()`` I/O.
|
|
333
|
+
|
|
334
|
+
Override in subclasses to provide an explicit profile instead of
|
|
335
|
+
relying on detection.
|
|
234
336
|
|
|
235
337
|
Returns:
|
|
236
338
|
SchemaProfile: A fully-validated :class:`~core_lens.schema.profile.SchemaProfile`.
|
|
237
339
|
"""
|
|
238
340
|
if not hasattr(self, "_schema_profile"):
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
self._schema_profile: SchemaProfile = detect(
|
|
341
|
+
_so = self._storage_options or {}
|
|
342
|
+
self._schema_profile: SchemaProfile = _cached_detect(
|
|
242
343
|
static_path=self._resolve(self.static_path),
|
|
243
|
-
key_cols=self.key_cols,
|
|
344
|
+
key_cols=tuple(self.key_cols),
|
|
244
345
|
geometry_col=self.geometry_col,
|
|
245
346
|
annual_path=(
|
|
246
347
|
self._resolve(self.annual_path)
|
|
@@ -252,24 +353,36 @@ class BaseEntity(ABC):
|
|
|
252
353
|
if self.fortnightly_path is not None
|
|
253
354
|
else None
|
|
254
355
|
),
|
|
255
|
-
|
|
356
|
+
storage_options_key=_so_key(_so),
|
|
256
357
|
)
|
|
257
358
|
return self._schema_profile
|
|
258
359
|
|
|
259
360
|
@property
|
|
260
361
|
def _index(self) -> pl.DataFrame:
|
|
362
|
+
"""In-memory bounding-box index for spatial pre-filtering.
|
|
363
|
+
|
|
364
|
+
Cached at two levels (same strategy as :attr:`schema_profile`):
|
|
365
|
+
|
|
366
|
+
1. **Instance level** — ``_cached_index`` attribute on ``self``.
|
|
367
|
+
2. **Process level** — :func:`_cached_build_index` (backed by
|
|
368
|
+
``functools.cache``) keyed on the resolved static path, key
|
|
369
|
+
columns, and bbox/geometry schema. All entity instances that
|
|
370
|
+
resolve to the same static file share one index ``pl.DataFrame``,
|
|
371
|
+
eliminating repeated geometry decodes.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
pl.DataFrame: DataFrame with columns ``(*key_cols, minx, miny, maxx, maxy)``.
|
|
375
|
+
"""
|
|
261
376
|
if not hasattr(self, "_cached_index"):
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
profile = self.schema_profile
|
|
266
|
-
self._cached_index: pl.DataFrame = build_bbox_index(
|
|
377
|
+
profile = self.schema_profile # already cached — no extra I/O
|
|
378
|
+
_so = self._storage_options or {}
|
|
379
|
+
self._cached_index: pl.DataFrame = _cached_build_index(
|
|
267
380
|
static_path=self._resolve(self.static_path),
|
|
268
|
-
key_cols=self.key_cols,
|
|
381
|
+
key_cols=tuple(self.key_cols),
|
|
269
382
|
bbox_cols=profile.bbox_cols,
|
|
270
383
|
geometry_col=profile.geometry_col,
|
|
271
384
|
geometry_type=profile.geometry_type,
|
|
272
|
-
|
|
385
|
+
storage_options_key=_so_key(_so),
|
|
273
386
|
)
|
|
274
387
|
return self._cached_index
|
|
275
388
|
|
|
@@ -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:
|
|
@@ -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
|
|
|
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
|
|
File without changes
|
|
File without changes
|