core-lens 0.1.dev196__tar.gz → 0.1.dev198__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 (95) hide show
  1. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/PKG-INFO +6 -33
  2. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/benchmarks/bench_aoi.py +1 -1
  3. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/benchmarks/bench_entity.py +5 -5
  4. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/benchmarks/bench_spatial.py +1 -1
  5. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/benchmarks/bench_view.py +4 -4
  6. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/pyproject.toml +4 -3
  7. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/src/core_lens/_version.py +2 -2
  8. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/src/core_lens/aoi.py +17 -7
  9. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/src/core_lens/base/entity.py +15 -8
  10. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/src/core_lens/base/namespaces/plot.py +23 -10
  11. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/src/core_lens/base/namespaces/stats.py +34 -18
  12. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/src/core_lens/base/result.py +13 -2
  13. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/src/core_lens/base/view.py +17 -9
  14. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/src/core_lens/schema/detection.py +29 -2
  15. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/src/core_lens/utils/paths.py +13 -6
  16. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/src/core_lens/utils/polars_utils.py +46 -6
  17. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/src/core_lens/utils/spatial.py +25 -10
  18. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/tests/unit/conftest.py +12 -0
  19. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/tests/unit/test_entity.py +46 -6
  20. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/tests/unit/test_polars_utils.py +44 -7
  21. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/tests/unit/test_result.py +1 -1
  22. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/tests/unit/test_schema_detection.py +26 -0
  23. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/tests/unit/test_season.py +1 -1
  24. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/tests/unit/test_view.py +2 -2
  25. core_lens-0.1.dev198/uv.lock +4285 -0
  26. core_lens-0.1.dev196/uv.lock +0 -4417
  27. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/.github/ISSUE_TEMPLATE/blank-proposal.yaml +0 -0
  28. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/.github/ISSUE_TEMPLATE/bug-report.yaml +0 -0
  29. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/.github/ISSUE_TEMPLATE/feature-request.yaml +0 -0
  30. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/.github/pull_request_template.md +0 -0
  31. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/.github/workflows/ci.yml +0 -0
  32. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/.github/workflows/gh-pages.yml +0 -0
  33. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/.github/workflows/pre-release.yml +0 -0
  34. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/.github/workflows/release.yml +0 -0
  35. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/.gitignore +0 -0
  36. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/.gitmessage +0 -0
  37. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/.pre-commit-config.yaml +0 -0
  38. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/.python-version +0 -0
  39. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/CONTRIBUTING.md +0 -0
  40. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/LICENSE +0 -0
  41. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/README.md +0 -0
  42. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/SKILLS.md +0 -0
  43. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/benchmarks/README.md +0 -0
  44. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/benchmarks/bench_export.py +0 -0
  45. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/benchmarks/bench_polars_utils.py +0 -0
  46. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/benchmarks/bench_result.py +0 -0
  47. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/benchmarks/bench_schema.py +0 -0
  48. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/benchmarks/bench_season.py +0 -0
  49. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/benchmarks/run_all.sh +0 -0
  50. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/docs/Makefile +0 -0
  51. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/docs/make.bat +0 -0
  52. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/docs/source/concepts.md +0 -0
  53. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/docs/source/conf.py +0 -0
  54. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/docs/source/export.md +0 -0
  55. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/docs/source/index.rst +0 -0
  56. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/docs/source/intro.md +0 -0
  57. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/docs/source/logging.md +0 -0
  58. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/docs/source/plots.md +0 -0
  59. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/docs/source/plugins.md +0 -0
  60. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/docs/source/queries.md +0 -0
  61. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/docs/source/quickstart.md +0 -0
  62. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/docs/source/stats.md +0 -0
  63. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/examples/demo_mws.py +0 -0
  64. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/examples/demo_tehsil.py +0 -0
  65. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/hooks/mypy.sh +0 -0
  66. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/hooks/no-parquet-outside-fixtures.sh +0 -0
  67. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/hooks/pytest.sh +0 -0
  68. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/src/core_lens/__init__.py +0 -0
  69. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/src/core_lens/__main__.py +0 -0
  70. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/src/core_lens/base/__init__.py +0 -0
  71. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/src/core_lens/base/namespaces/__init__.py +0 -0
  72. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/src/core_lens/entities/__init__.py +0 -0
  73. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/src/core_lens/entities/farms.py +0 -0
  74. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/src/core_lens/entities/mws.py +0 -0
  75. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/src/core_lens/entities/tehsil.py +0 -0
  76. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/src/core_lens/entities/waterbody.py +0 -0
  77. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/src/core_lens/export/__init__.py +0 -0
  78. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/src/core_lens/export/formats.py +0 -0
  79. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/src/core_lens/py.typed +0 -0
  80. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/src/core_lens/schema/__init__.py +0 -0
  81. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/src/core_lens/schema/profile.py +0 -0
  82. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/src/core_lens/utils/__init__.py +0 -0
  83. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/src/core_lens/utils/season.py +0 -0
  84. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/tests/fixtures/generate_fixtures.py +0 -0
  85. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/tests/unit/test_aoi.py +0 -0
  86. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/tests/unit/test_entities.py +0 -0
  87. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/tests/unit/test_export.py +0 -0
  88. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/tests/unit/test_main.py +0 -0
  89. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/tests/unit/test_plot.py +0 -0
  90. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/tests/unit/test_profile.py +0 -0
  91. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/tests/unit/test_schema_profile.py +0 -0
  92. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/tests/unit/test_season_config.py +0 -0
  93. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/tests/unit/test_spatial.py +0 -0
  94. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/tests/unit/test_stats.py +0 -0
  95. {core_lens-0.1.dev196 → core_lens-0.1.dev198}/usage.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: core-lens
3
- Version: 0.1.dev196
3
+ Version: 0.1.dev198
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
@@ -21,7 +21,7 @@ Requires-Dist: core-lens[spatial]
21
21
  Provides-Extra: core
22
22
  Requires-Dist: awscrt>=0.23.0; extra == 'core'
23
23
  Requires-Dist: loguru>=0.7.3; extra == 'core'
24
- Requires-Dist: polars<2,>=1.39.0; extra == 'core'
24
+ Requires-Dist: polars<3,>=2.0.0; extra == 'core'
25
25
  Requires-Dist: pyarrow<25,>=23.0.0; extra == 'core'
26
26
  Requires-Dist: pydantic<3,>=2.13.3; extra == 'core'
27
27
  Provides-Extra: full
@@ -33,7 +33,7 @@ Requires-Dist: geopandas<2,>=1.1.3; extra == 'full'
33
33
  Requires-Dist: loguru>=0.7.3; extra == 'full'
34
34
  Requires-Dist: lonboard<1,>=0.16.0; extra == 'full'
35
35
  Requires-Dist: matplotlib<4,>=3.10.9; extra == 'full'
36
- Requires-Dist: polars<2,>=1.39.0; extra == 'full'
36
+ Requires-Dist: polars<3,>=2.0.0; extra == 'full'
37
37
  Requires-Dist: pyarrow<25,>=23.0.0; extra == 'full'
38
38
  Requires-Dist: pydantic<3,>=2.13.3; extra == 'full'
39
39
  Requires-Dist: pyogrio<1,>=0.10.0; extra == 'full'
@@ -41,39 +41,12 @@ Requires-Dist: pyproj<4,>=3.7.2; extra == 'full'
41
41
  Requires-Dist: scipy<2,>=1.17.1; extra == 'full'
42
42
  Requires-Dist: shapely<3,>=2.1.2; extra == 'full'
43
43
  Requires-Dist: statsmodels<1,>=0.14.6; extra == 'full'
44
- Provides-Extra: full-gpu
45
- Requires-Dist: awscrt>=0.23.0; extra == 'full-gpu'
46
- Requires-Dist: bokeh<4,>=3.6.0; extra == 'full-gpu'
47
- Requires-Dist: cudf-cu13==26.6.*; extra == 'full-gpu'
48
- Requires-Dist: cudf-polars-cu13==26.6.*; extra == 'full-gpu'
49
- Requires-Dist: duckdb<2,>=1.5.3; extra == 'full-gpu'
50
- Requires-Dist: geoarrow-rust-core>=0.3.0; extra == 'full-gpu'
51
- Requires-Dist: geopandas<2,>=1.1.3; extra == 'full-gpu'
52
- Requires-Dist: loguru>=0.7.3; extra == 'full-gpu'
53
- Requires-Dist: lonboard<1,>=0.16.0; extra == 'full-gpu'
54
- Requires-Dist: matplotlib<4,>=3.10.9; extra == 'full-gpu'
55
- Requires-Dist: polars<2,>=1.39.0; extra == 'full-gpu'
56
- Requires-Dist: pyarrow<25,>=23.0.0; extra == 'full-gpu'
57
- Requires-Dist: pydantic<3,>=2.13.3; extra == 'full-gpu'
58
- Requires-Dist: pyogrio<1,>=0.10.0; extra == 'full-gpu'
59
- Requires-Dist: pyproj<4,>=3.7.2; extra == 'full-gpu'
60
- Requires-Dist: scipy<2,>=1.17.1; extra == 'full-gpu'
61
- Requires-Dist: shapely<3,>=2.1.2; extra == 'full-gpu'
62
- Requires-Dist: statsmodels<1,>=0.14.6; extra == 'full-gpu'
63
- Provides-Extra: gpu
64
- Requires-Dist: awscrt>=0.23.0; extra == 'gpu'
65
- Requires-Dist: cudf-cu13==26.6.*; extra == 'gpu'
66
- Requires-Dist: cudf-polars-cu13==26.6.*; extra == 'gpu'
67
- Requires-Dist: loguru>=0.7.3; extra == 'gpu'
68
- Requires-Dist: polars<2,>=1.39.0; extra == 'gpu'
69
- Requires-Dist: pyarrow<25,>=23.0.0; extra == 'gpu'
70
- Requires-Dist: pydantic<3,>=2.13.3; extra == 'gpu'
71
44
  Provides-Extra: spatial
72
45
  Requires-Dist: awscrt>=0.23.0; extra == 'spatial'
73
46
  Requires-Dist: duckdb<2,>=1.5.3; extra == 'spatial'
74
47
  Requires-Dist: geopandas<2,>=1.1.3; extra == 'spatial'
75
48
  Requires-Dist: loguru>=0.7.3; extra == 'spatial'
76
- Requires-Dist: polars<2,>=1.39.0; extra == 'spatial'
49
+ Requires-Dist: polars<3,>=2.0.0; extra == 'spatial'
77
50
  Requires-Dist: pyarrow<25,>=23.0.0; extra == 'spatial'
78
51
  Requires-Dist: pydantic<3,>=2.13.3; extra == 'spatial'
79
52
  Requires-Dist: pyogrio<1,>=0.10.0; extra == 'spatial'
@@ -82,7 +55,7 @@ Requires-Dist: shapely<3,>=2.1.2; extra == 'spatial'
82
55
  Provides-Extra: stats
83
56
  Requires-Dist: awscrt>=0.23.0; extra == 'stats'
84
57
  Requires-Dist: loguru>=0.7.3; extra == 'stats'
85
- Requires-Dist: polars<2,>=1.39.0; extra == 'stats'
58
+ Requires-Dist: polars<3,>=2.0.0; extra == 'stats'
86
59
  Requires-Dist: pyarrow<25,>=23.0.0; extra == 'stats'
87
60
  Requires-Dist: pydantic<3,>=2.13.3; extra == 'stats'
88
61
  Requires-Dist: scipy<2,>=1.17.1; extra == 'stats'
@@ -94,7 +67,7 @@ Requires-Dist: geoarrow-rust-core>=0.3.0; extra == 'viz'
94
67
  Requires-Dist: loguru>=0.7.3; extra == 'viz'
95
68
  Requires-Dist: lonboard<1,>=0.16.0; extra == 'viz'
96
69
  Requires-Dist: matplotlib<4,>=3.10.9; extra == 'viz'
97
- Requires-Dist: polars<2,>=1.39.0; extra == 'viz'
70
+ Requires-Dist: polars<3,>=2.0.0; extra == 'viz'
98
71
  Requires-Dist: pyarrow<25,>=23.0.0; extra == 'viz'
99
72
  Requires-Dist: pydantic<3,>=2.13.3; extra == 'viz'
100
73
  Description-Content-Type: text/markdown
@@ -116,7 +116,7 @@ view = aoi_bbox.mws
116
116
 
117
117
  t1 = time.perf_counter()
118
118
  print(f"First access : {(t1 - t0) * 1000:.2f} ms")
119
- print(f"Keys shape : {view.keys.collect().shape}")
119
+ print(f"Keys shape : {view.keys.collect(engine='in-memory').shape}")
120
120
 
121
121
  # Second access — should hit _scoped cache (near-zero cost).
122
122
  t0 = time.perf_counter()
@@ -216,7 +216,7 @@ t0 = time.perf_counter()
216
216
  view_attr = entity.where(state="Bihar")
217
217
  t1 = time.perf_counter()
218
218
  print(f"where(state) : {(t1 - t0) * 1000:.2f} ms")
219
- print(f"Bihar keys : {view_attr.keys.collect().shape}")
219
+ print(f"Bihar keys : {view_attr.keys.collect(engine='in-memory').shape}")
220
220
 
221
221
 
222
222
  # ── 9. BaseEntity.where() — composite attribute filter ───────────────────────
@@ -225,7 +225,7 @@ t0 = time.perf_counter()
225
225
  view_composite = entity.where(state="Bihar", district="Banka", tehsil="Banka")
226
226
  t1 = time.perf_counter()
227
227
  print(f"where(3 cols) : {(t1 - t0) * 1000:.2f} ms")
228
- print(f"Banka keys : {view_composite.keys.collect().shape}")
228
+ print(f"Banka keys : {view_composite.keys.collect(engine='in-memory').shape}")
229
229
 
230
230
 
231
231
  # ── 10. BaseEntity.where() — list value (is_in) ──────────────────────────────
@@ -234,7 +234,7 @@ t0 = time.perf_counter()
234
234
  view_list = entity.where(state=["Bihar", "Jharkhand"])
235
235
  t1 = time.perf_counter()
236
236
  print(f"where(list) : {(t1 - t0) * 1000:.2f} ms")
237
- print(f"Bihar+Jharkhand keys : {view_list.keys.collect().shape}")
237
+ print(f"Bihar+Jharkhand keys : {view_list.keys.collect(engine='in-memory').shape}")
238
238
 
239
239
 
240
240
  # ── 11. BaseEntity.where() — unknown kwarg error ─────────────────────────────
@@ -253,7 +253,7 @@ t0 = time.perf_counter()
253
253
  view_sf = entity.spatial_filter(bbox=SMALL_BBOX)
254
254
  t1 = time.perf_counter()
255
255
  print(f"spatial_filter (bbox): {(t1 - t0) * 1000:.2f} ms")
256
- print(f"Keys : {view_sf.keys.collect().shape}")
256
+ print(f"Keys : {view_sf.keys.collect(engine='in-memory').shape}")
257
257
 
258
258
  # geometry mode
259
259
  small_poly = sgeom.box(*SMALL_BBOX)
@@ -269,7 +269,7 @@ view_sf_area = entity.spatial_filter(
269
269
  )
270
270
  t1 = time.perf_counter()
271
271
  print(
272
- f"spatial_filter (area): {(t1 - t0) * 1000:.2f} ms keys={view_sf_area.keys.collect().shape}"
272
+ f"spatial_filter (area): {(t1 - t0) * 1000:.2f} ms keys={view_sf_area.keys.collect(engine='in-memory').shape}"
273
273
  )
274
274
 
275
275
 
@@ -183,7 +183,7 @@ view = entity.spatial_filter(bbox=SMALL_BBOX)
183
183
 
184
184
  t1 = time.perf_counter()
185
185
  print(f"spatial_filter (bbox) : {(t1 - t0) * 1000:.2f} ms")
186
- print(f"View keys : {view.keys.collect().shape}")
186
+ print(f"View keys : {view.keys.collect(engine='in-memory').shape}")
187
187
 
188
188
  t0 = time.perf_counter()
189
189
  view2 = entity.spatial_filter(geometry=small_poly)
@@ -54,7 +54,7 @@ t0 = time.perf_counter()
54
54
  view_all = aoi.mws
55
55
  t1 = time.perf_counter()
56
56
  print(f"aoi.mws : {(t1 - t0) * 1000:.2f} ms")
57
- print(f"Keys : {view_all.keys.collect().shape}")
57
+ print(f"Keys : {view_all.keys.collect(engine='in-memory').shape}")
58
58
 
59
59
 
60
60
  # ── 2. View.where() — single filter ──────────────────────────────────────────
@@ -63,7 +63,7 @@ t0 = time.perf_counter()
63
63
  view_bihar = view_all.where(state="Bihar")
64
64
  t1 = time.perf_counter()
65
65
  print(f"where(state) : {(t1 - t0) * 1000:.2f} ms")
66
- print(f"Bihar keys : {view_bihar.keys.collect().shape}")
66
+ print(f"Bihar keys : {view_bihar.keys.collect(engine='in-memory').shape}")
67
67
 
68
68
 
69
69
  # ── 3. View.where() — composite AND filter ────────────────────────────────────
@@ -72,7 +72,7 @@ t0 = time.perf_counter()
72
72
  view_banka = view_all.where(state="Bihar", district="Banka", tehsil="Banka")
73
73
  t1 = time.perf_counter()
74
74
  print(f"where(3 cols) : {(t1 - t0) * 1000:.2f} ms")
75
- print(f"Banka keys : {view_banka.keys.collect().shape}")
75
+ print(f"Banka keys : {view_banka.keys.collect(engine='in-memory').shape}")
76
76
 
77
77
 
78
78
  # ── 4. View.spatial_filter() — further narrow ─────────────────────────────────
@@ -82,7 +82,7 @@ t0 = time.perf_counter()
82
82
  view_spatial = view_all.spatial_filter(bbox=SMALL_BBOX)
83
83
  t1 = time.perf_counter()
84
84
  print(f"spatial_filter : {(t1 - t0) * 1000:.2f} ms")
85
- print(f"Spatial keys : {view_spatial.keys.collect().shape}")
85
+ print(f"Spatial keys : {view_spatial.keys.collect(engine='in-memory').shape}")
86
86
 
87
87
 
88
88
  # ── 5. View.between() — date range (no I/O) ───────────────────────────────────
@@ -47,7 +47,7 @@ venv = ".venv"
47
47
 
48
48
  [project.optional-dependencies]
49
49
  core = [
50
- "polars>=1.39.0,<2",
50
+ "polars>=2.0.0,<3",
51
51
  "pyarrow>=23.0.0,<25",
52
52
  "pydantic>=2.13.3,<3",
53
53
  "loguru>=0.7.3",
@@ -69,9 +69,10 @@ viz = [
69
69
  "bokeh>=3.6.0,<4",
70
70
  ]
71
71
  stats = ["core-lens[core]", "scipy>=1.17.1,<2", "statsmodels>=0.14.6,<1"]
72
- gpu = ["core-lens[core]", "cudf-cu13==26.6.*", "cudf-polars-cu13==26.6.*"]
72
+ # GPU dependencies commented out until NVIDIA RAPIDS releases cudf-polars for Polars 2.0+
73
+ # gpu = ["core-lens[core]", "cudf-cu13==26.6.*", "cudf-polars-cu13==26.6.*"]
73
74
  full = ["core-lens[spatial,viz,stats]"]
74
- full-gpu = ["core-lens[spatial,viz,stats,gpu]"]
75
+ # full-gpu = ["core-lens[spatial,viz,stats,gpu]"]
75
76
 
76
77
  [dependency-groups]
77
78
  dev = [
@@ -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.dev196'
22
- __version_tuple__ = version_tuple = (0, 1, 'dev196')
21
+ __version__ = version = '0.1.dev198'
22
+ __version_tuple__ = version_tuple = (0, 1, 'dev198')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -20,7 +20,11 @@ from loguru import logger
20
20
 
21
21
  from core_lens.base.entity import BaseEntity, EntityValidationError
22
22
  from core_lens.utils.paths import is_cloud_uri
23
- from core_lens.utils.polars_utils import cached_read_schema, parquet_scan_path
23
+ from core_lens.utils.polars_utils import (
24
+ cached_read_schema,
25
+ cast_filter_values,
26
+ parquet_scan_path,
27
+ )
24
28
 
25
29
  if TYPE_CHECKING:
26
30
  from core_lens.base.view import View
@@ -232,22 +236,28 @@ def _cached_resolve_boundary(
232
236
  schema_types = cached_read_schema(static_path, storage_options)
233
237
  for col, val in entity_kwargs.items():
234
238
  if col in schema.key_cols or col in schema.extra_static_cols:
235
- is_list_col = isinstance(schema_types.get(col), pl.List)
239
+ col_dtype = schema_types.get(col)
240
+ is_list_col = isinstance(col_dtype, pl.List)
241
+ cast_val = cast_filter_values(val, col_dtype)
236
242
  if isinstance(val, list):
237
243
  if is_list_col:
238
244
  filter_expr = (
239
245
  filter_expr
240
- & pl.col(col).list.eval(pl.element().is_in(val)).list.any()
246
+ & pl.col(col).list.eval(pl.element().is_in(cast_val)).list.any()
241
247
  )
242
248
  else:
243
- filter_expr = filter_expr & pl.col(col).is_in(val)
249
+ filter_expr = filter_expr & pl.col(col).is_in(cast_val)
244
250
  else:
245
251
  if is_list_col:
246
- filter_expr = filter_expr & pl.col(col).list.contains(val)
252
+ filter_expr = filter_expr & pl.col(col).list.contains(cast_val)
247
253
  else:
248
- filter_expr = filter_expr & (pl.col(col) == val)
254
+ filter_expr = filter_expr & (pl.col(col) == cast_val)
249
255
 
250
- df = lf.filter(filter_expr).select(candidate.key_cols + [geom_col]).collect()
256
+ df = (
257
+ lf.filter(filter_expr)
258
+ .select(candidate.key_cols + [geom_col])
259
+ .collect(engine="in-memory")
260
+ )
251
261
 
252
262
  if df.is_empty():
253
263
  raise ValueError(
@@ -29,7 +29,11 @@ from core_lens.utils.spatial import (
29
29
  build_bbox_index,
30
30
  exact_spatial_filter,
31
31
  )
32
- from core_lens.utils.polars_utils import cached_read_schema, parquet_scan_path
32
+ from core_lens.utils.polars_utils import (
33
+ cached_read_schema,
34
+ cast_filter_values,
35
+ parquet_scan_path,
36
+ )
33
37
 
34
38
  if TYPE_CHECKING:
35
39
  import shapely
@@ -487,23 +491,25 @@ class BaseEntity(ABC):
487
491
  dtype = schema[col]
488
492
  is_list_col = isinstance(dtype, pl.List) or dtype == pl.List
489
493
  is_val_list = isinstance(val, (list, tuple, set))
494
+ cast_val = cast_filter_values(val, dtype)
490
495
 
491
496
  if is_list_col:
492
497
  if is_val_list:
493
498
  filter_expr = filter_expr & (
494
- pl.col(col).list.set_intersection(list(val)).list.len() > 0
499
+ pl.col(col).list.set_intersection(list(cast_val)).list.len()
500
+ > 0
495
501
  )
496
502
  else:
497
- filter_expr = filter_expr & pl.col(col).list.contains(val)
503
+ filter_expr = filter_expr & pl.col(col).list.contains(cast_val)
498
504
  else:
499
505
  if is_val_list:
500
- filter_expr = filter_expr & pl.col(col).is_in(list(val))
506
+ filter_expr = filter_expr & pl.col(col).is_in(list(cast_val))
501
507
  else:
502
- filter_expr = filter_expr & (pl.col(col) == val)
508
+ filter_expr = filter_expr & (pl.col(col) == cast_val)
503
509
  lf = lf.filter(filter_expr)
504
510
 
505
511
  if not entity_kwargs:
506
- keys = lf.select(self.key_cols).collect()
512
+ keys = lf.select(self.key_cols).collect(engine="in-memory")
507
513
  else:
508
514
  import shapely
509
515
  import shapely.wkb as swkb
@@ -538,7 +544,7 @@ class BaseEntity(ABC):
538
544
  matched = (
539
545
  other_lf.filter(match_expr)
540
546
  .select([other_profile.geometry_col])
541
- .collect()
547
+ .collect(engine="in-memory")
542
548
  )
543
549
 
544
550
  if matched.is_empty():
@@ -572,8 +578,9 @@ class BaseEntity(ABC):
572
578
  candidates.select(self.key_cols).lazy(),
573
579
  on=self.key_cols,
574
580
  how="semi",
581
+ maintain_order="left",
575
582
  )
576
- df = lf.collect()
583
+ df = lf.collect(engine="in-memory")
577
584
 
578
585
  if df.is_empty():
579
586
  keys = df.select(self.key_cols)
@@ -91,7 +91,7 @@ def _wkb_to_arrow_table(
91
91
  )
92
92
 
93
93
  # --- Fast path: pure Polygon / MultiPolygon rows (no Shapely) ---
94
- poly_mask = geom_type_series.is_in([3, 6])
94
+ poly_mask = geom_type_series.is_in(pl.Series([3, 6], dtype=pl.UInt8).to_list())
95
95
  df_poly = df.filter(poly_mask)
96
96
 
97
97
  # --- Slow path: GeometryCollection rows — extract polygon parts via Shapely ---
@@ -545,7 +545,7 @@ class PlotNamespace:
545
545
  def _make_fig(pdf: Any, title: str) -> "BokehFigure":
546
546
  hover_cols = [c for c in pdf.columns if c not in ("geometry", "geom")]
547
547
  tooltips = [(c, f"@{{{c}}}") for c in hover_cols]
548
- fig = figure( # type: ignore[call-arg]
548
+ fig = figure(
549
549
  width=900,
550
550
  height=400,
551
551
  x_axis_label=x,
@@ -658,7 +658,10 @@ class PlotNamespace:
658
658
  if len(unique_entities) > top_n:
659
659
  entities = unique_entities.limit(top_n).to_list()
660
660
  entity_df = entity_df.join(
661
- pl.DataFrame({key_col: entities}), on=key_col, how="inner"
661
+ pl.DataFrame({key_col: entities}),
662
+ on=key_col,
663
+ how="inner",
664
+ maintain_order="left",
662
665
  )
663
666
  entity_pdf = entity_df.to_pandas()
664
667
 
@@ -716,12 +719,17 @@ class PlotNamespace:
716
719
  unique_entities = df[key_col].unique()
717
720
  if len(unique_entities) > top_n:
718
721
  entities = unique_entities.limit(top_n).to_list()
719
- df = df.join(pl.DataFrame({key_col: entities}), on=key_col, how="inner")
722
+ df = df.join(
723
+ pl.DataFrame({key_col: entities}),
724
+ on=key_col,
725
+ how="inner",
726
+ maintain_order="left",
727
+ )
720
728
 
721
729
  def _make_scatter(y_col: str) -> "BokehFigure":
722
730
  from bokeh.models import ColumnDataSource
723
731
 
724
- fig = figure( # type: ignore[call-arg]
732
+ fig = figure(
725
733
  width=800,
726
734
  height=450,
727
735
  x_axis_label=x,
@@ -815,12 +823,17 @@ class PlotNamespace:
815
823
  unique_entities = df[key_col].unique()
816
824
  if len(unique_entities) > top_n:
817
825
  entities = unique_entities.limit(top_n).to_list()
818
- df = df.join(pl.DataFrame({key_col: entities}), on=key_col, how="inner")
826
+ df = df.join(
827
+ pl.DataFrame({key_col: entities}),
828
+ on=key_col,
829
+ how="inner",
830
+ maintain_order="left",
831
+ )
819
832
 
820
833
  df_pd = df.to_pandas()
821
834
 
822
835
  def _make_hist(x_col: str) -> "BokehFigure":
823
- fig = figure( # type: ignore[call-arg]
836
+ fig = figure(
824
837
  width=800,
825
838
  height=400,
826
839
  x_axis_label=x_col,
@@ -902,7 +915,7 @@ class PlotNamespace:
902
915
 
903
916
  from bokeh.models import FactorRange
904
917
 
905
- fig = figure( # type: ignore[call-arg]
918
+ fig = figure(
906
919
  width=600,
907
920
  height=550,
908
921
  x_range=FactorRange(factors=cols),
@@ -993,7 +1006,7 @@ class PlotNamespace:
993
1006
 
994
1007
  mapper = LinearColorMapper(palette="Viridis256", low=low, high=high)
995
1008
 
996
- fig = figure( # type: ignore[call-arg]
1009
+ fig = figure(
997
1010
  width=900,
998
1011
  height=500,
999
1012
  x_range=FactorRange(factors=x_vals),
@@ -1046,7 +1059,7 @@ class PlotNamespace:
1046
1059
  for row_col in num_cols:
1047
1060
  row: list[Any] = []
1048
1061
  for col_col in num_cols:
1049
- fig = figure( # type: ignore[call-arg]
1062
+ fig = figure(
1050
1063
  width=200,
1051
1064
  height=200,
1052
1065
  x_axis_label=col_col if row_col == num_cols[-1] else "",
@@ -293,7 +293,7 @@ class StatsNamespace:
293
293
  columns=["dw_precipitation", "ci_cropping_intensity"],
294
294
  method=CorrelateMethod.PEARSON,
295
295
  )
296
- print(result.data.collect())
296
+ print(result.data.collect(engine="in-memory"))
297
297
 
298
298
  Per-MWS correlation, ranked by strength of dependence::
299
299
 
@@ -310,7 +310,7 @@ class StatsNamespace:
310
310
  .sort("abs_corr", descending=True)
311
311
  .select("mws_id", "correlation", "p_value", "n_obs")
312
312
  .limit(5)
313
- .collect()
313
+ .collect(engine="in-memory")
314
314
  )
315
315
  print(top5)
316
316
  """
@@ -378,18 +378,20 @@ class StatsNamespace:
378
378
  )
379
379
 
380
380
  # --- single collect boundary: scipy.t.sf needs eager numpy ---
381
- result = lazy_result.collect()
381
+ result = lazy_result.collect(engine="in-memory")
382
382
  t_vals = result["t_stat"].to_numpy()
383
383
  df_vals = result["n_obs"].to_numpy() - 2
384
384
  p_vals = 2.0 * sp.t.sf(np.abs(t_vals), df_vals)
385
- result = result.with_columns(pl.Series("p_value", p_vals))
385
+ result = result.with_columns(
386
+ pl.Series("p_value", np.atleast_1d(p_vals))
387
+ )
386
388
 
387
389
  data = result.select([group_by, "correlation", "p_value", "n_obs"])
388
390
 
389
391
  else:
390
392
  # Kendall: scipy has no batched/lazy equivalent, so this branch
391
393
  # is inherently eager. Collect ONCE here — not scattered filters.
392
- df = lf.collect()
394
+ df = lf.collect(engine="in-memory")
393
395
  rows: list[dict[str, Any]] = []
394
396
  for sub_full in df.partition_by(group_by):
395
397
  key = sub_full[group_by][0]
@@ -422,7 +424,7 @@ class StatsNamespace:
422
424
 
423
425
  # Pooled mode — eager needed regardless (scipy.pearsonr/spearmanr/
424
426
  # kendalltau all take raw numpy arrays). Collect ONCE here.
425
- df = lf.collect()
427
+ df = lf.collect(engine="in-memory")
426
428
  n_obs = len(df)
427
429
 
428
430
  rows = []
@@ -674,7 +676,9 @@ class StatsNamespace:
674
676
  .select(join_keys + [column])
675
677
  .rename({column: "value_to"})
676
678
  )
677
- joined = from_df.join(to_df, on=join_keys, how="inner").with_columns(
679
+ joined = from_df.join(
680
+ to_df, on=join_keys, how="inner", maintain_order="left"
681
+ ).with_columns(
678
682
  [
679
683
  pl.lit(from_period).alias("year_from").cast(pl.Int32),
680
684
  pl.lit(to_period).alias("year_to").cast(pl.Int32),
@@ -800,7 +804,7 @@ class StatsNamespace:
800
804
 
801
805
  df = self._r.df()
802
806
  if isinstance(df, pl.LazyFrame):
803
- df = df.collect()
807
+ df = df.collect(engine="in-memory")
804
808
  key = self._r.key_cols[0]
805
809
 
806
810
  if mode == "cross_sectional":
@@ -945,20 +949,22 @@ class StatsNamespace:
945
949
  n_dropped = counts.height - valid_keys.height
946
950
 
947
951
  med_df = (
948
- base_df.join(valid_keys, on=key)
952
+ base_df.join(valid_keys, on=key, maintain_order="left")
949
953
  .group_by(key)
950
954
  .agg(pl.col(column).median().alias("_med"))
951
955
  )
952
956
  mad_df = (
953
- base_df.join(valid_keys, on=key)
954
- .join(med_df, on=key)
957
+ base_df.join(valid_keys, on=key, maintain_order="left")
958
+ .join(med_df, on=key, maintain_order="left")
955
959
  .with_columns(
956
960
  (pl.col(column) - pl.col("_med")).abs().alias("_absdev")
957
961
  )
958
962
  .group_by(key)
959
963
  .agg(pl.col("_absdev").median().alias("_mad"))
960
964
  )
961
- stats_df = med_df.join(mad_df, on=key).with_columns(
965
+ stats_df = med_df.join(
966
+ mad_df, on=key, maintain_order="left"
967
+ ).with_columns(
962
968
  (pl.col("_mad") * 1.4826).clip(lower_bound=1e-12).alias("_scale")
963
969
  )
964
970
 
@@ -971,7 +977,7 @@ class StatsNamespace:
971
977
  )
972
978
 
973
979
  data = (
974
- eval_df.join(stats_df, on=key, how="inner")
980
+ eval_df.join(stats_df, on=key, how="inner", maintain_order="left")
975
981
  .with_columns(
976
982
  ((pl.col(column) - pl.col("_med")) / pl.col("_scale")).alias(
977
983
  "anomaly_score"
@@ -1045,9 +1051,9 @@ class StatsNamespace:
1045
1051
  26 if len(full_vals) >= 52 else max(2, len(full_vals) // 2)
1046
1052
  )
1047
1053
  min_required = 2 * period
1048
- min_reliable = (
1049
- 3 * period
1050
- ) # want ~3 cycles minimum for a trustworthy seasonal fit, not just 2
1054
+ # min_reliable = (
1055
+ # 3 * period
1056
+ # ) # want ~3 cycles minimum for a trustworthy seasonal fit, not just 2
1051
1057
 
1052
1058
  # if len(full_vals) < min_reliable:
1053
1059
  # warnings.warn(
@@ -1203,7 +1209,12 @@ class StatsNamespace:
1203
1209
  if spec is None:
1204
1210
  # Pull from result.data directly.
1205
1211
  if col_name in df.columns:
1206
- feat = feat.join(df.select([key, col_name]), on=key, how="left")
1212
+ feat = feat.join(
1213
+ df.select([key, col_name]),
1214
+ on=key,
1215
+ how="left",
1216
+ maintain_order="left",
1217
+ )
1207
1218
  # else silently skip — not in data and no spec provided
1208
1219
  continue
1209
1220
 
@@ -1286,7 +1297,12 @@ class StatsNamespace:
1286
1297
  else:
1287
1298
  fetched = collect_lf(col_lf.group_by(key).agg(agg_expr))
1288
1299
 
1289
- feat = feat.join(fetched.select([key, col_name]), on=key, how="left")
1300
+ feat = feat.join(
1301
+ fetched.select([key, col_name]),
1302
+ on=key,
1303
+ how="left",
1304
+ maintain_order="left",
1305
+ )
1290
1306
 
1291
1307
  feature_cols = [c for c in feat.columns if c != key]
1292
1308
 
@@ -184,13 +184,24 @@ class Result:
184
184
  # 2. Push down the geometry filter into the Parquet reader using is_in
185
185
  # so we don't scan the entire static geometry file (which takes ~0.4s).
186
186
  geo_lf = self.entity.geometry_lazy
187
+ geo_schema = geo_lf.collect_schema()
188
+ cast_exprs = [
189
+ pl.col(k).cast(geo_schema[k])
190
+ for k in key_cols
191
+ if k in geo_schema and keys_df[k].dtype != geo_schema[k]
192
+ ]
193
+ if cast_exprs:
194
+ keys_df = keys_df.with_columns(cast_exprs)
195
+
187
196
  if len(key_cols) == 1 and keys_df.height < 50000:
188
197
  key = key_cols[0]
189
198
  geo_lf = geo_lf.filter(pl.col(key).is_in(keys_df[key].to_list()))
190
199
  else:
191
- geo_lf = geo_lf.join(keys_df.lazy(), on=key_cols, how="semi")
200
+ geo_lf = geo_lf.join(
201
+ keys_df.lazy(), on=key_cols, how="semi", maintain_order="left"
202
+ )
192
203
 
193
- joined = res.data.join(geo_lf, on=key_cols, how="left")
204
+ joined = res.data.join(geo_lf, on=key_cols, how="left", maintain_order="left")
194
205
 
195
206
  # 3. Cache the joined geometry so multiple plots are instant.
196
207
  return res._replace(data=joined, has_geometry=True).materialise()
@@ -14,6 +14,7 @@ from core_lens.utils.polars_utils import (
14
14
  scan_with_key_filter,
15
15
  collect_lf,
16
16
  cached_read_schema,
17
+ cast_filter_values,
17
18
  )
18
19
 
19
20
  if TYPE_CHECKING:
@@ -129,20 +130,22 @@ class View:
129
130
 
130
131
  filter_expr = pl.lit(True)
131
132
  for col, val in kwargs.items():
132
- is_list_col = isinstance(schema_types.get(col), pl.List)
133
+ col_dtype = schema_types.get(col)
134
+ is_list_col = isinstance(col_dtype, pl.List)
135
+ cast_val = cast_filter_values(val, col_dtype)
133
136
  if isinstance(val, list):
134
137
  if is_list_col:
135
138
  filter_expr = (
136
139
  filter_expr
137
- & pl.col(col).list.eval(pl.element().is_in(val)).list.any()
140
+ & pl.col(col).list.eval(pl.element().is_in(cast_val)).list.any()
138
141
  )
139
142
  else:
140
- filter_expr = filter_expr & pl.col(col).is_in(val)
143
+ filter_expr = filter_expr & pl.col(col).is_in(cast_val)
141
144
  else:
142
145
  if is_list_col:
143
- filter_expr = filter_expr & pl.col(col).list.contains(val)
146
+ filter_expr = filter_expr & pl.col(col).list.contains(cast_val)
144
147
  else:
145
- filter_expr = filter_expr & (pl.col(col) == val)
148
+ filter_expr = filter_expr & (pl.col(col) == cast_val)
146
149
 
147
150
  keys = lf.filter(filter_expr).select(self.entity.key_cols)
148
151
 
@@ -202,8 +205,10 @@ class View:
202
205
  candidates = bbox_intersects_geometry(self.entity._index, geometry)
203
206
  candidates = (
204
207
  candidates.lazy()
205
- .join(self.keys, on=self.entity.key_cols, how="inner")
206
- .collect()
208
+ .join(
209
+ self.keys, on=self.entity.key_cols, how="inner", maintain_order="left"
210
+ )
211
+ .collect(engine="in-memory")
207
212
  )
208
213
 
209
214
  keys_df = exact_spatial_filter(
@@ -521,8 +526,11 @@ class View:
521
526
  data_df.select(self.entity.key_cols).lazy(),
522
527
  on=self.entity.key_cols,
523
528
  how="semi",
524
- ).collect()
525
- data_df = data_df.join(geom_df, on=self.entity.key_cols, how="left")
529
+ maintain_order="left",
530
+ ).collect(engine="in-memory")
531
+ data_df = data_df.join(
532
+ geom_df, on=self.entity.key_cols, how="left", maintain_order="left"
533
+ )
526
534
 
527
535
  from core_lens.utils.spatial import execute_spatial_join
528
536