core-lens 0.1.dev117__tar.gz → 0.1.dev121__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.
Files changed (77) hide show
  1. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/PKG-INFO +8 -1
  2. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/docs/source/index.rst +1 -0
  3. core_lens-0.1.dev121/docs/source/logging.md +32 -0
  4. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/examples/demo_mws.py +4 -3
  5. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/examples/demo_tehsil.py +6 -5
  6. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/pyproject.toml +6 -1
  7. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/src/core_lens/__init__.py +4 -0
  8. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/src/core_lens/_version.py +2 -2
  9. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/src/core_lens/aoi.py +50 -0
  10. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/src/core_lens/base/entity.py +36 -0
  11. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/src/core_lens/base/namespaces/plot.py +12 -7
  12. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/src/core_lens/base/view.py +34 -0
  13. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/src/core_lens/utils/spatial.py +30 -0
  14. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/tests/unit/test_entity.py +11 -3
  15. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/uv.lock +251 -251
  16. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/.github/pull_request_template.md +0 -0
  17. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/.github/workflows/ci.yml +0 -0
  18. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/.github/workflows/gh-pages.yml +0 -0
  19. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/.github/workflows/pre-release.yml +0 -0
  20. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/.github/workflows/release.yml +0 -0
  21. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/.gitignore +0 -0
  22. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/.gitmessage +0 -0
  23. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/.pre-commit-config.yaml +0 -0
  24. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/.python-version +0 -0
  25. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/CONTRIBUTING.md +0 -0
  26. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/LICENSE +0 -0
  27. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/README.md +0 -0
  28. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/SKILLS.md +0 -0
  29. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/docs/Makefile +0 -0
  30. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/docs/make.bat +0 -0
  31. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/docs/source/concepts.md +0 -0
  32. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/docs/source/conf.py +0 -0
  33. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/docs/source/export.md +0 -0
  34. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/docs/source/intro.md +0 -0
  35. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/docs/source/plots.md +0 -0
  36. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/docs/source/plugins.md +0 -0
  37. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/docs/source/queries.md +0 -0
  38. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/docs/source/quickstart.md +0 -0
  39. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/docs/source/stats.md +0 -0
  40. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/hooks/mypy.sh +0 -0
  41. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/hooks/no-parquet-outside-fixtures.sh +0 -0
  42. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/hooks/pytest.sh +0 -0
  43. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/src/core_lens/__main__.py +0 -0
  44. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/src/core_lens/base/__init__.py +0 -0
  45. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/src/core_lens/base/namespaces/__init__.py +0 -0
  46. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/src/core_lens/base/namespaces/stats.py +0 -0
  47. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/src/core_lens/base/result.py +0 -0
  48. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/src/core_lens/entities/__init__.py +0 -0
  49. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/src/core_lens/entities/mws.py +0 -0
  50. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/src/core_lens/entities/tehsil.py +0 -0
  51. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/src/core_lens/entities/waterbody.py +0 -0
  52. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/src/core_lens/export/__init__.py +0 -0
  53. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/src/core_lens/export/formats.py +0 -0
  54. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/src/core_lens/py.typed +0 -0
  55. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/src/core_lens/schema/__init__.py +0 -0
  56. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/src/core_lens/schema/detection.py +0 -0
  57. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/src/core_lens/schema/profile.py +0 -0
  58. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/src/core_lens/utils/__init__.py +0 -0
  59. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/src/core_lens/utils/polars_utils.py +0 -0
  60. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/src/core_lens/utils/season.py +0 -0
  61. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/tests/fixtures/generate_fixtures.py +0 -0
  62. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/tests/unit/conftest.py +0 -0
  63. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/tests/unit/test_aoi.py +0 -0
  64. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/tests/unit/test_entities.py +0 -0
  65. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/tests/unit/test_export.py +0 -0
  66. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/tests/unit/test_main.py +0 -0
  67. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/tests/unit/test_plot.py +0 -0
  68. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/tests/unit/test_polars_utils.py +0 -0
  69. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/tests/unit/test_profile.py +0 -0
  70. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/tests/unit/test_result.py +0 -0
  71. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/tests/unit/test_schema_detection.py +0 -0
  72. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/tests/unit/test_schema_profile.py +0 -0
  73. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/tests/unit/test_season.py +0 -0
  74. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/tests/unit/test_season_config.py +0 -0
  75. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/tests/unit/test_spatial.py +0 -0
  76. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/tests/unit/test_stats.py +0 -0
  77. {core_lens-0.1.dev117 → core_lens-0.1.dev121}/tests/unit/test_view.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: core-lens
3
- Version: 0.1.dev117
3
+ Version: 0.1.dev121
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
@@ -17,6 +17,7 @@ Requires-Python: >=3.13
17
17
  Requires-Dist: core-lens[core]
18
18
  Requires-Dist: core-lens[spatial]
19
19
  Provides-Extra: core
20
+ Requires-Dist: loguru>=0.7.3; extra == 'core'
20
21
  Requires-Dist: polars<2,>=1.39.0; extra == 'core'
21
22
  Requires-Dist: pyarrow<25,>=23.0.0; extra == 'core'
22
23
  Requires-Dist: pydantic<3,>=2.13.3; extra == 'core'
@@ -25,6 +26,7 @@ Requires-Dist: bokeh<4,>=3.6.0; extra == 'full'
25
26
  Requires-Dist: duckdb<2,>=1.5.3; extra == 'full'
26
27
  Requires-Dist: geoarrow-rust-core>=0.3.0; extra == 'full'
27
28
  Requires-Dist: geopandas<2,>=1.1.3; extra == 'full'
29
+ Requires-Dist: loguru>=0.7.3; extra == 'full'
28
30
  Requires-Dist: lonboard<1,>=0.16.0; extra == 'full'
29
31
  Requires-Dist: matplotlib<4,>=3.10.9; extra == 'full'
30
32
  Requires-Dist: polars<2,>=1.39.0; extra == 'full'
@@ -41,6 +43,7 @@ Requires-Dist: cudf-polars-cu13==26.6.*; extra == 'full-gpu'
41
43
  Requires-Dist: duckdb<2,>=1.5.3; extra == 'full-gpu'
42
44
  Requires-Dist: geoarrow-rust-core>=0.3.0; extra == 'full-gpu'
43
45
  Requires-Dist: geopandas<2,>=1.1.3; extra == 'full-gpu'
46
+ Requires-Dist: loguru>=0.7.3; extra == 'full-gpu'
44
47
  Requires-Dist: lonboard<1,>=0.16.0; extra == 'full-gpu'
45
48
  Requires-Dist: matplotlib<4,>=3.10.9; extra == 'full-gpu'
46
49
  Requires-Dist: polars<2,>=1.39.0; extra == 'full-gpu'
@@ -53,18 +56,21 @@ Requires-Dist: statsmodels<1,>=0.14.6; extra == 'full-gpu'
53
56
  Provides-Extra: gpu
54
57
  Requires-Dist: cudf-cu13==26.6.*; extra == 'gpu'
55
58
  Requires-Dist: cudf-polars-cu13==26.6.*; extra == 'gpu'
59
+ Requires-Dist: loguru>=0.7.3; extra == 'gpu'
56
60
  Requires-Dist: polars<2,>=1.39.0; extra == 'gpu'
57
61
  Requires-Dist: pyarrow<25,>=23.0.0; extra == 'gpu'
58
62
  Requires-Dist: pydantic<3,>=2.13.3; extra == 'gpu'
59
63
  Provides-Extra: spatial
60
64
  Requires-Dist: duckdb<2,>=1.5.3; extra == 'spatial'
61
65
  Requires-Dist: geopandas<2,>=1.1.3; extra == 'spatial'
66
+ Requires-Dist: loguru>=0.7.3; extra == 'spatial'
62
67
  Requires-Dist: polars<2,>=1.39.0; extra == 'spatial'
63
68
  Requires-Dist: pyarrow<25,>=23.0.0; extra == 'spatial'
64
69
  Requires-Dist: pydantic<3,>=2.13.3; extra == 'spatial'
65
70
  Requires-Dist: pyproj<4,>=3.7.2; extra == 'spatial'
66
71
  Requires-Dist: shapely<3,>=2.1.2; extra == 'spatial'
67
72
  Provides-Extra: stats
73
+ Requires-Dist: loguru>=0.7.3; extra == 'stats'
68
74
  Requires-Dist: polars<2,>=1.39.0; extra == 'stats'
69
75
  Requires-Dist: pyarrow<25,>=23.0.0; extra == 'stats'
70
76
  Requires-Dist: pydantic<3,>=2.13.3; extra == 'stats'
@@ -73,6 +79,7 @@ Requires-Dist: statsmodels<1,>=0.14.6; extra == 'stats'
73
79
  Provides-Extra: viz
74
80
  Requires-Dist: bokeh<4,>=3.6.0; extra == 'viz'
75
81
  Requires-Dist: geoarrow-rust-core>=0.3.0; extra == 'viz'
82
+ Requires-Dist: loguru>=0.7.3; extra == 'viz'
76
83
  Requires-Dist: lonboard<1,>=0.16.0; extra == 'viz'
77
84
  Requires-Dist: matplotlib<4,>=3.10.9; extra == 'viz'
78
85
  Requires-Dist: polars<2,>=1.39.0; extra == 'viz'
@@ -23,4 +23,5 @@ Provides a unified interface over microwatershed hydrology, administrative bound
23
23
  plots
24
24
  export
25
25
  plugins
26
+ logging
26
27
  autoapi/index
@@ -0,0 +1,32 @@
1
+ # Logging
2
+
3
+ `core_lens` uses [Loguru](https://loguru.readthedocs.io/) for internal logging and tracing.
4
+
5
+ To avoid polluting the standard output of your downstream applications, the logger is disabled by default upon import.
6
+
7
+ ## Enabling Logs
8
+
9
+ If you want to view the internal tracing logs (useful for debugging spatial resolution issues, inspecting generated time filters, or tracing validation errors), you need to explicitly enable the `core_lens` logger in your application and add a handler (e.g., `sys.stdout` or a log file).
10
+
11
+ ```python
12
+ import sys
13
+ from loguru import logger
14
+ from core_lens import AoI
15
+
16
+ # 1. Enable the internal logger
17
+ logger.enable("core_lens")
18
+
19
+ # 2. Add a handler with your desired level and formatting
20
+ logger.add(sys.stdout, level="DEBUG")
21
+
22
+ # 3. Operations will now emit tracing logs
23
+ aoi = AoI("data/", tehsil="Pangi", district="Chamba")
24
+ ```
25
+
26
+ ## Log Levels
27
+
28
+ `core_lens` emits logs at different levels depending on the operational context:
29
+
30
+ - **INFO**: Materialisation events (reading static, annual, or fortnightly Parquet files), entity registrations, AoI initialisation, and cross-entity spatial joins.
31
+ - **DEBUG**: Sub-operations such as spatial bounding box index generation, exact geometry relationships, lazy attribute filters, and query generation.
32
+ - **ERROR**: Incorrect initialisation arguments, failed path validations, missing geometries, unknown schema structures, or conflicting date configurations.
@@ -7,7 +7,7 @@ geometry, derive, stats, plot, temporal queries, and export.
7
7
 
8
8
  Prerequisites
9
9
  -------------
10
- pip install core-lens geopandas shapely polars scipy plotly lonboard
10
+ pip install core-lens geopandas shapely polars scipy bokeh lonboard
11
11
 
12
12
  Data layout expected
13
13
  --------------------
@@ -30,6 +30,7 @@ from __future__ import annotations
30
30
 
31
31
  import polars as pl
32
32
  import shapely.geometry as sgeom
33
+ from bokeh.io import show, save
33
34
 
34
35
  from core_lens import AoI, SeasonConfig
35
36
  from core_lens.entities import MWSEntity
@@ -253,7 +254,7 @@ except Exception:
253
254
 
254
255
  try:
255
256
  line_fig = result_annual.plot.timeseries(x="year", y="dw_et")
256
- line_fig.write_html("mws_et_trend.html")
257
+ save(line_fig, filename="mws_et_trend.html")
257
258
  print("Saved line plot to mws_et_trend.html")
258
259
  except Exception:
259
260
  pass
@@ -262,7 +263,7 @@ except Exception:
262
263
  # ── 21. Plot — scatter ───────────────────────────────────────────────────────
263
264
  try:
264
265
  scatter_fig = result_static_derived.plot.scatter(x="area_km2", y="dryspell_score")
265
- scatter_fig.write_html("mws_scatter.html")
266
+ save(scatter_fig, filename="mws_scatter.html")
266
267
  except Exception:
267
268
  pass
268
269
 
@@ -7,7 +7,7 @@ geometry, derive, stats, plot, and export.
7
7
 
8
8
  Prerequisites
9
9
  -------------
10
- pip install core-lens geopandas shapely polars scipy plotly lonboard
10
+ pip install core-lens geopandas shapely polars scipy bokeh lonboard
11
11
 
12
12
  Data layout expected
13
13
  --------------------
@@ -33,6 +33,7 @@ from core_lens.base.namespaces.stats import (
33
33
  TestMethod,
34
34
  )
35
35
  import shapely.geometry as sgeom
36
+ from bokeh.io import show, save
36
37
 
37
38
 
38
39
  DATA_ROOT = "data/"
@@ -288,19 +289,19 @@ result_plot = (
288
289
 
289
290
  # ── 19. Plot — scatter ────────────────────────────────────────────────────────
290
291
  scatter_fig = result_plot.plot.scatter(x="area_km2", y="Shape_Leng")
291
- scatter_fig.show()
292
+ show(scatter_fig)
292
293
 
293
294
 
294
295
  # ── 20. Plot — distribution ───────────────────────────────────────────────────
295
296
  dist_fig = result_plot.plot.distribution(x="area_km2")
296
- dist_fig.show()
297
+ show(dist_fig)
297
298
 
298
299
 
299
300
  # ── 21. Plot — correlation matrix ────────────────────────────────────────────
300
301
  corr_fig = result_plot.plot.correlation(
301
302
  columns=["area_km2", "Shape_Leng", "compactness"]
302
303
  )
303
- corr_fig.show()
304
+ show(corr_fig)
304
305
 
305
306
 
306
307
  # ── 22. Plot — heatmap ────────────────────────────────────────────────────────
@@ -312,7 +313,7 @@ heatmap_fig = result_plot.plot.heatmap(
312
313
  y="STATE",
313
314
  value="area_km2",
314
315
  )
315
- heatmap_fig.show()
316
+ show(heatmap_fig)
316
317
 
317
318
 
318
319
  # ── 23. AoI map ───────────────────────────────────────────────────────────────
@@ -44,7 +44,12 @@ venvPath = "."
44
44
  venv = ".venv"
45
45
 
46
46
  [project.optional-dependencies]
47
- core = ["polars>=1.39.0,<2", "pyarrow>=23.0.0,<25", "pydantic>=2.13.3,<3"]
47
+ core = [
48
+ "polars>=1.39.0,<2",
49
+ "pyarrow>=23.0.0,<25",
50
+ "pydantic>=2.13.3,<3",
51
+ "loguru>=0.7.3",
52
+ ]
48
53
  spatial = [
49
54
  "core-lens[core]",
50
55
  "duckdb>=1.5.3,<2",
@@ -6,4 +6,8 @@ except ImportError:
6
6
  from core_lens.aoi import AoI, SeasonConfig
7
7
  from core_lens.entities.tehsil import TehsilEntity
8
8
 
9
+ from loguru import logger
10
+
11
+ logger.disable("core_lens")
12
+
9
13
  __all__ = ["__version__", "AoI", "SeasonConfig", "TehsilEntity"]
@@ -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.dev117'
22
- __version_tuple__ = version_tuple = (0, 1, 'dev117')
21
+ __version__ = version = '0.1.dev121'
22
+ __version_tuple__ = version_tuple = (0, 1, 'dev121')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -12,6 +12,7 @@ if TYPE_CHECKING:
12
12
  from core_lens.base.result import Result
13
13
 
14
14
  import polars as pl
15
+ from loguru import logger
15
16
 
16
17
  from core_lens.base.entity import BaseEntity, EntityValidationError
17
18
 
@@ -177,6 +178,7 @@ class AoI:
177
178
  """
178
179
  self.data_root = pathlib.Path(data_root).resolve()
179
180
  self.seasons: SeasonConfig = seasons or SeasonConfig()
181
+ logger.info("Initializing AoI with data_root={}", self.data_root)
180
182
 
181
183
  n_modes = sum(
182
184
  [
@@ -186,11 +188,13 @@ class AoI:
186
188
  ]
187
189
  )
188
190
  if n_modes == 0:
191
+ logger.error("AoI initialization failed: no boundary argument supplied.")
189
192
  raise ValueError(
190
193
  "AoI requires exactly one boundary argument: "
191
194
  "bbox=, geometry=, or entity keyword filters such as tehsil='Pangi'."
192
195
  )
193
196
  if n_modes > 1:
197
+ logger.error("AoI initialization failed: multiple boundary modes supplied.")
194
198
  raise ValueError(
195
199
  "bbox, geometry, and entity keyword filters are mutually exclusive. "
196
200
  "Provide exactly one boundary mode."
@@ -320,6 +324,8 @@ class AoI:
320
324
  """
321
325
  import shapely.ops as sops
322
326
 
327
+ logger.debug("Resolving named boundary using kwargs: {}", entity_kwargs)
328
+
323
329
  # Find the registered entity whose key_col or known attribute column
324
330
  # matches one of the filter keys.
325
331
  candidate: BaseEntity | None = None
@@ -335,12 +341,19 @@ class AoI:
335
341
  # Fall back: look for an entity whose name matches a kwarg key
336
342
  # (e.g. tehsil="Pangi" → TehsilEntity if registered as "tehsil").
337
343
  if candidate is None:
344
+ logger.debug(
345
+ "No direct column match found for kwargs, attempting entity name fallback"
346
+ )
338
347
  for name, entity in self._entity_instances.items():
339
348
  if name in entity_kwargs:
340
349
  candidate = entity
341
350
  break
342
351
 
343
352
  if candidate is None:
353
+ logger.error(
354
+ "Boundary resolution failed: no registered entity matched filters {}",
355
+ entity_kwargs,
356
+ )
344
357
  raise EntityValidationError(
345
358
  f"No registered entity can satisfy the filters {entity_kwargs}. "
346
359
  f"Registered entities: {sorted(_REGISTRY)}."
@@ -367,6 +380,9 @@ class AoI:
367
380
  df = lf.filter(filter_expr).select(candidate.key_cols + [geom_col]).collect()
368
381
 
369
382
  if df.is_empty():
383
+ logger.error(
384
+ "Boundary resolution failed: no rows matched filters {}", entity_kwargs
385
+ )
370
386
  raise ValueError(
371
387
  f"No rows matched the filters {entity_kwargs} "
372
388
  f"in {candidate.static_path!r}."
@@ -401,6 +417,7 @@ class AoI:
401
417
  import pathlib as _pathlib
402
418
 
403
419
  name = _entity_name(entity_cls)
420
+ logger.info("Registering entity class: {}", name)
404
421
  # Probe with a no-root instance to check if paths are absolute.
405
422
  probe = entity_cls()
406
423
  static = probe.static_path
@@ -420,6 +437,7 @@ class AoI:
420
437
  entity_cls (type[BaseEntity]): The entity class to remove.
421
438
  """
422
439
  name = _entity_name(entity_cls)
440
+ logger.info("Deregistering entity class: {}", name)
423
441
  _REGISTRY.pop(name, None)
424
442
 
425
443
  @classmethod
@@ -452,11 +470,21 @@ def _validate_entity(entity: BaseEntity, name: str) -> None:
452
470
  try:
453
471
  static = entity._resolve(entity.static_path)
454
472
  except FileNotFoundError:
473
+ logger.error(
474
+ "Validation failed for entity {}: static_path '{}' does not exist.",
475
+ name,
476
+ entity.static_path,
477
+ )
455
478
  raise EntityValidationError(
456
479
  f"Entity {name!r}: static_path {entity.static_path!r} does not exist."
457
480
  )
458
481
 
459
482
  if not os.path.exists(static):
483
+ logger.error(
484
+ "Validation failed for entity {}: static path '{}' does not exist.",
485
+ name,
486
+ static,
487
+ )
460
488
  raise EntityValidationError(
461
489
  f"Entity {name!r}: static_path {static!r} does not exist."
462
490
  )
@@ -464,18 +492,34 @@ def _validate_entity(entity: BaseEntity, name: str) -> None:
464
492
  try:
465
493
  schema = pl.read_parquet_schema(static)
466
494
  except Exception as exc:
495
+ logger.error(
496
+ "Validation failed for entity {}: could not read schema from '{}': {}",
497
+ name,
498
+ static,
499
+ exc,
500
+ )
467
501
  raise EntityValidationError(
468
502
  f"Entity {name!r}: could not read schema from {static!r}: {exc}"
469
503
  ) from exc
470
504
 
471
505
  missing_keys = [c for c in entity.key_cols if c not in schema]
472
506
  if missing_keys:
507
+ logger.error(
508
+ "Validation failed for entity {}: key columns {} not found in schema.",
509
+ name,
510
+ missing_keys,
511
+ )
473
512
  raise EntityValidationError(
474
513
  f"Entity {name!r}: key_cols {missing_keys} not found in {static!r}. "
475
514
  f"Available columns: {list(schema.keys())}."
476
515
  )
477
516
 
478
517
  if entity.geometry_col not in schema:
518
+ logger.error(
519
+ "Validation failed for entity {}: geometry column '{}' not found in schema.",
520
+ name,
521
+ entity.geometry_col,
522
+ )
479
523
  raise EntityValidationError(
480
524
  f"Entity {name!r}: geometry_col {entity.geometry_col!r} "
481
525
  f"not found in {static!r}. Available columns: {list(schema.keys())}."
@@ -489,6 +533,12 @@ def _validate_entity(entity: BaseEntity, name: str) -> None:
489
533
  except FileNotFoundError:
490
534
  abs_path = None
491
535
  if abs_path is None or not os.path.exists(abs_path):
536
+ logger.error(
537
+ "Validation failed for entity {}: {} path '{}' does not exist.",
538
+ name,
539
+ label,
540
+ path,
541
+ )
492
542
  raise EntityValidationError(
493
543
  f"Entity {name!r}: {label}_path {path!r} does not exist."
494
544
  )
@@ -16,6 +16,7 @@ from abc import ABC, abstractmethod
16
16
  from typing import TYPE_CHECKING, Any
17
17
 
18
18
  import polars as pl
19
+ from loguru import logger
19
20
 
20
21
  from core_lens.utils.spatial import (
21
22
  bbox_intersects_geometry,
@@ -115,6 +116,9 @@ class BaseEntity(ABC):
115
116
  )
116
117
  p = root / p
117
118
  if not p.exists():
119
+ logger.error(
120
+ "Path resolution failed: '{}' (resolved to {}) does not exist.", path, p
121
+ )
118
122
  raise FileNotFoundError(
119
123
  f"Entity path {path!r} (resolved to {p}) does not exist. "
120
124
  "Provide an absolute path or ensure the file exists relative to "
@@ -219,6 +223,9 @@ class BaseEntity(ABC):
219
223
  @property
220
224
  def _index(self) -> pl.DataFrame:
221
225
  if not hasattr(self, "_cached_index"):
226
+ logger.debug(
227
+ "Building lazy bounding box index for {}", self.__class__.__name__
228
+ )
222
229
  profile = self.schema_profile
223
230
  self._cached_index: pl.DataFrame = build_bbox_index(
224
231
  static_path=self._resolve(self.static_path),
@@ -256,6 +263,10 @@ class BaseEntity(ABC):
256
263
  from core_lens.aoi import _REGISTRY
257
264
  from core_lens.base.view import View
258
265
 
266
+ logger.debug(
267
+ "Applying attribute filters on {}: {}", self.__class__.__name__, kwargs
268
+ )
269
+
259
270
  static = self._resolve(self.static_path)
260
271
  schema = pl.read_parquet_schema(static)
261
272
 
@@ -265,6 +276,10 @@ class BaseEntity(ABC):
265
276
  # Validate entity-kwargs early so we give a useful error message.
266
277
  for k in entity_kwargs:
267
278
  if k not in _REGISTRY:
279
+ logger.error(
280
+ "BaseEntity.where failed: '{}' is neither a column nor a registered entity.",
281
+ k,
282
+ )
268
283
  raise ValueError(
269
284
  f"BaseEntity.where: {k!r} is not a column in {self.static_path!r} "
270
285
  f"and is not a registered entity name. "
@@ -306,6 +321,12 @@ class BaseEntity(ABC):
306
321
  )
307
322
 
308
323
  if matched.is_empty():
324
+ logger.error(
325
+ "BaseEntity.where failed: No rows matched {}={!r} in {}",
326
+ entity_kwarg_name,
327
+ entity_kwarg_val,
328
+ other_entity.static_path,
329
+ )
309
330
  raise ValueError(
310
331
  f"BaseEntity.where: No rows matched {entity_kwarg_name}={entity_kwarg_val!r} "
311
332
  f"in {other_entity.static_path!r}."
@@ -379,6 +400,9 @@ class BaseEntity(ABC):
379
400
  from core_lens.base.view import View
380
401
 
381
402
  if geometry is None and bbox is None:
403
+ logger.error(
404
+ "BaseEntity.spatial_filter failed: neither 'geometry' nor 'bbox' provided."
405
+ )
382
406
  raise ValueError(
383
407
  "spatial_filter() requires either 'geometry' or 'bbox' to be provided."
384
408
  )
@@ -387,6 +411,12 @@ class BaseEntity(ABC):
387
411
 
388
412
  assert geometry is not None # guaranteed by the guards above
389
413
 
414
+ logger.debug(
415
+ "Applying spatial filter on {} (relationship='{}')",
416
+ self.__class__.__name__,
417
+ relationship,
418
+ )
419
+
390
420
  profile = self.schema_profile
391
421
  candidates = bbox_intersects_geometry(self._index, geometry)
392
422
  keys = exact_spatial_filter(
@@ -424,6 +454,12 @@ class BaseEntity(ABC):
424
454
  """
425
455
  from core_lens.base.view import View
426
456
 
457
+ logger.debug(
458
+ "Deferred spatial join registered for {} with {}",
459
+ self.__class__.__name__,
460
+ other.__class__.__name__,
461
+ )
462
+
427
463
  entity_name = _entity_name(type(self))
428
464
  join_spec = {"other": other, "agg": agg}
429
465
  return View(
@@ -277,15 +277,20 @@ class PlotNamespace:
277
277
  # --- Direct Polars → GeoArrow → Lonboard path (zero Shapely, zero GeoPandas) ---
278
278
  arrow_table = _wkb_to_arrow_table(df, geom_col, extra_cols=extra)
279
279
 
280
+ import warnings
281
+
280
282
  # Extract colormap values from the already-filtered arrow_table.
281
283
  # arrow_table has the same row order as df post-filter, so indices align.
282
- values = np.asarray(arrow_table.column(column).to_pylist())
283
- v_min, v_max = np.nanmin(values), np.nanmax(values)
284
- norm_values = (
285
- (values - v_min) / (v_max - v_min)
286
- if v_max > v_min
287
- else np.zeros_like(values)
288
- )
284
+ values = np.asarray(arrow_table.column(column).to_pylist(), dtype=float)
285
+
286
+ with warnings.catch_warnings():
287
+ warnings.simplefilter("ignore", category=RuntimeWarning)
288
+ v_min, v_max = np.nanmin(values), np.nanmax(values)
289
+
290
+ if np.isnan(v_min) or np.isnan(v_max) or v_min >= v_max:
291
+ norm_values = np.where(np.isnan(values), np.nan, 0.0)
292
+ else:
293
+ norm_values = (values - v_min) / (v_max - v_min)
289
294
 
290
295
  cmap = mpl.colormaps["plasma"]
291
296
 
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  from typing import TYPE_CHECKING, Any
6
6
 
7
7
  import polars as pl
8
+ from loguru import logger
8
9
 
9
10
  from enum import Enum
10
11
 
@@ -145,6 +146,9 @@ class View:
145
146
  )
146
147
 
147
148
  if geometry is None and bbox is None:
149
+ logger.error(
150
+ "View.spatial_filter failed: neither 'geometry' nor 'bbox' provided."
151
+ )
148
152
  raise ValueError(
149
153
  "spatial_filter() requires either 'geometry' or 'bbox' to be provided."
150
154
  )
@@ -193,6 +197,9 @@ class View:
193
197
  View: A new lazy :class:`View` with the join spec recorded.
194
198
  """
195
199
  if self.join_spec is not None:
200
+ logger.error(
201
+ "View.spatial_join failed: View already has a pending spatial_join."
202
+ )
196
203
  raise ValueError("View already has a pending spatial_join.")
197
204
 
198
205
  return View(
@@ -252,23 +259,33 @@ class View:
252
259
  supplying ``year`` with ``season="current"``).
253
260
  """
254
261
  if season is not None and (start is not None or end is not None):
262
+ logger.error(
263
+ "View.between failed: Date range (start/end) and season mode are mutually exclusive."
264
+ )
255
265
  raise ValueError(
256
266
  "View.between: Date range (start/end) and season mode are mutually exclusive. "
257
267
  "Use either positional date strings or the 'season' keyword, not both."
258
268
  )
259
269
 
260
270
  if season is None and year is not None:
271
+ logger.error(
272
+ "View.between failed: 'year' argument provided without 'season'."
273
+ )
261
274
  raise ValueError(
262
275
  "View.between: 'year' argument is only valid when 'season' is also provided."
263
276
  )
264
277
 
265
278
  if season is not None:
266
279
  if not isinstance(season, Season):
280
+ logger.error("View.between failed: season is not a Season enum.")
267
281
  raise ValueError(
268
282
  f"View.between: season must be a Season enum. "
269
283
  f"Valid options: {[e.name for e in Season]}."
270
284
  )
271
285
  if season is Season.CURRENT and year is not None:
286
+ logger.error(
287
+ "View.between failed: Cannot combine year with season='current'."
288
+ )
272
289
  raise ValueError(
273
290
  "View.between: Cannot combine year with season='current'. "
274
291
  "The current season is always resolved to the present calendar date."
@@ -278,6 +295,9 @@ class View:
278
295
  time_filter["year"] = year
279
296
  else:
280
297
  if start is None or end is None:
298
+ logger.error(
299
+ "View.between failed: Both 'start' and 'end' must be provided for date range mode."
300
+ )
281
301
  raise ValueError(
282
302
  "View.between: Both 'start' and 'end' must be provided for date range mode."
283
303
  )
@@ -340,12 +360,20 @@ class View:
340
360
 
341
361
  profile = self.entity.schema_profile
342
362
 
363
+ logger.info(
364
+ "Materialising {} resolution for {}", resolution.name, self.entity_name
365
+ )
366
+
343
367
  path: str
344
368
  if resolution == Resolution.STATIC:
345
369
  path = self.entity.static_path
346
370
  elif resolution == Resolution.ANNUAL:
347
371
  annual_path = self.entity.annual_path
348
372
  if annual_path is None:
373
+ logger.error(
374
+ "View._materialise failed: Entity '{}' has no annual_path declared.",
375
+ self.entity_name,
376
+ )
349
377
  raise AttributeError(
350
378
  f"Entity {self.entity_name!r} has no annual_path declared."
351
379
  )
@@ -353,6 +381,10 @@ class View:
353
381
  else:
354
382
  fn_path = self.entity.fortnightly_path
355
383
  if fn_path is None:
384
+ logger.error(
385
+ "View._materialise failed: Entity '{}' has no fortnightly_path declared.",
386
+ self.entity_name,
387
+ )
356
388
  raise AttributeError(
357
389
  f"Entity {self.entity_name!r} has no fortnightly_path declared."
358
390
  )
@@ -396,6 +428,7 @@ class View:
396
428
  _tf = dict(self.time_filter)
397
429
  if "start" in _tf and "end" in _tf:
398
430
  _tf["_is_year_col"] = _is_year_col
431
+ logger.debug("Applying time filter expr using {}", _tf)
399
432
  time_expr = resolve_time_filter(_tf, time_col, season_cfg)
400
433
 
401
434
  lf = scan_with_key_filter(
@@ -439,6 +472,7 @@ class View:
439
472
 
440
473
  from core_lens.utils.spatial import execute_spatial_join
441
474
 
475
+ logger.debug("Executing spatial join with {}", other_entity_name)
442
476
  data = execute_spatial_join(
443
477
  primary_df=data,
444
478
  primary_key_cols=self.entity.key_cols,